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.

265 lines
7.6 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
7 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
6 years ago
6 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  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