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.

235 lines
6.4KB

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