Compare commits

...

4 Commits

Author SHA1 Message Date
Sebastian Lohff 33defb85a7 Further codeformatting
3 years ago
Sebastian Lohff 579363201c Code reformatting
3 years ago
Sebastian Lohff cd28811fcf Release v0.5.2
3 years ago
Sebastian Lohff 46d4433a1d Explicitly set encoding for http requests in tests
3 years ago

@ -2,8 +2,10 @@ servefile changelog
=================== ===================
Unreleased 2021-09-08 v0.5.2
---------- -----------------
0.5.2 released
* fixed bug where exception was shown on transmission abort with python3 * fixed bug where exception was shown on transmission abort with python3
* fixed wrong/outdated pyopenssl package names * fixed wrong/outdated pyopenssl package names

@ -1,4 +1,4 @@
.TH SERVEFILE 1 "September 2020" "servefile 0.5.1" "User Commands" .TH SERVEFILE 1 "September 2021" "servefile 0.5.2" "User Commands"
.SH NAME .SH NAME
servefile \- small HTTP-Server for temporary file transfer servefile \- small HTTP-Server for temporary file transfer

@ -7,7 +7,7 @@
from __future__ import print_function from __future__ import print_function
__version__ = '0.5.1' __version__ = '0.5.2'
import argparse import argparse
import base64 import base64
@ -42,11 +42,13 @@ try:
except ImportError: except ImportError:
pass pass
def getDateStrNow(): def getDateStrNow():
""" Get the current time formatted for HTTP header """ """ Get the current time formatted for HTTP header """
now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())) now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
return now.strftime("%a, %d %b %Y %H:%M:%S GMT") return now.strftime("%a, %d %b %Y %H:%M:%S GMT")
class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
fileName = None fileName = None
blockSize = 1024 * 1024 blockSize = 1024 * 1024
@ -141,7 +143,7 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# now we can wind the file *brrrrrr* # now we can wind the file *brrrrrr*
myfile.seek(fromto[0]) myfile.seek(fromto[0])
if fromto != None: if fromto is not None:
self.send_response(216) self.send_response(216)
self.send_header('Content-Range', 'bytes %d-%d/%d' % (fromto[0], fromto[1], fileLength)) self.send_header('Content-Range', 'bytes %d-%d/%d' % (fromto[0], fromto[1], fileLength))
fileLength = fromto[1] - fromto[0] + 1 fileLength = fromto[1] - fromto[0] + 1
@ -246,7 +248,7 @@ class TarFileHandler(FileBaseHandler):
# give the process a short time to find out if it can # give the process a short time to find out if it can
# pack/compress the file # pack/compress the file
time.sleep(0.05) time.sleep(0.05)
if tarCmd.poll() != None and tarCmd.poll() != 0: if tarCmd.poll() is not None and tarCmd.poll() != 0:
# something went wrong # something went wrong
print("Error while compressing '%s'. Aborting request." % self.target) print("Error while compressing '%s'. Aborting request." % self.target)
self.send_response(500) self.send_response(500)
@ -416,7 +418,7 @@ class DirListingHandler(FileBaseHandler):
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
""" % {'path': os.path.normpath(unquote(self.path))} """ % {'path': os.path.normpath(unquote(self.path))} # noqa: E501
footer = """</tbody></table></div> footer = """</tbody></table></div>
<div class="footer"><a href="http://seba-geek.de/stuff/servefile/">servefile %(version)s</a></div> <div class="footer"><a href="http://seba-geek.de/stuff/servefile/">servefile %(version)s</a></div>
<script> <script>
@ -610,7 +612,7 @@ class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
env = os.environ env = os.environ
env['REQUEST_METHOD'] = "POST" env['REQUEST_METHOD'] = "POST"
fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env) fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
if not "file" in fstorage: if "file" not in fstorage:
self.sendResponse(400, "No file found in request.") self.sendResponse(400, "No file found in request.")
return return
@ -677,7 +679,8 @@ class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
self.sendResponse(411, "Content-Length was invalid or not set.") self.sendResponse(411, "Content-Length was invalid or not set.")
return -1 return -1
if self.maxUploadSize > 0 and length > self.maxUploadSize: if self.maxUploadSize > 0 and length > self.maxUploadSize:
self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" % self.maxUploadSize) self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" %
self.maxUploadSize)
return -1 return -1
return length return length
@ -714,6 +717,7 @@ class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
return extraDestFileName return extraDestFileName
# never reached # never reached
class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
def handle_error(self, request, client_address): def handle_error(self, request, client_address):
_, exc_value, _ = sys.exc_info() _, exc_value, _ = sys.exc_info()
@ -789,6 +793,7 @@ class SecureHandler():
self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
class ServeFileException(Exception): class ServeFileException(Exception):
pass pass
@ -813,7 +818,8 @@ class ServeFile():
if self.serveMode not in range(self._NUM_MODES): if self.serveMode not in range(self._NUM_MODES):
self.serveMode = None self.serveMode = None
raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, MODE_SINGLETAR, MODE_UPLOAD or MODE_DIRLIST.") raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, "
"MODE_SINGLETAR, MODE_UPLOAD or MODE_DIRLIST.")
def setIPv4(self, ipv4): def setIPv4(self, ipv4):
""" En- or disable ipv4 """ """ En- or disable ipv4 """
@ -827,18 +833,18 @@ class ServeFile():
""" Get IPs from all interfaces via ip or ifconfig. """ """ Get IPs from all interfaces via ip or ifconfig. """
# ip and ifconfig sometimes are located in /sbin/ # ip and ifconfig sometimes are located in /sbin/
os.environ['PATH'] += ':/sbin:/usr/sbin' os.environ['PATH'] += ':/sbin:/usr/sbin'
proc = Popen(r"ip addr|" + \ proc = Popen(r"ip addr|"
"sed -n -e 's/.*inet6\{0,1\} \([0-9.a-fA-F:]\+\).*/\\1/ p'|" + \ r"sed -n -e 's/.*inet6\{0,1\} \([0-9.a-fA-F:]\+\).*/\1/ p'|"
"grep -v '^fe80\|^127.0.0.1\|^::1'", \ r"grep -v '^fe80\|^127.0.0.1\|^::1'",
shell=True, stdout=PIPE, stderr=PIPE) shell=True, stdout=PIPE, stderr=PIPE)
if proc.wait() != 0: if proc.wait() != 0:
# ip failed somehow, falling back to ifconfig # ip failed somehow, falling back to ifconfig
oldLang = os.environ.get("LC_ALL", None) oldLang = os.environ.get("LC_ALL", None)
os.environ['LC_ALL'] = "C" os.environ['LC_ALL'] = "C"
proc = Popen(r"ifconfig|" + \ proc = Popen(r"ifconfig|"
"sed -n 's/.*inet6\{0,1\}\( addr:\)\{0,1\} \{0,1\}\([0-9a-fA-F.:]*\).*/" + \ r"sed -n 's/.*inet6\{0,1\}\( addr:\)\{0,1\} \{0,1\}\([0-9a-fA-F.:]*\).*/"
"\\2/p'|" + \ r"\2/p'|"
"grep -v '^fe80\|^127.0.0.1\|^::1'", \ r"grep -v '^fe80\|^127.0.0.1\|^::1'",
shell=True, stdout=PIPE, stderr=PIPE) shell=True, stdout=PIPE, stderr=PIPE)
if oldLang: if oldLang:
os.environ['LC_ALL'] = oldLang os.environ['LC_ALL'] = oldLang
@ -886,7 +892,7 @@ class ServeFile():
req = crypto.X509Req() req = crypto.X509Req()
subj = req.get_subject() subj = req.get_subject()
subj.CN = "127.0.0.1" subj.CN = "127.0.0.1"
subj.O = "servefile laboratories" subj.O = "servefile laboratories" # noqa: E741
subj.OU = "servefile" subj.OU = "servefile"
# generate altnames # generate altnames
@ -951,7 +957,8 @@ class ServeFile():
server = SecureThreadedHTTPServer(self._getCert(), self._getKey(), server = SecureThreadedHTTPServer(self._getCert(), self._getKey(),
(listenIp, self.port), handler, bind_and_activate=False) (listenIp, self.port), handler, bind_and_activate=False)
except SSL.Error as e: except SSL.Error as e:
raise ServeFileException("SSL error: Could not read SSL public/private key from file(s) (error was: \"%s\")" % (e[0][0][2],)) raise ServeFileException("SSL error: Could not read SSL public/private key "
"from file(s) (error was: \"%s\")" % (e[0][0][2],))
else: else:
server = ThreadedHTTPServer((listenIp, self.port), handler, server = ThreadedHTTPServer((listenIp, self.port), handler,
bind_and_activate=False) bind_and_activate=False)
@ -984,7 +991,7 @@ class ServeFile():
print("Serving \"%s\" for uploads at port %d." % (self.target, self.port)) print("Serving \"%s\" for uploads at port %d." % (self.target, self.port))
# print urls with local network adresses # print urls with local network adresses
print("\nSome addresses %s will be available at:" % \ print("\nSome addresses %s will be available at:" %
("this file" if (self.serveMode != self.MODE_UPLOAD) else "the uploadform", )) ("this file" if (self.serveMode != self.MODE_UPLOAD) else "the uploadform", ))
ips = self.getIPs() ips = self.getIPs()
if not ips or len(ips) == 0 or ips[0] == '': if not ips or len(ips) == 0 or ips[0] == '':
@ -1041,7 +1048,8 @@ class ServeFile():
try: try:
os.mkdir(self.target) os.mkdir(self.target)
except (IOError, OSError) as e: except (IOError, OSError) as e:
raise ServeFileException("Error: Could not create directory '%s' for uploads, %r" % (self.target, str(e))) raise ServeFileException("Error: Could not create directory '%s' for uploads, %r" %
(self.target, str(e)))
else: else:
raise ServeFileException("Error: Upload directory already exists and is a file.") raise ServeFileException("Error: Upload directory already exists and is a file.")
FilePutter.targetDir = os.path.abspath(self.target) FilePutter.targetDir = os.path.abspath(self.target)
@ -1060,6 +1068,7 @@ class ServeFile():
AuthenticationHandler.authString = self.auth AuthenticationHandler.authString = self.auth
if self.authrealm: if self.authrealm:
AuthenticationHandler.realm = self.authrealm AuthenticationHandler.realm = self.authrealm
class AuthenticatedHandler(AuthenticationHandler, handler): class AuthenticatedHandler(AuthenticationHandler, handler):
pass pass
handler = AuthenticatedHandler handler = AuthenticatedHandler
@ -1105,7 +1114,8 @@ class AuthenticationHandler():
self.send_response(401) self.send_response(401)
self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm) self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm)
self.send_header("Connection", "close") self.send_header("Connection", "close")
errorMsg = "<html><head><title>401 - Unauthorized</title></head><body><h1>401 - Unauthorized</h1></body></html>" errorMsg = ("<html><head><title>401 - Unauthorized</title></head>"
"<body><h1>401 - Unauthorized</h1></body></html>")
self.send_header("Content-Length", str(len(errorMsg))) self.send_header("Content-Length", str(len(errorMsg)))
self.end_headers() self.end_headers()
self.wfile.write(errorMsg.encode()) self.wfile.write(errorMsg.encode())
@ -1115,32 +1125,35 @@ def main():
parser = argparse.ArgumentParser(prog='servefile', description='Serve a single file via HTTP.') parser = argparse.ArgumentParser(prog='servefile', description='Serve a single file via HTTP.')
parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
parser.add_argument('target', metavar='file/directory', type=str) parser.add_argument('target', metavar='file/directory', type=str)
parser.add_argument('-p', '--port', type=int, default=8080, \ parser.add_argument('-p', '--port', type=int, default=8080,
help='Port to listen on') help='Port to listen on')
parser.add_argument('-u', '--upload', action="store_true", default=False, \ parser.add_argument('-u', '--upload', action="store_true", default=False,
help="Enable uploads to a given directory") help="Enable uploads to a given directory")
parser.add_argument('-s', '--max-upload-size', type=str, \ parser.add_argument('-s', '--max-upload-size', type=str,
help="Limit upload size in kB. Size modifiers are allowed, e.g. 2G, 12MB, 1B") help="Limit upload size in kB. Size modifiers are allowed, e.g. 2G, 12MB, 1B")
parser.add_argument('-l', '--list-dir', action="store_true", default=False, \ parser.add_argument('-l', '--list-dir', action="store_true", default=False,
help="Show directory indexes and allow access to all subdirectories") help="Show directory indexes and allow access to all subdirectories")
parser.add_argument('--ssl', action="store_true", default=False, \ parser.add_argument('--ssl', action="store_true", default=False,
help="Enable SSL. If no key/cert is specified one will be generated") help="Enable SSL. If no key/cert is specified one will be generated")
parser.add_argument('--key', type=str, \ parser.add_argument('--key', type=str,
help="Keyfile to use for SSL. If no cert is given with --cert the keyfile will also be searched for a cert") help="Keyfile to use for SSL. If no cert is given with --cert the keyfile "
parser.add_argument('--cert', type=str, \ "will also be searched for a cert")
parser.add_argument('--cert', type=str,
help="Certfile to use for SSL") help="Certfile to use for SSL")
parser.add_argument('-a', '--auth', type=str, metavar='user:password', \ parser.add_argument('-a', '--auth', type=str, metavar='user:password',
help="Set user and password for HTTP basic authentication") help="Set user and password for HTTP basic authentication")
parser.add_argument('--realm', type=str, default=None,\ parser.add_argument('--realm', type=str, default=None,
help="Set a realm for HTTP basic authentication") help="Set a realm for HTTP basic authentication")
parser.add_argument('-t', '--tar', action="store_true", default=False, \ parser.add_argument('-t', '--tar', action="store_true", default=False,
help="Enable on the fly tar creation for given file or directory. Note: Download continuation will not be available") help="Enable on the fly tar creation for given file or directory. "
parser.add_argument('-c', '--compression', type=str, metavar='method', \ "Note: Download continuation will not be available")
default="none", \ parser.add_argument('-c', '--compression', type=str, metavar='method',
help="Set compression method, only in combination with --tar. Can be one of %s" % ", ".join(TarFileHandler.compressionMethods)) default="none",
parser.add_argument('-4', '--ipv4-only', action="store_true", default=False, \ help="Set compression method, only in combination with --tar. "
"Can be one of %s" % ", ".join(TarFileHandler.compressionMethods))
parser.add_argument('-4', '--ipv4-only', action="store_true", default=False,
help="Listen on IPv4 only") help="Listen on IPv4 only")
parser.add_argument('-6', '--ipv6-only', action="store_true", default=False, \ parser.add_argument('-6', '--ipv6-only', action="store_true", default=False,
help="Listen on IPv6 only") help="Listen on IPv6 only")
args = parser.parse_args() args = parser.parse_args()
@ -1156,7 +1169,7 @@ def main():
sys.exit(1) sys.exit(1)
if args.max_upload_size: if args.max_upload_size:
sizeRe = re.match("^(\d+(?:[,.]\d+)?)(?:([bkmgtpe])(?:(?<!b)b?)?)?$", args.max_upload_size.lower()) sizeRe = re.match(r"^(\d+(?:[,.]\d+)?)(?:([bkmgtpe])(?:(?<!b)b?)?)?$", args.max_upload_size.lower())
if not sizeRe: if not sizeRe:
print("Error: Your max upload size param is broken. Try something like 3M or 2.5Gb.") print("Error: Your max upload size param is broken. Try something like 3M or 2.5Gb.")
sys.exit(1) sys.exit(1)
@ -1183,7 +1196,8 @@ def main():
if args.auth: if args.auth:
dpos = args.auth.find(":") dpos = args.auth.find(":")
if dpos <= 0 or dpos == (len(args.auth)-1): if dpos <= 0 or dpos == (len(args.auth)-1):
print("Error: User and password for HTTP basic authentication need to be both at least one character and have to be separated by a \":\".") print("Error: User and password for HTTP basic authentication need to be both "
"at least one character and have to be separated by a \":\".")
sys.exit(1) sys.exit(1)
if args.realm and not args.auth: if args.realm and not args.auth:
@ -1254,4 +1268,3 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()

@ -11,7 +11,7 @@ setup(
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
platforms='posix', platforms='posix',
version='0.5.1', version='0.5.2',
license='GPLv3 or later', license='GPLv3 or later',
url='https://github.com/sebageek/servefile/', url='https://github.com/sebageek/servefile/',
author='Sebastian Lohff', author='Sebastian Lohff',

@ -92,11 +92,14 @@ def datadir(tmp_path):
def make_request(path='/', host='localhost', port=SERVEFILE_DEFAULT_PORT, method='get', protocol='http', timeout=5, def make_request(path='/', host='localhost', port=SERVEFILE_DEFAULT_PORT, method='get', protocol='http', timeout=5,
**kwargs): encoding='utf-8', **kwargs):
url = '{}://{}:{}{}'.format(protocol, host, port, path) url = '{}://{}:{}{}'.format(protocol, host, port, path)
print('Calling {} on {} with {}'.format(method, url, kwargs)) print('Calling {} on {} with {}'.format(method, url, kwargs))
r = getattr(requests, method)(url, **kwargs) r = getattr(requests, method)(url, **kwargs)
if r.encoding is None and encoding:
r.encoding = encoding
return r return r
@ -127,7 +130,7 @@ def _test_version(run_servefile, standalone):
version = s.stdout.readline().decode().strip() version = s.stdout.readline().decode().strip()
# hardcode version as string until servefile is a module # hardcode version as string until servefile is a module
assert version == 'servefile 0.5.1' assert version == 'servefile 0.5.2'
def test_version(run_servefile): def test_version(run_servefile):

Loading…
Cancel
Save