commit 3b341972b0a4c5b8447ffddf65c796ffc54ca8e6 Author: Daniel Gultsch Date: Sun Jun 28 11:21:30 2015 +0200 inital commit diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..d7acc35 --- /dev/null +++ b/config.yml @@ -0,0 +1,11 @@ +jid: upload.siacs.eu +secret: something +storage_path : ./ +whitelist: + - jabber.ccc.de + - siacs.eu + - gultsch.de +max_file_size: 20971520 #20MiB +http_port: 8080 +get_url : http://upload.siacs.eu:8080 +put_url : http://upload.siacs.eu:8080 diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/upload.py b/plugins/upload.py new file mode 100644 index 0000000..d9defcf --- /dev/null +++ b/plugins/upload.py @@ -0,0 +1,38 @@ +from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin +from sleekxmpp import Iq +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath + +class upload(base_plugin): + + def plugin_init(self): + self.description = "upload files via http" + self.xep = "0999" + self.xmpp['xep_0030'].add_feature("urn:xmpp:http:upload") + self.xmpp.register_handler( + Callback('Upload request', + MatchXPath('{%s}iq/{urn:xmpp:http:upload}request' % self.xmpp.default_ns), + self._handleUpload)) + register_stanza_plugin(Iq, UploadRequest) + register_stanza_plugin(Iq, UploadSlot) + + + def _handleUpload(self, iq): + if iq['type'] == 'get': + self.xmpp.event('request_upload_slot',iq) + + +class UploadRequest(ElementBase): + namespace = "urn:xmpp:http:upload" + name = "request" + plugin_attrib = "request" + interfaces = set(('size','filename')) + sub_interfaces = interfaces + +class UploadSlot(ElementBase): + namespace = "urn:xmpp:http:upload" + name = "slot" + plugin_attrib = "slot" + interfaces = set(('put','get')) + sub_interfaces = interfaces diff --git a/server.py b/server.py new file mode 100644 index 0000000..b119dbd --- /dev/null +++ b/server.py @@ -0,0 +1,165 @@ +import yaml +import sys +import shutil +import signal +import logging +import string +import hashlib +import random +import os +from threading import Thread +from threading import Lock +from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn +import sleekxmpp +from sleekxmpp.componentxmpp import ComponentXMPP + +global files +global files_lock +global config + +class MissingComponent(ComponentXMPP): + def __init__(self, jid, secret): + ComponentXMPP.__init__(self, jid, secret, "localhost", 5347) + self.register_plugin('xep_0030') + self.register_plugin('upload',module='plugins.upload') + self.add_event_handler('request_upload_slot',self.request_upload_slot) + + def request_upload_slot(self, iq): + global config + global files + global files_lock + request = iq['request'] + maxfilesize = int(config['max_file_size']) + if not request['filename'] or not request['size']: + self._sendError(iq,'modify','bad-request','please specify filename and size') + elif maxfilesize < int(request['size']): + self._sendError(iq,'modify','not-acceptable','file too large. max file size is '+str(maxfilesize)) + elif 'whitelist' not in config or iq['from'].domain in config['whitelist']: + sender = iq['from'].bare + sender_hash = hashlib.sha1(sender.encode()).hexdigest() + filename = request['filename'] + folder = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(len(sender_hash))) + sane_filename = "".join([c for c in filename if c.isalpha() or c.isdigit() or c=="."]).rstrip() + path = sender_hash+'/'+folder + if sane_filename: + path += '/'+sane_filename + with files_lock: + files.add(path) + print(path) + reply = iq.reply() + reply['slot']['get'] = config['get_url'] + '/' + path + reply['slot']['put'] = config['put_url'] + '/' + path + reply.send() + else: + self. _sendError(iq,'cancel','not-allowed','not allowed to request upload slots') + + def _sendError(self, iq, error_type, condition, text): + reply = iq.reply() + iq.error() + iq['error']['type'] = error_type + iq['error']['condition'] = condition + iq['error']['text'] = text + iq.send() + +class HttpHandler(BaseHTTPRequestHandler): + def do_PUT(self): + print('do put') + global files + global files_lock + global config + path = self.path[1:] + length = int(self.headers['Content-Length']) + maxfilesize = int(config['max_file_size']) + if maxfilesize < length: + self.send_response(400,'file too large') + self.end_headers() + else: + print('path: '+path) + files_lock.acquire() + if path in files: + files.remove(path) + files_lock.release() + filename = config['storage_path'] + path + os.makedirs(os.path.dirname(filename)) + remaining = length + f = open(filename,'wb') + data = self.rfile.read(4096) + while data and remaining >= 0: + remaining -= len(data) + f.write(data) + data = self.rfile.read(min(4096,remaining)) + f.close() + self.send_response(200,'ok') + self.end_headers() + else: + files_lock.release() + self.send_response(403,'invalid slot') + self.end_headers() + + def do_GET(self): + global config + path = self.path[1:].replace('../','').replace('./','') + slashcount = path.count('/') + if slashcount < 1 or slashcount > 2: + self.send_response(404,'file not found') + self.end_headers() + else: + filename = config['storage_path']+'/'+path + print('requesting file: '+filename) + try: + with open(filename,'rb') as f: + self.send_response(200) + self.send_header("Content-Type", 'application/octet-stream') + self.send_header("Content-Disposition", 'attachment; filename="{}"'.format(os.path.basename(filename))) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs.st_size)) + self.end_headers() + shutil.copyfileobj(f, self.wfile) + except FileNotFoundError: + self.send_response(404,'file not found') + self.end_headers() + + def do_HEAD(self): + global config + path = self.path[1:].replace('../','').replace('./','') + slashcount = path.count('/') + if slashcount < 1 or slashcount > 2: + self.send_response(404,'file not found') + self.end_headers() + else: + try: + filename = config['storage_path']+'/'+path + self.send_response(200,'OK') + self.send_header("Content-Type", 'application/octet-stream') + self.send_header("Content-Disposition", 'attachment; filename="{}"'.format(os.path.basename(filename))) + self.send_header("Content-Length",str(os.path.getsize(filename))) + self.end_headers() + except FileNotFoundError: + self.send_response(404,'file not found') + self.end_headers() + + +class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + """Handle requests in a separate thread.""" + +if __name__ == "__main__": + global files + global files_lock + global config + + with open('config.yml','r') as ymlfile: + config = yaml.load(ymlfile) + + files = set() + files_lock = Lock() + logging.basicConfig(level=logging.DEBUG, + format='%(levelname)-8s %(message)s') + server = ThreadedHTTPServer(('0.0.0.0', config['http_port']), HttpHandler) + xmpp = MissingComponent(config['jid'],config['secret']) + if xmpp.connect(): + xmpp.process(block=False) + print("connected") + server.serve_forever() + else: + print("unable to connect")