submit-server.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #!/usr/bin/env python3
  2. '''
  3. Simple HTTP server that accepts POST request
  4. from submit.html and stores them in a folder
  5. '''
  6. import traceback
  7. import argparse
  8. import http.server
  9. import cgi
  10. import uuid
  11. import json
  12. import time
  13. import ssl
  14. import os
  15. import re
  16. # for debugging
  17. import cgitb
  18. cgitb.enable()
  19. # byte size of a single submission
  20. INBOX_SIZE_BYTES = 1_000_000_000
  21. # minimum seconds between submissions
  22. SUBMIT_INTERVAL_SEC = 20
  23. last_submission = 0
  24. fn_regex = re.compile(r'^[0-9a-zA-Z_.-]{3,64}$')
  25. def check_file_name(filename):
  26. return bool(fn_regex.match(filename))
  27. def check_text_value(text):
  28. return len(text) < 64
  29. def check_file_size(data):
  30. return len(data) < (10*1000*1000)
  31. def get_total_size(start_path):
  32. total_size = 0
  33. for dirpath, dirnames, filenames in os.walk(start_path):
  34. for f in filenames:
  35. fp = os.path.join(dirpath, f)
  36. # skip if it is symbolic link
  37. if not os.path.islink(fp):
  38. total_size += os.path.getsize(fp)
  39. return total_size
  40. def store_submission(form):
  41. global last_submission
  42. if (time.time() - last_submission) < SUBMIT_INTERVAL_SEC:
  43. next = int(SUBMIT_INTERVAL_SEC - (time.time() - last_submission))
  44. return (False, "Please wait {} seconds for the next submission slot!".format(next))
  45. os.makedirs("/tmp/sticker_submissions", exist_ok=True)
  46. if get_total_size("/tmp/sticker_submissions") > INBOX_SIZE_BYTES:
  47. return (False, "Submission directory is full. Alert the maintainer!")
  48. output = {}
  49. for key in ["tags", "notes", "notes", "link", "language", "license"]:
  50. if key in form:
  51. value = form[key].value.strip()
  52. if len(value) == 0:
  53. continue
  54. if check_text_value(value):
  55. output[key] = value
  56. else:
  57. return (False, "Invalid field {}.".format(key))
  58. files = {}
  59. if "files[]" in form:
  60. entries = form["files[]"]
  61. # make sure entries is a list
  62. if isinstance(entries, cgi.FieldStorage):
  63. entries = [entries]
  64. if len(entries) > 3:
  65. return (False, "Too many files.")
  66. for entry in entries:
  67. file_name = os.path.basename(entry.filename)
  68. file_data = entry.value
  69. if not check_file_size(file_data):
  70. return (False, "File too big: {}".format(file_name))
  71. if not check_file_name(file_name):
  72. return (False, "Invalid file name: {}".format(file_name))
  73. files[file_name] = file_data
  74. # store submission here
  75. path = "/tmp/sticker_submissions/{}".format(uuid.uuid4())
  76. print("Create directory {}".format(path))
  77. os.mkdir(path)
  78. # write json
  79. if len(output) > 0:
  80. with open(f"{path}/data.json", "w") as file:
  81. json.dump(output, file, indent=" ", sort_keys=True)
  82. if len(files) > 0:
  83. for file_name, file_data in files.items():
  84. with open("{}/{}".format(path, file_name), "wb") as file:
  85. file.write(file_data)
  86. last_submission = time.time()
  87. return (True, "Success - Thank you for your contribution!")
  88. class MyHandler(http.server.BaseHTTPRequestHandler):
  89. def _set_response(self):
  90. self.send_response(200)
  91. self.send_header('Content-type', 'text/html')
  92. self.end_headers()
  93. def do_OPTIONS(self):
  94. self.send_response(200, "ok")
  95. self.send_header('Access-Control-Allow-Credentials', 'false')
  96. self.send_header('Access-Control-Allow-Origin', 'https://mwarning.github.io')
  97. self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
  98. self.send_header('Access-Control-Allow-Headers', '*')
  99. def do_POST(self, *args, **kwargs):
  100. #content_len = int(self.headers.get('content-length'))
  101. #pdict['CONTENT-LENGTH'] = content_len
  102. try:
  103. ctype, pdict = cgi.parse_header(self.headers.get('content-type'))
  104. success = False
  105. message = 'unhandled data format'
  106. if ctype == 'multipart/form-data':
  107. pdict['boundary'] = bytes(pdict['boundary'], "utf-8") # hack
  108. form = cgi.FieldStorage(
  109. fp=self.rfile,
  110. headers=self.headers,
  111. environ={'REQUEST_METHOD':'POST'})
  112. success, message = store_submission(form)
  113. body = bytes(message, 'utf-8')
  114. code = 200 if success else 400
  115. self.send_response(200)
  116. self.send_header('Access-Control-Allow-Credentials', 'true')
  117. self.send_header('Access-Control-Allow-Origin', '*')
  118. self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
  119. self.send_header('Content-type', 'text/plain')
  120. self.send_header('Content-length', str(len(body)))
  121. self.end_headers()
  122. self.wfile.write(body)
  123. except Exception as e:
  124. traceback.print_exc()
  125. print(e)
  126. if __name__ == "__main__":
  127. parser = argparse.ArgumentParser()
  128. parser.add_argument("--listen", default="0.0.0.0")
  129. parser.add_argument("--port", type=int, default=4223)
  130. parser.add_argument("--certfile", default="", help="Private key file in PEM format.")
  131. parser.add_argument("--keyfile", default="", help="Public key file in PEM format")
  132. args = parser.parse_args()
  133. print("Listen on {} port {}".format(args.listen, args.port))
  134. try:
  135. server = http.server.HTTPServer((args.listen, args.port), MyHandler)
  136. if len(args.certfile) > 0 and len(args.keyfile) > 0:
  137. server.socket = ssl.wrap_socket(server.socket, certfile=args.certfile, keyfile=args.keyfile, server_side=True)
  138. server.serve_forever()
  139. except KeyboardInterrupt:
  140. server.socket.close()