scripts and tools to administer the lingy.in public unix / tilde
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

254 行
6.9KB

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