diff --git a/.gitignore b/.gitignore index 0d20b64..57bcfa3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.pyc +www +*~ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..98675a3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2012, Bryce Boe +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..0edb80b --- /dev/null +++ b/cert.pem @@ -0,0 +1,49 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDyY/jYTBJcS83v +1HiZwOaiFg9NOW4XYx5Th8k8iaVfyc2s1HkdvvNZme6RgVzPPIjpp+P/ZpYc6mWl +6mbFA01qECBES4uIQ/+H49odB6OhQn4RTqhEp/7ovgLcmPhdcD15lSFwv8qcD1SF +BeQksblT4yJ4ZmwKHEVMrrS8pHDI39rXpBlcqGMEzaz+O4WEXfjgqQFdza4uaBB4 +fTqUQOgOCCudML6NvcPJsV39IbRrqWFMy4vS8oe95z1QV1DyPLuGCMA8mOxQV16/ +Zr1f1+pbsihh1WgbSf+ugjtBFJpvOUpFPgiiMe8IT3Ad1H1WKeJuwZd4Xdo7mwSy +C3Vo8SYbAgMBAAECggEBAIokzgNAP8qYbXM9TcqbNRa1qdziWUs882C558f3zuer +1OumYKzCUmEDqQMGjkWSA5nfVMMPzYgHUw1fbVjlS+6h6DHkOE7eKjlhc8LNwsU6 +Zy0n/iC6+j3dZTYifJKMUI0FoHxo4mlVF1+E/wSmwa/8qH2kKfkNKbXNcDZ/JcRl +LIMpUG+RiOjUdeYcoMoIwZ0NxOEyD0GCijIRpp5oClBDcMfMgHFzp6VVkGAVPawL +BZTgnMfT5Gq7TuxvL41wpeA+nsOIVnBG+/vA2uvvRAVAGOF9P7Tw7gOO1afzfndy +evGEh22NRD/sr0Sw6oHodgdZYusILsyjehFhq2KvsAECgYEA/7psX3roLhxFdMry +2LBNL0nRMt9I9wxyd+b2rji/df6P49uW5azyva4B06MQfvmjwQyTrnLISAFAXUqB +6dov53K7jr/ZCMQJd08QoAezBG+MYq8pwcinShWotGEpv3QjSTa1avSNs9nbCjvV +h+23qu7A+tzwWIxd2cOWDc0lzhsCgYEA8qXrfjEpCKZzfQQzajz7OHyTg8s2xs25 +8Q/Ujfz/42F2+I0LF65CMuQ/+0PwSMkAA+S+p+fFjL/6Vl7u/QhMaO3a89QdkzPT +DY/fTfQft4ZdyAt3/1OD74ec+mDIjHvGbz11hXaBBiiMDKm7I09Mpqho/qt8WnC4 +/gTTHqO3iAECgYAhk7u5Ca1EqKeh/yfFtdhcliems1SkQ3kgLy5Cj+brukHwXBvD +CmNQ7nEE9csrsOGEbn9Rj3ampatq1Gacau6RSCpCQwRfN0Y66SG4sWoa8f6P0on8 +DLmSa6ecD4novnoHmexWH8gfTkKJZHPUBi1wfyrNhYb1SXDlL94a782/8wKBgAmn +8Kck7a0acp4W/LTNmHG3A2fLPnLK3Qtqxdqgps41orZhZn+av6emzaUCHx3GYenF +wJCN+J6RRTUKshf/rIrAbGYnmsWw2kU/5HMFs/1pq3G6gxv2BtoRW33bPB/bDRqA +J50YCipkkq4uUvQkw7geG4+G43v14Ga7amtduIABAoGBALrcz2bfufySjrxz1tIv ++9K6KFaFCuJAOrHUdVu/p/iBgIysfhR3InFY5v2jIo5nO6+zjRV4jkka3idDGKTl +sQt5sgoIqGdmRXimpqtvD8E7l7LwAqrvRHMEjKETFaJRnIFXPJgxve5VRLNHsXbC +2j5N19dNtLzLLCY3eI5cIFaV +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAN25hw0LmTvVMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwODA0MDcwNTMzWhcNMTcwODA0MDcwNTMzWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA8mP42EwSXEvN79R4mcDmohYPTTluF2MeU4fJPImlX8nNrNR5Hb7zWZnu +kYFczzyI6afj/2aWHOplpepmxQNNahAgREuLiEP/h+PaHQejoUJ+EU6oRKf+6L4C +3Jj4XXA9eZUhcL/KnA9UhQXkJLG5U+MieGZsChxFTK60vKRwyN/a16QZXKhjBM2s +/juFhF344KkBXc2uLmgQeH06lEDoDggrnTC+jb3DybFd/SG0a6lhTMuL0vKHvec9 +UFdQ8jy7hgjAPJjsUFdev2a9X9fqW7IoYdVoG0n/roI7QRSabzlKRT4IojHvCE9w +HdR9VinibsGXeF3aO5sEsgt1aPEmGwIDAQABo1AwTjAdBgNVHQ4EFgQUpoSxpWj2 +4k3w4jezGR7IeF+FV64wHwYDVR0jBBgwFoAUpoSxpWj24k3w4jezGR7IeF+FV64w +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVJ/dxQOynCH3fxdNxYWd +65VAFl+BwoB5BWqevk+48FAjnjTfssS9fDuHT46YWQ1UfWdD/GF6nlKRlQ3qzOt7 +eo15cfS9acFjUnBSSI9eg6MG7b4KKMWZ9qnLosY0PMJC/6KInpGU2RcqR8UlRfTS +B2794p+bewP37Tu1P1MrXvTgmtoVXWUB8APBfcz1GC+Yy3jaC/xVwe7Jy9AeuTF3 +Xon6GqI0jurw8ogsLjDcTaTrLc1afyAizOe1sb8DG9H3kRbH29bkikkerzrPd3ed +qmir6bWjq3Q+Xehh0K2jK8F7RN2DASDw2he3mMMp8lyDxSUWqieey3QGeKlJsNpE +gA== +-----END CERTIFICATE----- diff --git a/httpserver.py b/httpserver.py index 660d559..2dbb2a9 100755 --- a/httpserver.py +++ b/httpserver.py @@ -228,10 +228,10 @@ class ThreadingServer(ThreadingMixIn, HTTPServer): pass -def run(port=8080, doc_root=os.getcwd()): +def run(port=8888, doc_root=os.getcwd()+"/www"): serveraddr = ('', port) serv = ThreadingServer(serveraddr, get_handler(doc_root)) - print 'Server Started at port:', port + print 'Server Started at port:', port,' doc_root ', doc_root serv.serve_forever() def test(): diff --git a/README b/httpserver.txt similarity index 100% rename from README rename to httpserver.txt diff --git a/httpserver2.py b/httpserver2.py new file mode 100755 index 0000000..c5749d0 --- /dev/null +++ b/httpserver2.py @@ -0,0 +1,141 @@ +#!/usr/bin/python +#encoding=utf-8 + +from ftplib import FTP +import sys +import os.path + +class MyFTP(FTP): + ''' + conncet to FTP Server + ''' + def __init__(self): + print 'make a object' + def ConnectFTP(self,remoteip,remoteport,loginname,loginpassword): + ftp=MyFTP() + + try: + ftp.connect(remoteip,remoteport,600) + print 'success' + except Exception, e: + print >> sys.stderr, "conncet failed1 - %s" % e + return (0,'conncet failed') + else: + try: + ftp.login(loginname,loginpassword) + print('login success') + except Exception, e: + print >>sys.stderr, 'login failed - %s' % e + return (0,'login failed') + else: + print 'return 1' + return (1,ftp) + + def download(self,remoteHost,remotePort,loginname,loginpassword,remotePath,localPath): + #connect to the FTP Server and check the return + res = self.ConnectFTP(remoteHost,remotePort,loginname,loginpassword) + if(res[0]!=1): + print >>sys.stderr, res[1] + sys.exit() + + #change the remote directory and get the remote file size + ftp=res[1] + ftp.set_pasv(0) + dires = self.splitpath(remotePath) + if dires[0]: + ftp.cwd(dires[0]) # change remote work dir + remotefile=dires[1] # remote file name + print dires[0]+' '+ dires[1] + fsize=ftp.size(remotefile) + if fsize==0 : # localfime's site is 0 + return + + #check local file isn't exists and get the local file size + lsize=0L + if os.path.exists(localPath): + lsize=os.stat(localPath).st_size + + if lsize >= fsize: + print 'local file is bigger or equal remote file' + return + blocksize=1024 * 1024 + cmpsize=lsize + ftp.voidcmd('TYPE I') + conn = ftp.transfercmd('RETR '+remotefile,lsize) + lwrite=open(localPath,'ab') + while True: + data=conn.recv(blocksize) + if not data: + break + lwrite.write(data) + cmpsize+=len(data) + print '\b'*30,'download process:%.2f%%'%(float(cmpsize)/fsize*100), + lwrite.close() + ftp.voidcmd('NOOP') + ftp.voidresp() + conn.close() + ftp.quit() + + def upload(self,remotehost,remoteport,loginname,loginpassword,remotepath,localpath,callback=None): + if not os.path.exists(localpath): + print "Local file doesn't exists" + return + self.set_debuglevel(2) + res=self.ConnectFTP(remotehost,remoteport,loginname,loginpassword) + if res[0]!=1: + print res[1] + sys.exit() + ftp=res[1] + remote=self.splitpath(remotepath) + ftp.cwd(remote[0]) + rsize=0L + try: + rsize=ftp.size(remote[1]) + except: + pass + if (rsize==None): + rsize=0L + lsize=os.stat(localpath).st_size + print('rsize : %d, lsize : %d' % (rsize, lsize)) + if (rsize==lsize): + print 'remote filesize is equal with local' + return + if (rsize>sys.stderr, '----------ftp.ntransfercmd-------- : %s' % e + return + cmpsize=rsize + while True: + buf=localf.read(1024 * 1024) + if not len(buf): + print '\rno data break' + break + datasock.sendall(buf) + if callback: + callback(buf) + cmpsize+=len(buf) + print '\b'*30,'uploading %.2f%%'%(float(cmpsize)/lsize*100), + if cmpsize==lsize: + print '\rfile size equal break' + break + datasock.close() + print 'close data handle' + localf.close() + print 'close local file handle' + ftp.voidcmd('NOOP') + print 'keep alive cmd success' + ftp.voidresp() + print 'No loop cmd' + ftp.quit() + + def splitpath(self,remotepath): + position=remotepath.rfind('/') + return (remotepath[:position+1],remotepath[position+1:]) diff --git a/httpserver2.txt b/httpserver2.txt new file mode 100755 index 0000000..f8bbd30 --- /dev/null +++ b/httpserver2.txt @@ -0,0 +1 @@ +a ftp server with resume, upload function diff --git a/httpserver3.py b/httpserver3.py new file mode 100755 index 0000000..a9de8f1 --- /dev/null +++ b/httpserver3.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +"""A small set of improvements upon the Simple and BaseHTTPServers.""" + +import SocketServer +import base64 +import os +import socket +import ssl +import sys +import threading +import time +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler +from SimpleHTTPServer import SimpleHTTPRequestHandler +from optparse import OptionParser +from warnings import warn + + +__version__ = '0.2' + + +# +# Helpers +# +class RateLimitWriter(object): + """A class that rate limits writing to associated file streams + + This method only supports threading and not forking (multiprocessing). + """ + INTERVAL_LEN = .125 + block_size = 16384 + lock = threading.Lock() + block_start = None + block_sent = 0 + + @classmethod + def bytes_to_write(cls, desired): + """Determine how many bytes to write and sleep when over the limit.""" + to_send = 0 + while not to_send: + cls.lock.acquire() + now = time.time() + if not cls.block_start: + # First data of block, send it all + cls.block_start = now + to_send = min(desired, cls.block_size) + cls.block_sent = to_send + elif cls.block_sent < cls.block_size: + # Haven't sent a complete block, send remainder + to_send = min(desired, cls.block_size - cls.block_sent) + cls.block_sent += to_send + else: + # A complete block has been sent, sleep if necessary + sleep_time = cls.INTERVAL_LEN - (now - cls.block_start) + if sleep_time > 0: + time.sleep(sleep_time) + cls.block_start = cls.block_sent = None + cls.block_sent = 0 + cls.lock.release() + return to_send + + @classmethod + def set_rate_limit(cls, limit): + """Set the rate limit in kilobytes per second.""" + cls.block_size = int(1024 * limit * cls.INTERVAL_LEN) + + def __init__(self, to_wrap): + """Store the output stream we are wrapping.""" + self.wrapped = to_wrap + + def __getattr__(self, attr): + """Redirect all function calls through the wrapped output stream.""" + return getattr(self.wrapped, attr) + + def write(self, message): + """Perform a throttled write to the wrapped output stream.""" + while message: + to_send = RateLimitWriter.bytes_to_write(len(message)) + self.wrapped.write(message[:to_send]) + message = message[to_send:] + + +# +# HTTPServer extensions +# +class SecureHTTPServer(HTTPServer, object): + """A HTTP Server object that support HTTPS""" + def __init__(self, address, handler, cert_file): + """Support TLS/SSL by wrapping the socket.""" + super(SecureHTTPServer, self).__init__(address, handler) + self.socket = ssl.wrap_socket(self.socket, certfile=cert_file) + + +# +# BaseHTTPRequestHandler extensions +# +class AuthHandler(BaseHTTPRequestHandler, object): + """A handler that supports basic HTTP authentication/authorization""" + message = 'Authentication required.' + realm = 'Something' + users = set() + + @classmethod + def add_user(cls, username, password): + """Add a set of credentials.""" + cls.users.add(base64.b64encode('{0}:{1}'.format(username, password))) + + def handle_auth(self, head=False): + """Output the authentication headers if the user is not valid.""" + auth = self.headers.getheader('Authorization') + if auth: + try: + _, encoded = auth.split(' ', 1) + except ValueError: + encoded = None + # Verify the user + if encoded in AuthHandler.users: + return True + # Send authentication header information + self.send_response(401) + self.send_header('WWW-Authenticate', + 'Basic realm="{0}"'.format(AuthHandler.realm)) + self.send_header('Content-Type', 'text/html') + self.send_header('Content-Length', len(AuthHandler.message)) + self.end_headers() + if not head: + self.wfile.write(AuthHandler.message) + return False + + def do_GET(self): + """Call the parent's do_GET function if the user is authorized.""" + if self.handle_auth(): + super(AuthHandler, self).do_GET() + + def do_HEAD(self): + """Call the parent's do_HEAD function if the user is authorized.""" + if self.handle_auth(head=True): + super(AuthHandler, self).do_HEAD() + + +class RangeHandler(SimpleHTTPRequestHandler, object): + """A handler that supports HTTP requests with the Range header + + The Range header allows for the resume download functionality. + """ + def copyfile(self, source, outputfile): + """Copy only the ranged part of the file when appropriate.""" + if self.is_ranged: + source.seek(self.range_begin) + super(RangeHandler, self).copyfile(source, outputfile) + + def do_GET(self): + """Set is_ranged flag if a valid Range header is sent.""" + self.handle_range() + super(RangeHandler, self).do_GET() + + def do_HEAD(self): + """Set is_ranged flag if a valid Range header is sent.""" + self.handle_range() + super(RangeHandler, self).do_HEAD() + + def handle_range(self): + """Parse the Range header if it exists.""" + self.is_ranged = False + if 'range' in self.headers: + try: + range_unit, other = self.headers['range'].split('=', 1) + if range_unit == 'bytes': + if ',' in other: # Handle only a single range + warn('Multiple ranges are not supported.') + return + begin, end = other.split('-', 1) + if end: + warn('Shortened ranges are not supported.') + return + self.range_begin = int(begin) if begin else 0 + self.range_end = None + self.is_ranged = True + except ValueError: + pass + + def send_header(self, key, value): + """Modify Content-Length and add Content-Range when ranged.""" + if key == 'Content-Length' and self.is_ranged: + length = int(value) + if self.range_end is None: + end = length - 1 + else: + end = min(self.range_end, length - 1) + value = str(1 + end - self.range_begin) + self.send_header('Content-Range', 'bytes {0}-{1}/{2}' + .format(self.range_begin, end, length)) + super(RangeHandler, self).send_header(key, value) + + def send_response(self, status, *args, **kwargs): + """Send 206 status for ranged responses.""" + if self.is_ranged and status == 200: + status = 206 + super(RangeHandler, self).send_response(status, *args, **kwargs) + + def setup(self): + """Set HTTP/1.1 as Range is supported only on HTTP/1.1.""" + super(RangeHandler, self).setup() + self.protocol_version = 'HTTP/1.1' + self.is_ranged = False + + +class RateLimitHandler(BaseHTTPRequestHandler, object): + """A hander that supports rate limiting from server to client. + + This handler will not properly rate limit if a ForkinMixIn is used in the + HTTPServer object. However, it works great in combination with the + ThreadingMixIn. + """ + def handle(self): + """Setup rate limiting on the outgoing connection.""" + self.wfile = RateLimitWriter(self.wfile) + super(RateLimitHandler, self).handle() + + +# +# Combined classes for use with the main functionality +# +class MyHandler(AuthHandler, RangeHandler, RateLimitHandler): + """A handler that supports auth, download resuming, and throttling.""" + + +class MyServer(SocketServer.ThreadingMixIn, SecureHTTPServer): + """A threaded SecureHTTPServer with basic error filtering""" + def handle_error(self, request, client_address): + """Disable tracebacks on connection close errors.""" + exc_type, exc_value, _ = sys.exc_info() + if exc_type is socket.error and exc_value[0] == 32: + print('{0} closed connection'.format(client_address)) + elif exc_type is ssl.SSLError and exc_value.errno == 1: + print('{0} SSL Error: bad write retry'.format(client_address)) + else: + super(MyServer, self).handle_error(request, client_address) + + +def main(): + """Run a secure threaded server with auth resume and rate limit support.""" + parser = OptionParser(version='%prog {0}'.format(__version__)) + parser.add_option('-p', '--port', type='int', default='8888') + parser.add_option('-c', '--cert', help='The TLS/SSL certificate file') + parser.add_option('-d', '--directory', help='The directory to serve') + parser.add_option('-r', '--ratelimit', help='The ratelimit in KBps', + type='int', default=128) + parser.add_option('-a', '--add-auth', help='Add user:password combination', + action='append') + options, _ = parser.parse_args() + + # Configure Services + if not options.add_auth: + parser.error('At least one user must be added via --add-auth') + for auth in options.add_auth: + try: + username, password = auth.split(':', 1) + except ValueError: + parser.error('{0!r} is not a valid username:password'.format(auth)) + AuthHandler.add_user(username, password) + RateLimitWriter.set_rate_limit(options.ratelimit) + + # Verify cert file + if not options.cert: + parser.error('--cert must be provided') + cert_path = os.path.abspath(options.cert) + if not os.path.isfile(cert_path): + parser.error('Invalid cert file') + + # Change into serving directory + if options.directory: + try: + os.chdir(options.directory) + except OSError: + parser.error('Invalid --directory') + + server = MyServer(('', options.port), MyHandler, cert_path) + print('Server listening on port %d' % options.port) + try: + server.serve_forever() + except KeyboardInterrupt: + print('\nGoodbye') + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/httpserver3.txt b/httpserver3.txt new file mode 100644 index 0000000..67f1d96 --- /dev/null +++ b/httpserver3.txt @@ -0,0 +1,22 @@ +### Installation + + pip install ext_http_server + +### Generate a certificate + +Run the following to generate cert.pem: + + openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout cert.pem + +### Running ext_http_server + +If you have files you want to serve in `/tmp/path/to/files/` run the following +to serve them up with a max outgoing throughput of 16KBps: + + ext_http_server --cert cert.pem -d /tmp/path/to/files -r16 -a foo:bar + +By default, you will be able to access the webserver at +[https://localhost:8000](https://localhost:8000). To authenticate, use the +username `foo` and the password `bar` as indicated by the `-a foo:bar` +argument. Note that multiple `-a` arguments can be added to add more than one +user.