Fork of elimage with specific modifications for my instance. https://i.catgirlsin.space
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
7.6KB

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