No Description

main.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. #!/usr/bin/env python3
  2. from flask import Flask, request, abort, make_response, \
  3. render_template, redirect, url_for, \
  4. json, jsonify, session, flash
  5. from collections import namedtuple, OrderedDict
  6. from subprocess import check_call, call
  7. from fcntl import flock, LOCK_EX, LOCK_SH, LOCK_UN
  8. import hashlib
  9. import random
  10. import shutil
  11. import time
  12. import glob
  13. import sys
  14. import re
  15. import os
  16. #app = Flask("ISABEL-2 Verifier") # That app name breaks Ubuntu 14.04 :-o
  17. app = Flask("main")
  18. app.secret_key = "6ab77f3c45447429c2ae163c260a626029519a66450e474c"
  19. users_file = "/etc/freeradius/dpto2/users"
  20. dhcp_hosts_file = "/etc/dnsmasq.d/dpto2/dhcp-hosts"
  21. dhcp_opts_file = "/etc/dnsmasq.d/dpto2/dhcp-opts"
  22. dnsmasq_leases = "/var/lib/misc/dnsmasq.leases"
  23. impossible_mac = "ff:ff:ff:ff:ff:fe"
  24. class Lock:
  25. def __init__(self, lockfile):
  26. self.lockfile = lockfile
  27. def __enter__(self):
  28. flock(self.lockfile, LOCK_EX)
  29. def __exit__(self, exc_type, exc_val, exc_tb):
  30. flock(self.lockfile, LOCK_UN)
  31. class SharedLock(Lock):
  32. def __init__(self, lockfile):
  33. super().__init__(lockfile)
  34. def __enter__(self, exc_type, exc_val, exc_tb):
  35. flock(self.lockfile, LOCK_SH)
  36. lock_file = open("/run/lock/dpto2.lock","w")
  37. exclusive_lock = Lock(lock_file)
  38. shared_lock = SharedLock(lock_file)
  39. def reload_freeradius():
  40. call("./sendhup freeradius".split())
  41. def reload_dnsmasq():
  42. call("./sendhup dnsmasq".split())
  43. def nthash(password):
  44. return hashlib.new('md4',password.encode('utf-16le')).hexdigest().upper()
  45. def create_user(username, password, creator):
  46. with exclusive_lock:
  47. f = open(users_file,"a")
  48. f.write('# created by {}\n{} NT-Password := "{}"\n'.format(creator, username, nthash(password)))
  49. f.close()
  50. reload_freeradius()
  51. def delete_user(deluser):
  52. f = open(users_file)
  53. lines = f.readlines()
  54. f.close()
  55. timestamp = time.time()
  56. tempfile = "{}.{}".format(users_file, timestamp)
  57. f = open(tempfile,"w")
  58. createdby = None
  59. for line in lines:
  60. if re.search("#\s+created by", line):
  61. createdby = line
  62. continue
  63. if line.startswith(deluser):
  64. createdby = None
  65. continue
  66. if line.startswith("#"):
  67. f.write(line)
  68. else:
  69. if createdby:
  70. f.write(createdby)
  71. f.write(line)
  72. createdby = None
  73. f.close()
  74. rename(tempfile, users_file)
  75. reload_freeradius()
  76. def load_users():
  77. users = []
  78. creator = None
  79. with open(users_file) as f:
  80. for l in f:
  81. l = l.strip()
  82. if l.startswith("#"):
  83. m = re.match("#\s+created by\s+(\S+)", l)
  84. if m:
  85. creator = m.group(1)
  86. continue
  87. m = re.match("(^\S+).*-Password\s+:=\s+\"(\S+)\"", l)
  88. if m:
  89. users.append((m.group(1), m.group(2), creator))
  90. creator = None
  91. return users
  92. def login_required(f):
  93. def g(*args, **kwargs):
  94. if not session.get('logged_in', False):
  95. return redirect(url_for('login', redirect_to=request.url))
  96. return f(*args, **kwargs)
  97. g.__name__ = f.__name__
  98. return g
  99. @app.route("/login",methods=['GET','POST'])
  100. def login():
  101. if session.get('logged_in',False):
  102. return redirect(url_for(request.args.get('redirect_to','admin')))
  103. if request.method == 'GET':
  104. return render_template("login.html")
  105. if request.method == 'POST':
  106. username = request.form.get("username",None)
  107. password = request.form.get("password",None)
  108. if username is None or password is None:
  109. return render_template("login.html",error=True,errormsg="invalid username or password")
  110. if username == 'guest':
  111. return render_template("login.html",error=True,errormsg="guest user has no admin privileges")
  112. try:
  113. check_login(username, password)
  114. except Exception as e:
  115. return render_template("login.html",error=True,errormsg=str(e))
  116. session['logged_in'] = True
  117. session['username'] = username
  118. return redirect(request.args.get('redirect_to','admin'))
  119. @app.route("/")
  120. def index():
  121. f = open(users_file)
  122. guestpass = "?"
  123. for line in f:
  124. if line.startswith("guest"):
  125. m = re.search(':=\s+"(.+?)"\s*$',line)
  126. if m:
  127. guestpass = m.group(1)
  128. break
  129. return render_template("index.html", guestpass=guestpass)
  130. def check_login(username, password):
  131. for u,p,c in load_users():
  132. if u == username and p == nthash(password):
  133. return True
  134. raise ValueError("Invalid username or password")
  135. @app.route("/admin",methods=['GET','POST'])
  136. @login_required
  137. def admin():
  138. if request.method == 'POST':
  139. deluser = request.form.get('deluser',None)
  140. if deluser is not None:
  141. if deluser == 'guest':
  142. return render_template("admin.html", delete_error=True, errormsg="Cannot delete guest user")
  143. delete_user(deluser)
  144. flash("User deleted succesfully")
  145. username = request.form.get('username',None)
  146. pass1 = request.form.get('password1',None)
  147. pass2 = request.form.get('password2',None)
  148. creator = session['username']
  149. if username is not None:
  150. if username == 'guest':
  151. return render_template("admin.html", create_error=True, errormsg="Cannot create guest user")
  152. if pass1 is None or \
  153. pass2 is None or \
  154. pass1 != pass2:
  155. return render_template("admin.html", create_error=True, errormsg="Passwords do not match")
  156. if pass1 == '':
  157. return render_template("admin.html", create_error=True, errormsg="Password cannot be empty")
  158. create_user(username,pass1,creator)
  159. flash("User created successfully")
  160. return render_template("admin.html")
  161. def render_users_tree(tree):
  162. lines = []
  163. def _render(user, edges, is_last):
  164. lines.append((edges + (" └─" if is_last else " ├─"), user))
  165. n = len(tree[user])
  166. for i,u in enumerate(tree[user]):
  167. _render(u, edges + (" " if is_last else " │ "), i == n - 1)
  168. return lines
  169. # root = tree(""); n = len(root)
  170. # for i,u in enumerate(root):
  171. _render("", "", True)
  172. return lines
  173. @app.route("/users")
  174. @login_required
  175. def list_users():
  176. users = load_users()
  177. users_set = set(x[0] for x in users)
  178. tree = { "": [] }
  179. for user,passwd,creator in users:
  180. if user == "guest":
  181. continue
  182. tree[user] = []
  183. if creator is None:
  184. tree[""].append(user)
  185. else:
  186. if creator not in users_set:
  187. creator += " (invalid username)"
  188. tree[""].append(creator)
  189. tree[creator] = tree.get(creator,[]) + [user]
  190. lines = render_users_tree(tree)
  191. return render_template("users.html", users=lines)
  192. mac_re = re.compile("(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}")
  193. ip_re_str = "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
  194. ip_re = re.compile(ip_re_str)
  195. DhcpHostLine = namedtuple("DhcpHostLine","macs ip tag")
  196. def parse_dchp_host(line):
  197. macs = []
  198. ip = ""
  199. tag = None
  200. parts = line.split(",")
  201. for p in parts:
  202. p = p.strip()
  203. m = mac_re.match(p)
  204. if m:
  205. if m.group(0) != impossible_mac:
  206. macs.append(m.group(0))
  207. continue
  208. m = ip_re.match(p)
  209. if m:
  210. ip = m.group(0)
  211. continue
  212. if p.startswith("set:"):
  213. tag = p[4:]
  214. return DhcpHostLine(macs, ip, tag)
  215. Ip = namedtuple("Ip","reserved_by dhcp_pool locked macs gw color")
  216. def load_ips():
  217. ipmap = {}
  218. for i in range(1,255):
  219. ipmap[i] = Ip(
  220. reserved_by="",
  221. dhcp_pool=True,
  222. locked=False,
  223. macs=[],
  224. gw=None,
  225. color=None
  226. )
  227. ipmap[0] = ipmap[255] = Ip(
  228. reserved_by="",
  229. dhcp_pool=False,
  230. locked=False,
  231. macs=[],
  232. gw=None,
  233. color=None
  234. )
  235. prefix = None
  236. # Read custom gateways
  237. gws = {}
  238. with open(dhcp_opts_file) as f:
  239. for line in f:
  240. line = line.strip()
  241. if line.startswith("#"): continue
  242. m = re.match(
  243. "tag:(client[0-9a-f]{12}),option:router,(" + ip_re_str + ")",
  244. line
  245. )
  246. if m:
  247. gws[m.group(1)] = m.group(2)
  248. with open(dhcp_hosts_file) as f:
  249. meta = {}
  250. for line in f:
  251. line = line.strip()
  252. if line.startswith("#"):
  253. try:
  254. meta = json.loads(line[1:])
  255. except:
  256. meta = {}
  257. continue
  258. if not isinstance(meta,dict):
  259. meta = {}
  260. continue
  261. if re.match("^[0-9a-fA-F]{2}:",line):
  262. r = parse_dchp_host(line)
  263. ip = int(r.ip.split(".")[-1])
  264. if prefix is None:
  265. prefix = ".".join(r.ip.split(".")[:-1])
  266. ipmap[ip] = Ip(
  267. reserved_by=meta.get("reserved-by",""),
  268. dhcp_pool=False,
  269. locked=meta.get("locked",False),
  270. macs=r.macs,
  271. gw=gws.get(r.tag, None),
  272. color = meta.get("color", None)
  273. )
  274. meta = {}
  275. ipmap["prefix"] = prefix
  276. return ipmap
  277. def load_leases():
  278. leases = {}
  279. with open(dnsmasq_leases) as f:
  280. for line in f:
  281. parts = line.strip().split()
  282. try:
  283. ip = int(parts[2].split(".")[-1])
  284. except IndexError:
  285. continue
  286. except ValueError:
  287. continue
  288. try:
  289. lease = [
  290. int(parts[0]), # expiration timestamp
  291. parts[1], # client mac address
  292. parts[2], # leased ip
  293. parts[3], # host name or * if no name sent
  294. parts[4], # client id or * if no id sent
  295. ]
  296. except IndexError:
  297. # line is shorter than I thought, skippping
  298. continue
  299. leases[ip] = lease
  300. return leases
  301. @app.route("/ips")
  302. @login_required
  303. def ips():
  304. ipmap = load_ips()
  305. leases = load_leases()
  306. return render_template("ips.html", ipmap=ipmap, leases=leases)
  307. def parse_macs(macs):
  308. r = []
  309. macs = macs.strip()
  310. parts = macs.split(',')
  311. for p in parts:
  312. p = p.strip()
  313. m = mac_re.match(p)
  314. if m:
  315. r.append(m.group(0).replace("-",":"))
  316. return r
  317. def rename(src, dst):
  318. shutil.move(src, dst)
  319. shutil.copy(dst, src)
  320. copies = glob.glob(dst + ".[0-9]*")
  321. # sort from most recent to oldest
  322. copies = sorted(copies, key=lambda x: -float(x[len(dst)+1:]))
  323. # keep only the most recent 5
  324. for c in copies[5:]:
  325. print("Deleting", c)
  326. os.remove(c)
  327. def write_dhcp_files(ipmap):
  328. timestamp = time.time()
  329. tempfile = "{}.{}".format(dhcp_hosts_file, timestamp)
  330. with open(tempfile,"w") as f:
  331. prefix = ipmap["prefix"]
  332. gws = {}
  333. for i in range(1,255):
  334. ip = ipmap[i]
  335. if ip.dhcp_pool:
  336. continue
  337. if ip.reserved_by or ip.locked:
  338. meta = OrderedDict()
  339. if ip.reserved_by:
  340. meta['reserved-by'] = ip.reserved_by
  341. if ip.locked:
  342. meta['locked'] = True
  343. if ip.color:
  344. meta['color'] = ip.color
  345. print("# " + json.dumps(meta), file=f)
  346. if len(ip.macs) == 0:
  347. macs = impossible_mac
  348. else:
  349. macs = ",".join(ip.macs)
  350. settag = ""
  351. if ip.gw and len(ip.macs) > 0:
  352. tag = "client" + ip.macs[0].replace(":","").lower()
  353. settag = ",set:" + tag
  354. gws[tag] = ip.gw
  355. print("{},{}.{}{}".format(macs,prefix,i,settag), file=f)
  356. rename(tempfile, dhcp_hosts_file)
  357. # Write custom gateways file
  358. tempfile = "{}.{}".format(dhcp_opts_file, timestamp)
  359. with open(tempfile,"w") as f:
  360. for tag in gws:
  361. print(
  362. "tag:{},option:router,{}".format(tag,gws[tag]),
  363. file=f,
  364. )
  365. rename(tempfile, dhcp_opts_file)
  366. @app.route("/ip/<int:last_byte>", methods=['GET', 'POST'])
  367. @login_required
  368. def ip(last_byte):
  369. valid_colors = "black red gold green blue purple".split()
  370. if request.method == 'POST':
  371. #print(request.form)
  372. reserved_by = request.form.get('reserved-by', '')[:100]
  373. dhcp_pool = request.form.get('dhcp-client', '') == 'pool'
  374. macs = parse_macs(request.form.get('dhcp-client-mac', ''))
  375. gw = request.form.get('dhcp-gw','')[:15]
  376. gw = gw if ip_re.match(gw) else None
  377. color = request.form.get('label-color', '')[:20]
  378. color = color if color in valid_colors else None
  379. with exclusive_lock:
  380. ipmap = load_ips()
  381. if not ipmap[last_byte].locked:
  382. ipmap[last_byte] = Ip(
  383. reserved_by=reserved_by,
  384. dhcp_pool=dhcp_pool,
  385. locked=False,
  386. macs=macs,
  387. gw=gw,
  388. color=color
  389. )
  390. write_dhcp_files(ipmap)
  391. reload_dnsmasq()
  392. return redirect(url_for('ips'))
  393. else:
  394. ipmap = load_ips()
  395. leases = load_leases()
  396. lease = None
  397. try:
  398. if last_byte in leases:
  399. lease = leases[last_byte]
  400. today = time.localtime().tm_wday
  401. expiry_t = time.localtime(int(lease[0]))
  402. expiry_day = expiry_t.tm_wday
  403. expiry_hour = time.strftime("%I:%M %p", expiry_t)
  404. if today == expiry_day:
  405. lease[0] = "las " + expiry_hour
  406. elif (today + 1) % 7 == expiry_day:
  407. lease[0] = "ma\N{LATIN SMALL LETTER N WITH TILDE}ana a las " + expiry_hour
  408. else:
  409. lease[0] = time.strftime("%B %d, ", expiry_t) + expiry_hour
  410. except ValueError:
  411. pass
  412. except IndexError:
  413. pass
  414. addr = "{}.{}".format(ipmap["prefix"], last_byte)
  415. return render_template(
  416. "ip.html",
  417. addr=addr,
  418. valid_colors=valid_colors,
  419. meta=ipmap[last_byte],
  420. lease=lease,
  421. )
  422. @app.route("/logout")
  423. def logout():
  424. session.pop("logged_in",None)
  425. return redirect(url_for("index"))
  426. if __name__ == '__main__':
  427. if "debug" in sys.argv:
  428. users_file = "users.dpto2"
  429. dhcp_hosts_file = "dhcp-hosts.dpto2"
  430. dhcp_opts_file = "dhcp-opts.dpto2"
  431. app.debug = True
  432. app.run(host="0.0.0.0")