Fork of elimage with specific modifications for my instance. https://i.catgirlsin.space
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.

238 lines
7.0KB

  1. #!/usr/bin/env python3
  2. import os
  3. import sys
  4. import logging
  5. import hashlib
  6. from collections import OrderedDict
  7. import mimetypes
  8. import subprocess
  9. from functools import lru_cache
  10. from hmac import compare_digest
  11. from pathlib import Path
  12. import tornado.web
  13. import tornado.template
  14. import tornado.process
  15. import config
  16. from models import model
  17. SCRIPT_PATH = 'elimage'
  18. @lru_cache()
  19. def guess_mime_using_file(path):
  20. result = subprocess.check_output(['file', '-i', path]).decode()
  21. _, mime, encoding = result.split()
  22. mime = mime.rstrip(';')
  23. encoding = encoding.split('=')[-1]
  24. # older file doesn't know webp
  25. if mime == 'application/octet-stream':
  26. result = subprocess.check_output(['file', path]).decode()
  27. _, desc = result.split(None, 1)
  28. if 'Web/P image' in desc:
  29. return 'image/webp', None
  30. # Tornado will treat non-gzip encoding as application/octet-stream
  31. if encoding != 'gzip':
  32. encoding = None
  33. return mime, encoding
  34. def qrencode(s):
  35. return subprocess.check_output(
  36. ['qrencode', '-t', 'UTF8', s]).decode()
  37. mimetypes.guess_type = guess_mime_using_file
  38. def guess_extension(ftype):
  39. if ftype == 'application/octet-stream':
  40. return '.bin'
  41. elif ftype == 'image/webp':
  42. return '.webp'
  43. ext = mimetypes.guess_extension(ftype)
  44. if ext in ('.jpe', '.jpeg'):
  45. ext = '.jpg'
  46. return ext
  47. def get_url_prefix(request):
  48. if HTTPS:
  49. return "https" + "://" + request.host + request.uri
  50. else:
  51. return "http" + "://" + request.host + request.uri
  52. class IndexHandler(tornado.web.RequestHandler):
  53. index_template = None
  54. def get(self):
  55. # self.render() would compress whitespace after it meets '{{' even in <pre>
  56. if self.index_template is None:
  57. template_path = Path(self.settings['template_path'])
  58. try:
  59. file_name = template_path / 'index-site.html'
  60. try:
  61. with open(file_name) as index_file:
  62. text = index_file.read()
  63. except FileNotFoundError:
  64. file_name = template_path / 'index.html'
  65. with open(file_name) as index_file:
  66. text = index_file.read()
  67. self.__class__.index_template = tornado.template.Template(
  68. text, compress_whitespace=False)
  69. except IOError:
  70. logging.exception('failed to open the file: %s', file_name)
  71. raise tornado.web.HTTPError(404, 'index.html is missing')
  72. url_prefix = get_url_prefix(self.request)
  73. if '?' in url_prefix:
  74. url_prefix = url_prefix.split('?', 1)[0]
  75. content = self.index_template.generate(
  76. url=url_prefix,
  77. password_required=bool(self.settings['password'])
  78. )
  79. self.write(content)
  80. def post(self):
  81. # Check the user has been blocked or not
  82. user = model.get_user_by_ip(self.request.remote_ip)
  83. if user is None:
  84. uid = model.add_user(self.request.remote_ip)
  85. else:
  86. if user['blocked']:
  87. raise tornado.web.HTTPError(403, 'You are on our blacklist.')
  88. else:
  89. uid = user['id']
  90. # Check whether password is required
  91. expected_password = self.settings['password']
  92. if expected_password and \
  93. not compare_digest(self.get_argument('password'), expected_password):
  94. raise tornado.web.HTTPError(403, 'You need a valid password to post.')
  95. files = self.request.files
  96. if not files:
  97. raise tornado.web.HTTPError(400, 'upload your image please')
  98. ret = OrderedDict()
  99. url_prefix = get_url_prefix(self.request)
  100. if '?' in url_prefix:
  101. url_prefix = url_prefix.split('?', 1)[0]
  102. url_prefix = url_prefix.rstrip('/')
  103. for filelist in files.values():
  104. for file in filelist:
  105. m = hashlib.sha1()
  106. m.update(file['body'])
  107. h = m.hexdigest()
  108. model.add_image(uid, h, file['filename'], len(file['body']))
  109. d = h[:2]
  110. f = h[2:]
  111. p = os.path.join(self.settings['datadir'], d)
  112. if not os.path.exists(p):
  113. os.mkdir(p, 0o750)
  114. fpath = os.path.join(p, f)
  115. if not os.path.exists(fpath):
  116. try:
  117. with open(fpath, 'wb') as img_file:
  118. img_file.write(file['body'])
  119. except IOError:
  120. logging.exception('failed to open the file: %s', fpath)
  121. ret[file['filename']] = 'FAIL'
  122. self.set_status(500)
  123. continue
  124. ftype = mimetypes.guess_type(fpath)[0]
  125. ext = None
  126. if ftype:
  127. ext = guess_extension(ftype)
  128. if ext:
  129. f += ext
  130. ret[file['filename']] = '%s/%s/%s' % (url_prefix, d, f)
  131. output_qr = self.get_argument('qr', None) is not None
  132. if len(ret) > 1:
  133. for item in ret.items():
  134. self.write('%s: %s\n' % item)
  135. if output_qr:
  136. self.write('%s\n' % qrencode(item[1]))
  137. elif ret:
  138. img_url = tuple(ret.values())[0]
  139. self.write("%s\n" % img_url)
  140. if output_qr:
  141. self.write('%s\n' % qrencode(img_url))
  142. logging.info('%s posted: %s', self.request.remote_ip, ret)
  143. class ToolHandler(tornado.web.RequestHandler):
  144. def get(self):
  145. self.set_header('Content-Type', 'text/x-python')
  146. self.render('elimage', url=self.request.full_url()[:-len(SCRIPT_PATH)])
  147. class HashHandler(tornado.web.RequestHandler):
  148. def get(self, p):
  149. if '.' in p:
  150. h, ext = p.split('.', 1)
  151. ext = '.' + ext
  152. else:
  153. h, ext = p, ''
  154. h = h.replace('/', '')
  155. if len(h) != 40:
  156. raise tornado.web.HTTPError(404)
  157. else:
  158. self.redirect('/%s/%s%s' % (h[:2], h[2:], ext), permanent=True)
  159. def main():
  160. import tornado.httpserver
  161. from tornado.options import define, options
  162. from tornado.platform.asyncio import AsyncIOMainLoop
  163. import asyncio
  164. AsyncIOMainLoop().install()
  165. define("port", default=config.DEFAULT_PORT, help="run on the given port", type=int)
  166. define("address", default='', help="run on the given address", type=str)
  167. define("datadir", default=config.DEFAULT_DATA_DIR, help="the directory to put uploaded data", type=str)
  168. define("fork", default=False, help="fork after startup", type=bool)
  169. define("cloudflare", default=config.CLOUDFLARE, help="check for Cloudflare IPs", type=bool)
  170. define("password", default=config.UPLOAD_PASSWORD, help="optional password", type=str)
  171. tornado.options.parse_command_line()
  172. if options.fork:
  173. if os.fork():
  174. sys.exit()
  175. if options.cloudflare:
  176. import cloudflare
  177. cloudflare.install()
  178. loop = asyncio.get_event_loop()
  179. loop.create_task(cloudflare.updater())
  180. application = tornado.web.Application([
  181. (r"/", IndexHandler),
  182. (r"/" + SCRIPT_PATH, ToolHandler),
  183. (r"/([a-fA-F0-9]{2}/[a-fA-F0-9]{38})(?:\.\w*)?", tornado.web.StaticFileHandler, {
  184. 'path': options.datadir,
  185. }),
  186. (r"/([a-fA-F0-9/]+(?:\.\w*)?)", HashHandler),
  187. ],
  188. datadir=options.datadir,
  189. debug=config.DEBUG,
  190. template_path=os.path.join(os.path.dirname(__file__), "templates"),
  191. password=config.UPLOAD_PASSWORD,
  192. )
  193. http_server = tornado.httpserver.HTTPServer(
  194. application,
  195. xheaders=config.XHEADERS,
  196. )
  197. http_server.listen(options.port, address=options.address)
  198. asyncio.get_event_loop().run_forever()
  199. if __name__ == "__main__":
  200. try:
  201. main()
  202. except KeyboardInterrupt:
  203. pass