Compare commits

...

4 Commits

Author SHA1 Message Date
Sebastian Lohff 33defb85a7 Further codeformatting
* break up some extra long lines
 * add a bit of noqa for warnings I don't want to have
 * rework formatting for ip addr / ifconfig part
2021-09-08 00:43:22 +02:00
Sebastian Lohff 579363201c Code reformatting
* replace tabs with spaces
 * replace some of the != None with is not None etc.
 * more whitespace fixes
 * remove all the newline \
2021-09-08 00:43:22 +02:00
Sebastian Lohff cd28811fcf Release v0.5.2 2021-09-08 00:23:11 +02:00
Sebastian Lohff 46d4433a1d Explicitly set encoding for http requests in tests
Due to the upgrade to charset-normalizer 2.0.4 guessing the encoding
inside the tests did not work anymore and caused the umlaut tests to
fail. Explicitly specifying the encoding on the requests' response
object fixes this.
2021-09-07 23:24:57 +02:00
5 changed files with 980 additions and 962 deletions

View File

@ -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

View File

@ -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

View File

@ -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>
@ -502,7 +504,7 @@ class DirListingHandler(FileBaseHandler):
dir_items = list() dir_items = list()
file_items = list() file_items = list()
for item in [".."] + sorted(os.listdir(path), key=lambda x:x.lower()): for item in [".."] + sorted(os.listdir(path), key=lambda x: x.lower()):
# create path to item # create path to item
itemPath = os.path.join(path, item) itemPath = os.path.join(path, item)
@ -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()

View File

@ -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',

View File

@ -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):