scripts and tools to administer the lingy.in public unix / tilde
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
6.9KB

  1. import glob
  2. import json
  3. import re
  4. import sshpubkeys
  5. from flask import Flask, redirect, url_for, render_template, request
  6. # lyadmin
  7. # scripts and web form for a tilde / PAUS instance
  8. #
  9. # gashapwn
  10. # Nov 2020
  11. #
  12. # https://git.lain.church/gashapwn/lyadmin
  13. # gashapwn@protonmail.com
  14. # or
  15. # gasahwpn on irc.lainchan.org
  16. app=Flask(__name__)
  17. # Paths for conf file,
  18. # user list,
  19. # directory containing
  20. # account request files...
  21. ACCOUNT_PATH = "./req/";
  22. CONF_FN = "lyadmin.conf.json"
  23. CONF_PATH = "./" + str(CONF_FN)
  24. # validation stuff
  25. MAX_PUB_KEY_LEN = 5000
  26. EMAIL_REGEX = "^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,10}$"
  27. KEY_REGEX = "^[ -~]+$"
  28. # Account requests are given ID numbers
  29. # the first request will have the below
  30. # id number
  31. INIT_REQ_ID = "00000"
  32. # The main home page
  33. @app.route("/")
  34. def home():
  35. app.route('/')
  36. # Load the list of tilde users
  37. # to generate links for
  38. u_list = [];
  39. with open("user_list.txt") as u_file:
  40. for line in u_file.readlines():
  41. u_list.append(line.strip());
  42. return render_template("index.html", u_list=u_list, page_name="home")
  43. # Generates the page with rule. No logic needed.
  44. def rules():
  45. return render_template("rules.html")
  46. # Generate HTML for a form widget
  47. def widg_fun(widg):
  48. if(widg.w_type == "input"):
  49. return "input id=id_%s name=%s type=text></input"%(
  50. widg.w_name, widg.w_name
  51. )
  52. elif(widg.w_type == "textarea"):
  53. return "textarea cols=40 id=id_%s name=%s rows=10 required=\"\""%(
  54. widg.w_name, widg.w_name
  55. )
  56. elif(widg.w_type == "check"):
  57. return "input id=id_%s name=%s type=checkbox required=\"\""%(
  58. widg.w_name, widg.w_name)
  59. return widg.w_type;
  60. # Generate HTML for request form
  61. # probably a strange way to do this...
  62. def req():
  63. app.route('/req')
  64. class Widg:
  65. def __init__(self, w_name, w_type, w_opt):
  66. self.w_name = w_name
  67. self.w_type = w_type
  68. self.w_opt = w_opt # only for choice type widg
  69. # Configuration for our request form
  70. rt = {
  71. "username": Widg(
  72. "username",
  73. "input",
  74. None
  75. ),
  76. "email for account lockout / registration confirmation (optional)": Widg(
  77. "email",
  78. "input",
  79. None
  80. ),
  81. "SSH public key": Widg(
  82. "pub_key",
  83. "textarea",
  84. None
  85. ),
  86. "shell of choice": Widg(
  87. "shell",
  88. "choice",
  89. conf_obj["shell_tup_list"]
  90. ),
  91. "have you read the rules?": Widg(
  92. "rule_read", "check", None
  93. )
  94. };
  95. return render_template(
  96. "req.html",
  97. req_tab = rt,
  98. widg_fun = widg_fun,
  99. page_name="req"
  100. )
  101. def handle_invalid_data(req):
  102. # print(str(e))
  103. return render_template("signup.html", is_email_user = False)
  104. # Process input from user creation POST request
  105. def signup():
  106. app.route('/req/signup')
  107. # Get all the params from the POST
  108. # request
  109. username = request.form["username"].strip()
  110. email = request.form["email"].strip()
  111. pub_key = request.form["pub_key"].strip()
  112. shell = request.form["shell"].strip()
  113. rule_read = request.form["rule_read"].strip()
  114. xff_header = request.headers["X-Forwarded-For"]
  115. is_email_user = False;
  116. # If a user didnt read the rules
  117. # send them back
  118. if(rule_read != "on"):
  119. return redirect(url_for('req'))
  120. # Set placeholder if user didnt send an email
  121. if(len(email) > 1):
  122. is_email_user = True
  123. else:
  124. email = "NO_EMAIL"
  125. # Validate shell
  126. if(not shell in conf_obj["shell"]):
  127. print("failed shell validation")
  128. return handle_invalid_data(req)
  129. # Validate email
  130. if( is_email_user and not re.search(EMAIL_REGEX, email)):
  131. print("failed email validation")
  132. return handle_invalid_data(req)
  133. # Validate the SSH pub key
  134. # Most software only handles up to 4096 bit keys
  135. if(len(pub_key) > MAX_PUB_KEY_LEN):
  136. print("key failed len check")
  137. return handle_invalid_data(req)
  138. # Only printable ascii characters in
  139. # a valid key
  140. # if(not re.search("^[ -~]+$", pub_key)):
  141. if(not re.search(KEY_REGEX, pub_key)):
  142. print("key failed regex")
  143. return handle_invalid_data(req)
  144. # Check the key against a library
  145. key = sshpubkeys.SSHKey(
  146. pub_key,
  147. strict_mode=False,
  148. skip_option_parsing=True
  149. )
  150. try:
  151. key.parse()
  152. except Exception as e:
  153. print("key failed lib validation")
  154. return handle_invalid_data(request)
  155. if(len(xff_header) < 1):
  156. xff_header = "NO_XFF"
  157. # All users requests have a sequential ID
  158. # The below picks the next ID based on
  159. # how many requests we already have saved
  160. # to disk
  161. if(len(glob.glob(ACCOUNT_PATH + str("[0-9]*ident*"))) == 0):
  162. new_id = int(INIT_REQ_ID)
  163. new_id_str = INIT_REQ_ID
  164. else:
  165. max_id = max(
  166. list(map(
  167. lambda path : path.split("/")[-1].split(".")[0],
  168. glob.glob(str(ACCOUNT_PATH) + "[0-9]*ident*")))
  169. )
  170. zpad = len(max_id)
  171. new_id = int(max_id)+1
  172. new_id_str = str(new_id).zfill(zpad)
  173. # write the request to disk
  174. # fn1 = str(FULL_PATH) + str(new_id_str) + ".ident"
  175. fn1 = str(ACCOUNT_PATH) + str(new_id_str) + ".ident"
  176. with open(fn1, "w") as ident_file:
  177. ident_file.write(str(username) + "\n")
  178. ident_file.write(str(email) + "\n")
  179. ident_file.write(str(shell) + "\n")
  180. ident_file.write(str(pub_key) + "\n")
  181. ident_file.write(str(xff_header) + "\n")
  182. return render_template("signup.html", is_email_user = is_email_user)
  183. @app.context_processor
  184. def get_site_name():
  185. return {"site_name": conf_obj["site_name"]}
  186. @app.context_processor
  187. def get_admin_email():
  188. return {"admin_email": conf_obj["admin_email"]}
  189. def check_pwd_for_conf():
  190. pwd_file_list = list(map(
  191. lambda path : path.split("/")[-1],
  192. glob.glob("*")
  193. ))
  194. if(not CONF_FN in pwd_file_list):
  195. print("could not find " + str(CONF_PATH))
  196. print("please run in the installation directory")
  197. return False
  198. return True
  199. # MAIN STARTS HERE
  200. if(__name__=="__main__" and check_pwd_for_conf()):
  201. # Slurp the conf file
  202. with open(CONF_PATH) as c: conf_json_str = c.read()
  203. conf_obj = json.loads(conf_json_str)
  204. # A global list of all the shell enums
  205. conf_obj["shell_tup_list"] = list(map(
  206. lambda k : (
  207. k, conf_obj["shell"][k]
  208. ),
  209. list(conf_obj["shell"].keys())
  210. ))
  211. # Setup URL rules
  212. app.add_url_rule('/rules', 'rules', rules)
  213. app.add_url_rule('/req', 'req', req, methods = ['POST', 'GET'])
  214. app.add_url_rule('/req/signup', 'signup', signup, methods = ['POST'])
  215. # Run that app!
  216. app.run(host=conf_obj["listen_ip"],debug=True)