Compare commits
23 Commits
fix-pyopen
...
master
Author | SHA1 | Date |
---|---|---|
Sebastian Lohff | f668fc3fe6 | |
Sebastian Lohff | 9784c82679 | |
Sebastian Lohff | f23dfd2a51 | |
Sebastian Lohff | b1145af6bb | |
MasterofJOKers | 0b010d5c10 | |
Sebastian Lohff | 4f3b916b9f | |
Sebastian Lohff | 5dcf364e0f | |
Sebastian Lohff | aa54e8536a | |
Sebastian Lohff | 96e9e76ff4 | |
Sebastian Lohff | c7af20388d | |
Sebastian Pipping | 413ea76746 | |
Sebastian Pipping | 8b16b7626c | |
Sebastian Pipping | 8f9ba0e387 | |
Sebastian Lohff | cd28811fcf | |
Sebastian Lohff | 46d4433a1d | |
Sebastian Lohff | d87a42cf8e | |
Paweł Chojnacki | 6537c054e5 | |
Sebastian Lohff | 65fcac5c49 | |
Sebastian Lohff | 0334e74996 | |
Sebastian Lohff | 8217034753 | |
Sebastian Lohff | 9fa4ed0026 | |
Sebastian Lohff | 1f451e0f29 | |
Sebastian Lohff | e31c8fb016 |
|
@ -0,0 +1,37 @@
|
||||||
|
name: Run Tox
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python: [2.7, 3.7, 3.8, 3.9, "3.10", 3.11]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python }}
|
||||||
|
- name: Install Tox
|
||||||
|
run: pip install tox
|
||||||
|
- name: Run Tox
|
||||||
|
run: tox -e py
|
||||||
|
pep8:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Install Tox
|
||||||
|
run: pip install tox
|
||||||
|
- name: Run Tox pep8
|
||||||
|
run: "tox -e pep8"
|
34
ChangeLog
34
ChangeLog
|
@ -1,11 +1,41 @@
|
||||||
servefile changelog
|
servefile changelog
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
2023-01-23 v0.5.4
|
||||||
|
-----------------
|
||||||
|
|
||||||
Unreleased
|
0.5.4 released
|
||||||
----------
|
|
||||||
|
* code reformatting for better maintainability
|
||||||
|
* upload to uploaddir instead of /tmp for large files
|
||||||
|
* add python3.10 / python3.11 support
|
||||||
|
* drop python3.6 support
|
||||||
|
|
||||||
|
|
||||||
|
2021-11-18 v0.5.3
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
0.5.3 released
|
||||||
|
|
||||||
|
* improved test performance
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
* tests are now using a free non-default port to avoid clashes; if
|
||||||
|
wished the ports can be set from outside by specifying the
|
||||||
|
environment variables SERVEFILE_DEFAULT_PORT and
|
||||||
|
SERVEFILE_SECONDARY_PORT
|
||||||
|
* fixed broken redirect when filename contained umlauts or other characters
|
||||||
|
that should have been quoted
|
||||||
|
* fixed broken special char handling in directory listing for python2
|
||||||
|
* drop python3.5 support
|
||||||
|
* fixed PUT uploads with python3 and documented PUT-uploads with curl
|
||||||
|
|
||||||
|
|
||||||
2020-10-30 v0.5.1
|
2020-10-30 v0.5.1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.TH SERVEFILE 1 "September 2020" "servefile 0.5.1" "User Commands"
|
.TH SERVEFILE 1 "January 2023" "servefile 0.5.4" "User Commands"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
servefile \- small HTTP-Server for temporary file transfer
|
servefile \- small HTTP-Server for temporary file transfer
|
||||||
|
@ -28,8 +28,8 @@ In upload mode with \fB\-u\fR servefile creates a directory and saves all
|
||||||
uploaded files into that directory. When uploading with curl or wget the
|
uploaded files into that directory. When uploading with curl or wget the
|
||||||
filename is extracted from the path part of the url used for the upload.
|
filename is extracted from the path part of the url used for the upload.
|
||||||
|
|
||||||
For SSL support python-openssl (pyssl) needs to be installed. If no key and
|
For SSL support pyopenssl (python3-openssl) needs to be installed. If no key
|
||||||
cert is given, servefile will generate a key pair for you and display its
|
and cert is given, servefile will generate a key pair for you and display its
|
||||||
fingerprint.
|
fingerprint.
|
||||||
|
|
||||||
In \fB--tar\fR mode the given file or directory will be packed on (each)
|
In \fB--tar\fR mode the given file or directory will be packed on (each)
|
||||||
|
|
|
@ -7,11 +7,10 @@
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
__version__ = '0.5.1'
|
__version__ = '0.5.4'
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
import cgi
|
|
||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
@ -21,7 +20,9 @@ import select
|
||||||
import socket
|
import socket
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import warnings
|
||||||
|
|
||||||
# fix imports for python2/python3
|
# fix imports for python2/python3
|
||||||
try:
|
try:
|
||||||
|
@ -42,11 +43,18 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
# scheduled for removal in python3.13, used for FieldStorage
|
||||||
|
import cgi
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -60,7 +68,7 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
fileName = self.fileName
|
fileName = self.fileName
|
||||||
if unquote(self.path) != "/" + fileName:
|
if unquote(self.path) != "/" + fileName:
|
||||||
self.send_response(302)
|
self.send_response(302)
|
||||||
self.send_header('Location', '/' + fileName)
|
self.send_header('Location', '/' + quote(fileName))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -141,7 +149,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 +254,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 +424,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>
|
||||||
|
@ -524,10 +532,7 @@ class DirListingHandler(FileBaseHandler):
|
||||||
target_items.append((item, itemPath, stat))
|
target_items.append((item, itemPath, stat))
|
||||||
|
|
||||||
# Directories first, then files
|
# Directories first, then files
|
||||||
for (tuple_list, is_dir) in (
|
for (tuple_list, is_dir) in ((dir_items, True), (file_items, False)):
|
||||||
(dir_items, True),
|
|
||||||
(file_items, False),
|
|
||||||
):
|
|
||||||
for (item, itemPath, stat) in tuple_list:
|
for (item, itemPath, stat) in tuple_list:
|
||||||
self._appendToListing(content, item, itemPath, stat, is_dir=is_dir)
|
self._appendToListing(content, item, itemPath, stat, is_dir=is_dir)
|
||||||
|
|
||||||
|
@ -542,7 +547,9 @@ class DirListingHandler(FileBaseHandler):
|
||||||
self.send_header("Content-Length", str(len(listing)))
|
self.send_header("Content-Length", str(len(listing)))
|
||||||
self.send_header('Connection', 'close')
|
self.send_header('Connection', 'close')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(listing.encode())
|
if sys.version_info.major >= 3:
|
||||||
|
listing = listing.encode()
|
||||||
|
self.wfile.write(listing)
|
||||||
|
|
||||||
def convertSize(self, size):
|
def convertSize(self, size):
|
||||||
for ext in "KMGT":
|
for ext in "KMGT":
|
||||||
|
@ -607,8 +614,25 @@ class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
# create FieldStorage object for multipart parsing
|
# create FieldStorage object for multipart parsing
|
||||||
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)
|
targetDir = self.targetDir
|
||||||
if not "file" in fstorage:
|
|
||||||
|
class CustomFieldStorage(cgi.FieldStorage):
|
||||||
|
|
||||||
|
def make_file(self, *args, **kwargs):
|
||||||
|
"""Overwritten to use a named file and the upload directory
|
||||||
|
|
||||||
|
Python 2.7 has an unused "binary" argument while Python 3 does
|
||||||
|
not have any arguments. Python 2.7 does not have a
|
||||||
|
self._binary_file attribute.
|
||||||
|
"""
|
||||||
|
if sys.version_info.major == 2 or self._binary_file:
|
||||||
|
return tempfile.NamedTemporaryFile("wb+", dir=targetDir)
|
||||||
|
else:
|
||||||
|
return tempfile.NamedTemporaryFile(
|
||||||
|
"w+", encoding=self.encoding, newline='\n', dir=targetDir)
|
||||||
|
|
||||||
|
fstorage = CustomFieldStorage(fp=self.rfile, headers=self.headers, environ=env)
|
||||||
|
if "file" not in fstorage:
|
||||||
self.sendResponse(400, "No file found in request.")
|
self.sendResponse(400, "No file found in request.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -617,7 +641,14 @@ class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
self.sendResponse(400, "Filename was empty or invalid")
|
self.sendResponse(400, "Filename was empty or invalid")
|
||||||
return
|
return
|
||||||
|
|
||||||
# write file down to disk, send a 200 afterwards
|
# put the file at the right place, send 200 afterwards
|
||||||
|
if getattr(fstorage["file"].file, "name", None):
|
||||||
|
# the sent file was large, so we can just hard link the temporary
|
||||||
|
# file and are done
|
||||||
|
os.link(fstorage["file"].file.name, destFileName)
|
||||||
|
else:
|
||||||
|
# write file to disk. it was small enough so no temporary file was
|
||||||
|
# created
|
||||||
target = open(destFileName, "wb")
|
target = open(destFileName, "wb")
|
||||||
bytesLeft = length
|
bytesLeft = length
|
||||||
while bytesLeft > 0:
|
while bytesLeft > 0:
|
||||||
|
@ -635,7 +666,7 @@ class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
http://host:8080/testfile will cause the file to be named testfile. If
|
http://host:8080/testfile will cause the file to be named testfile. If
|
||||||
no filename is given, a random name will be generated.
|
no filename is given, a random name will be generated.
|
||||||
|
|
||||||
Files can be uploaded with e.g. curl -X POST -d @file <url> .
|
Files can be uploaded with e.g. curl -T file <url> .
|
||||||
"""
|
"""
|
||||||
length = self.getContentLength()
|
length = self.getContentLength()
|
||||||
if length < 0:
|
if length < 0:
|
||||||
|
@ -652,11 +683,11 @@ class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Sometimes clients want to be told to continue with their transfer
|
# Sometimes clients want to be told to continue with their transfer
|
||||||
if self.headers.getheader("Expect") == "100-continue":
|
if self.headers.get("Expect") == "100-continue":
|
||||||
self.send_response(100)
|
self.send_response(100)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
target = open(cleanFileName, "w")
|
target = open(cleanFileName, "wb")
|
||||||
bytesLeft = int(self.headers['Content-Length'])
|
bytesLeft = int(self.headers['Content-Length'])
|
||||||
while bytesLeft > 0:
|
while bytesLeft > 0:
|
||||||
bytesToRead = min(self.blockSize, bytesLeft)
|
bytesToRead = min(self.blockSize, bytesLeft)
|
||||||
|
@ -675,7 +706,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
|
||||||
|
|
||||||
|
@ -712,6 +744,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()
|
||||||
|
@ -787,6 +820,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
|
||||||
|
|
||||||
|
@ -811,7 +845,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 """
|
||||||
|
@ -825,23 +860,23 @@ 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
|
||||||
else:
|
else:
|
||||||
del(os.environ['LC_ALL'])
|
del os.environ['LC_ALL']
|
||||||
if proc.wait() != 0:
|
if proc.wait() != 0:
|
||||||
# we couldn't find any ip address
|
# we couldn't find any ip address
|
||||||
proc = None
|
proc = None
|
||||||
|
@ -858,7 +893,7 @@ class ServeFile():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def setSSLKeys(self, cert, key):
|
def setSSLKeys(self, cert, key):
|
||||||
""" Set SSL cert/key. Can be either path to file or pyssl X509/PKey object. """
|
""" Set SSL cert/key. Can be either path to file or pyopenssl X509/PKey object. """
|
||||||
self.cert = cert
|
self.cert = cert
|
||||||
self.key = key
|
self.key = key
|
||||||
|
|
||||||
|
@ -884,7 +919,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
|
||||||
|
@ -949,7 +984,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)
|
||||||
|
@ -982,7 +1018,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] == '':
|
||||||
|
@ -1039,7 +1075,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)
|
||||||
|
@ -1058,6 +1095,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
|
||||||
|
@ -1103,7 +1141,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())
|
||||||
|
@ -1113,32 +1152,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()
|
||||||
|
@ -1154,7 +1196,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)
|
||||||
|
@ -1167,7 +1209,7 @@ def main():
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.ssl and not HAVE_SSL:
|
if args.ssl and not HAVE_SSL:
|
||||||
print("Error: SSL is not available, please install pyssl (python-openssl).")
|
print("Error: SSL is not available, please install pyopenssl (python3-openssl).")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.cert and not args.key:
|
if args.cert and not args.key:
|
||||||
|
@ -1181,7 +1223,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:
|
||||||
|
@ -1252,4 +1295,3 @@ def main():
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -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.4',
|
||||||
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',
|
||||||
|
@ -38,10 +38,11 @@ setup(
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python :: 2',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
'Programming Language :: Python :: 3.8',
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
|
'Programming Language :: Python :: 3.11',
|
||||||
'Topic :: Communications',
|
'Topic :: Communications',
|
||||||
'Topic :: Communications :: File Sharing',
|
'Topic :: Communications :: File Sharing',
|
||||||
'Topic :: Internet',
|
'Topic :: Internet',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -8,18 +9,36 @@ import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import time
|
import time
|
||||||
import urllib3
|
import urllib3
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
# crudly written to learn more about pytest and to have a base for refactoring
|
# crudly written to learn more about pytest and to have a base for refactoring
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info.major >= 3:
|
if sys.version_info.major >= 3:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
connrefused_exc = ConnectionRefusedError
|
connrefused_exc = ConnectionRefusedError
|
||||||
else:
|
else:
|
||||||
from pathlib2 import Path
|
from pathlib2 import Path
|
||||||
|
from urllib import quote
|
||||||
connrefused_exc = socket.error
|
connrefused_exc = socket.error
|
||||||
|
|
||||||
|
|
||||||
|
def _get_port_from_env(var_name, default):
|
||||||
|
port = int(os.environ.get(var_name, default))
|
||||||
|
if port == 0:
|
||||||
|
# do a one-time port selection for a free port, use it for all tests
|
||||||
|
s = socket.socket()
|
||||||
|
s.bind(('', 0))
|
||||||
|
port = s.getsockname()[1]
|
||||||
|
s.close()
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
SERVEFILE_DEFAULT_PORT = _get_port_from_env('SERVEFILE_DEFAULT_PORT', 0)
|
||||||
|
SERVEFILE_SECONDARY_PORT = _get_port_from_env('SERVEFILE_SECONDARY_PORT', 0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def run_servefile():
|
def run_servefile():
|
||||||
instances = []
|
instances = []
|
||||||
|
@ -34,9 +53,12 @@ def run_servefile():
|
||||||
# call servefile as python module
|
# call servefile as python module
|
||||||
servefile_path = ['-m', 'servefile']
|
servefile_path = ['-m', 'servefile']
|
||||||
|
|
||||||
|
# use non-default default port, if one is given via env (and none via args)
|
||||||
|
if '-p' not in args and '--port' not in args:
|
||||||
|
args.extend(['-p', str(SERVEFILE_DEFAULT_PORT)])
|
||||||
|
|
||||||
print("running {} with args {}".format(", ".join(servefile_path), args))
|
print("running {} with args {}".format(", ".join(servefile_path), args))
|
||||||
p = subprocess.Popen([sys.executable] + servefile_path + args, **kwargs)
|
p = subprocess.Popen([sys.executable] + servefile_path + args, **kwargs)
|
||||||
time.sleep(kwargs.get('timeout', 0.3))
|
|
||||||
instances.append(p)
|
instances.append(p)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
@ -62,22 +84,26 @@ def datadir(tmp_path):
|
||||||
_datadir(v, new_path)
|
_datadir(v, new_path)
|
||||||
else:
|
else:
|
||||||
if hasattr(v, 'decode'):
|
if hasattr(v, 'decode'):
|
||||||
v = v.decode() # python2 compability
|
v = v.decode('utf-8') # python2 compability
|
||||||
(path / k).write_text(v)
|
(path / k).write_text(v)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
return _datadir
|
return _datadir
|
||||||
|
|
||||||
|
|
||||||
def make_request(path='/', host='localhost', port=8080, method='get', protocol='http', **kwargs):
|
def make_request(path='/', host='localhost', port=SERVEFILE_DEFAULT_PORT, method='get', protocol='http',
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def check_download(expected_data=None, path='/', fname=None, status_code=200, **kwargs):
|
def check_download(expected_data=None, path='/', fname=None, **kwargs):
|
||||||
if fname is None:
|
if fname is None:
|
||||||
fname = os.path.basename(path)
|
fname = os.path.basename(path)
|
||||||
r = make_request(path, **kwargs)
|
r = make_request(path, **kwargs)
|
||||||
|
@ -91,6 +117,22 @@ def check_download(expected_data=None, path='/', fname=None, status_code=200, **
|
||||||
return r # for additional tests
|
return r # for additional tests
|
||||||
|
|
||||||
|
|
||||||
|
def _retry_while(exception, function, timeout=2):
|
||||||
|
now = time.time # float seconds since epoch
|
||||||
|
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
timeout_after = now() + timeout
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
except exception:
|
||||||
|
if now() >= timeout_after:
|
||||||
|
raise
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def _test_version(run_servefile, standalone):
|
def _test_version(run_servefile, standalone):
|
||||||
# we expect the version on stdout (python3.4+) or stderr(python2.6-3.3)
|
# we expect the version on stdout (python3.4+) or stderr(python2.6-3.3)
|
||||||
s = run_servefile('--version', standalone=standalone, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
s = run_servefile('--version', standalone=standalone, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
@ -104,7 +146,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.4'
|
||||||
|
|
||||||
|
|
||||||
def test_version(run_servefile):
|
def test_version(run_servefile):
|
||||||
|
@ -121,7 +163,7 @@ def test_correct_headers(run_servefile, datadir):
|
||||||
p = datadir({'testfile': data}) / 'testfile'
|
p = datadir({'testfile': data}) / 'testfile'
|
||||||
run_servefile(str(p))
|
run_servefile(str(p))
|
||||||
|
|
||||||
r = make_request()
|
r = _retry_while(ConnectionError, make_request)()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.headers.get('Content-Type') == 'application/octet-stream'
|
assert r.headers.get('Content-Type') == 'application/octet-stream'
|
||||||
assert r.headers.get('Content-Disposition') == 'attachment; filename="testfile"'
|
assert r.headers.get('Content-Disposition') == 'attachment; filename="testfile"'
|
||||||
|
@ -134,7 +176,7 @@ def test_redirect_and_download(run_servefile, datadir):
|
||||||
run_servefile(str(p))
|
run_servefile(str(p))
|
||||||
|
|
||||||
# redirect
|
# redirect
|
||||||
r = make_request(allow_redirects=False)
|
r = _retry_while(ConnectionError, make_request)(allow_redirects=False)
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
assert r.headers.get('Location') == '/testfile'
|
assert r.headers.get('Location') == '/testfile'
|
||||||
|
|
||||||
|
@ -142,12 +184,29 @@ def test_redirect_and_download(run_servefile, datadir):
|
||||||
check_download(data, fname='testfile')
|
check_download(data, fname='testfile')
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_and_download_with_umlaut(run_servefile, datadir):
|
||||||
|
data = "NÖÖT NÖÖT"
|
||||||
|
filename = "tästføile"
|
||||||
|
p = datadir({filename: data}) / filename
|
||||||
|
run_servefile(str(p))
|
||||||
|
|
||||||
|
# redirect
|
||||||
|
r = _retry_while(ConnectionError, make_request)(allow_redirects=False)
|
||||||
|
assert r.status_code == 302
|
||||||
|
assert r.headers.get('Location') == '/{}'.format(quote(filename))
|
||||||
|
|
||||||
|
# normal download
|
||||||
|
if sys.version_info.major < 3:
|
||||||
|
data = unicode(data, 'utf-8')
|
||||||
|
check_download(data, fname=filename)
|
||||||
|
|
||||||
|
|
||||||
def test_specify_port(run_servefile, datadir):
|
def test_specify_port(run_servefile, datadir):
|
||||||
data = "NOOT NOOT"
|
data = "NOOT NOOT"
|
||||||
p = datadir({'testfile': data}) / 'testfile'
|
p = datadir({'testfile': data}) / 'testfile'
|
||||||
run_servefile([str(p), '-p', '8081'])
|
run_servefile([str(p), '-p', str(SERVEFILE_SECONDARY_PORT)])
|
||||||
|
|
||||||
check_download(data, fname='testfile', port=8081)
|
_retry_while(ConnectionError, check_download)(data, fname='testfile', port=SERVEFILE_SECONDARY_PORT)
|
||||||
|
|
||||||
|
|
||||||
def test_ipv4_only(run_servefile, datadir):
|
def test_ipv4_only(run_servefile, datadir):
|
||||||
|
@ -155,11 +214,11 @@ def test_ipv4_only(run_servefile, datadir):
|
||||||
p = datadir({'testfile': data}) / 'testfile'
|
p = datadir({'testfile': data}) / 'testfile'
|
||||||
run_servefile([str(p), '-4'])
|
run_servefile([str(p), '-4'])
|
||||||
|
|
||||||
check_download(data, fname='testfile', host='127.0.0.1')
|
_retry_while(ConnectionError, check_download)(data, fname='testfile', host='127.0.0.1')
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET6)
|
sock = socket.socket(socket.AF_INET6)
|
||||||
with pytest.raises(connrefused_exc):
|
with pytest.raises(connrefused_exc):
|
||||||
sock.connect(("::1", 8080))
|
sock.connect(("::1", SERVEFILE_DEFAULT_PORT))
|
||||||
|
|
||||||
|
|
||||||
def test_big_download(run_servefile, datadir):
|
def test_big_download(run_servefile, datadir):
|
||||||
|
@ -168,7 +227,7 @@ def test_big_download(run_servefile, datadir):
|
||||||
p = datadir({'testfile': data}) / 'testfile'
|
p = datadir({'testfile': data}) / 'testfile'
|
||||||
run_servefile(str(p))
|
run_servefile(str(p))
|
||||||
|
|
||||||
check_download(data, fname='testfile')
|
_retry_while(ConnectionError, check_download)(data, fname='testfile')
|
||||||
|
|
||||||
|
|
||||||
def test_authentication(run_servefile, datadir):
|
def test_authentication(run_servefile, datadir):
|
||||||
|
@ -177,11 +236,11 @@ def test_authentication(run_servefile, datadir):
|
||||||
|
|
||||||
run_servefile([str(p), '-a', 'user:password'])
|
run_servefile([str(p), '-a', 'user:password'])
|
||||||
for auth in [('foo', 'bar'), ('user', 'wrong'), ('unknown', 'password')]:
|
for auth in [('foo', 'bar'), ('user', 'wrong'), ('unknown', 'password')]:
|
||||||
r = make_request(auth=auth)
|
r = _retry_while(ConnectionError, make_request)(auth=auth)
|
||||||
assert '401 - Unauthorized' in r.text
|
assert '401 - Unauthorized' in r.text
|
||||||
assert r.status_code == 401
|
assert r.status_code == 401
|
||||||
|
|
||||||
check_download(data, fname='testfile', auth=('user', 'password'))
|
_retry_while(ConnectionError, check_download)(data, fname='testfile', auth=('user', 'password'))
|
||||||
|
|
||||||
|
|
||||||
def test_serve_directory(run_servefile, datadir):
|
def test_serve_directory(run_servefile, datadir):
|
||||||
|
@ -190,6 +249,7 @@ def test_serve_directory(run_servefile, datadir):
|
||||||
'bar': {'thisisaverylongfilenamefortestingthatthisstillworksproperly': 'jup!'},
|
'bar': {'thisisaverylongfilenamefortestingthatthisstillworksproperly': 'jup!'},
|
||||||
'noot': 'still data in here',
|
'noot': 'still data in here',
|
||||||
'bigfile': 'x' * (10 * 1024 ** 2),
|
'bigfile': 'x' * (10 * 1024 ** 2),
|
||||||
|
'möwe': 'KRAKRAKRAKA',
|
||||||
}
|
}
|
||||||
p = datadir(d)
|
p = datadir(d)
|
||||||
run_servefile([str(p), '-l'])
|
run_servefile([str(p), '-l'])
|
||||||
|
@ -197,12 +257,12 @@ def test_serve_directory(run_servefile, datadir):
|
||||||
# check if all files are in directory listing
|
# check if all files are in directory listing
|
||||||
# (could be made more sophisticated with beautifulsoup)
|
# (could be made more sophisticated with beautifulsoup)
|
||||||
for path in '/', '/../':
|
for path in '/', '/../':
|
||||||
r = make_request(path)
|
r = _retry_while(ConnectionError, make_request)(path)
|
||||||
for k in d:
|
for k in d:
|
||||||
assert k in r.text
|
assert quote(k) in r.text
|
||||||
|
|
||||||
for fname, content in d['foo'].items():
|
for fname, content in d['foo'].items():
|
||||||
check_download(content, '/foo/' + fname)
|
_retry_while(ConnectionError, check_download)(content, '/foo/' + fname)
|
||||||
|
|
||||||
r = make_request('/unknown')
|
r = make_request('/unknown')
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
@ -224,7 +284,7 @@ def test_serve_relative_directory(run_servefile, datadir):
|
||||||
# check if all files are in directory listing
|
# check if all files are in directory listing
|
||||||
# (could be made more sophisticated with beautifulsoup)
|
# (could be made more sophisticated with beautifulsoup)
|
||||||
for path in '/', '/../':
|
for path in '/', '/../':
|
||||||
r = make_request(path)
|
r = _retry_while(ConnectionError, make_request)(path)
|
||||||
for k in d:
|
for k in d:
|
||||||
assert k in r.text
|
assert k in r.text
|
||||||
|
|
||||||
|
@ -248,14 +308,14 @@ def test_upload(run_servefile, tmp_path):
|
||||||
|
|
||||||
run_servefile(['-u', str(uploaddir)])
|
run_servefile(['-u', str(uploaddir)])
|
||||||
|
|
||||||
# check that servefile created the directory
|
|
||||||
assert uploaddir.is_dir()
|
|
||||||
|
|
||||||
# check upload form present
|
# check upload form present
|
||||||
r = make_request()
|
r = _retry_while(ConnectionError, make_request)()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert 'multipart/form-data' in r.text
|
assert 'multipart/form-data' in r.text
|
||||||
|
|
||||||
|
# check that servefile created the directory
|
||||||
|
assert uploaddir.is_dir()
|
||||||
|
|
||||||
# upload file
|
# upload file
|
||||||
files = {'file': ('haiku.txt', data)}
|
files = {'file': ('haiku.txt', data)}
|
||||||
r = make_request(method='post', files=files)
|
r = make_request(method='post', files=files)
|
||||||
|
@ -271,6 +331,13 @@ def test_upload(run_servefile, tmp_path):
|
||||||
with open(str(uploaddir / 'haiku.txt(1)')) as f:
|
with open(str(uploaddir / 'haiku.txt(1)')) as f:
|
||||||
assert f.read() == data
|
assert f.read() == data
|
||||||
|
|
||||||
|
# upload file using PUT
|
||||||
|
r = make_request("/haiku.txt", method='put', data=data)
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert 'OK!' in r.text
|
||||||
|
with open(str(uploaddir / 'haiku.txt(2)')) as f:
|
||||||
|
assert f.read() == data
|
||||||
|
|
||||||
|
|
||||||
def test_upload_size_limit(run_servefile, tmp_path):
|
def test_upload_size_limit(run_servefile, tmp_path):
|
||||||
uploaddir = tmp_path / 'upload'
|
uploaddir = tmp_path / 'upload'
|
||||||
|
@ -278,7 +345,7 @@ def test_upload_size_limit(run_servefile, tmp_path):
|
||||||
|
|
||||||
# upload file that is too big
|
# upload file that is too big
|
||||||
files = {'file': ('toobig', "x" * 2049)}
|
files = {'file': ('toobig', "x" * 2049)}
|
||||||
r = make_request(method='post', files=files)
|
r = _retry_while(ConnectionError, make_request)(method='post', files=files)
|
||||||
assert 'Your file was too big' in r.text
|
assert 'Your file was too big' in r.text
|
||||||
assert r.status_code == 413
|
assert r.status_code == 413
|
||||||
assert not (uploaddir / 'toobig').exists()
|
assert not (uploaddir / 'toobig').exists()
|
||||||
|
@ -290,6 +357,20 @@ def test_upload_size_limit(run_servefile, tmp_path):
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_large_file(run_servefile, tmp_path):
|
||||||
|
# small files end up in BytesIO while large files get temporary files. this
|
||||||
|
# test makes sure we hit the large file codepath at least once
|
||||||
|
uploaddir = tmp_path / 'upload'
|
||||||
|
run_servefile(['-u', str(uploaddir)])
|
||||||
|
|
||||||
|
data = "asdf" * 1024
|
||||||
|
files = {'file': ('more_data.txt', data)}
|
||||||
|
r = _retry_while(ConnectionError, make_request)(method='post', files=files)
|
||||||
|
assert r.status_code == 200
|
||||||
|
with open(str(uploaddir / 'more_data.txt')) as f:
|
||||||
|
assert f.read() == data
|
||||||
|
|
||||||
|
|
||||||
def test_tar_mode(run_servefile, datadir):
|
def test_tar_mode(run_servefile, datadir):
|
||||||
d = {
|
d = {
|
||||||
'foo': {
|
'foo': {
|
||||||
|
@ -303,7 +384,7 @@ def test_tar_mode(run_servefile, datadir):
|
||||||
# test redirect?
|
# test redirect?
|
||||||
|
|
||||||
# test contents of tar file
|
# test contents of tar file
|
||||||
r = make_request()
|
r = _retry_while(ConnectionError, make_request)()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
tar = tarfile.open(fileobj=io.BytesIO(r.content))
|
tar = tarfile.open(fileobj=io.BytesIO(r.content))
|
||||||
assert len(tar.getmembers()) == 3
|
assert len(tar.getmembers()) == 3
|
||||||
|
@ -319,7 +400,7 @@ def test_tar_compression(run_servefile, datadir):
|
||||||
p = datadir(d)
|
p = datadir(d)
|
||||||
run_servefile(['-c', 'gzip', '-t', str(p / 'foo')])
|
run_servefile(['-c', 'gzip', '-t', str(p / 'foo')])
|
||||||
|
|
||||||
r = make_request()
|
r = _retry_while(ConnectionError, make_request)()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
tar = tarfile.open(fileobj=io.BytesIO(r.content), mode='r:gz')
|
tar = tarfile.open(fileobj=io.BytesIO(r.content), mode='r:gz')
|
||||||
assert len(tar.getmembers()) == 1
|
assert len(tar.getmembers()) == 1
|
||||||
|
@ -329,7 +410,6 @@ def test_https(run_servefile, datadir):
|
||||||
data = "NOOT NOOT"
|
data = "NOOT NOOT"
|
||||||
p = datadir({'testfile': data}) / 'testfile'
|
p = datadir({'testfile': data}) / 'testfile'
|
||||||
run_servefile(['--ssl', str(p)])
|
run_servefile(['--ssl', str(p)])
|
||||||
time.sleep(0.2) # time for generating ssl certificates
|
|
||||||
|
|
||||||
# fingerprint = None
|
# fingerprint = None
|
||||||
# while not fingerprint:
|
# while not fingerprint:
|
||||||
|
@ -344,7 +424,7 @@ def test_https(run_servefile, datadir):
|
||||||
|
|
||||||
# assert fingerprint
|
# assert fingerprint
|
||||||
urllib3.disable_warnings()
|
urllib3.disable_warnings()
|
||||||
check_download(data, protocol='https', verify=False)
|
_retry_while(ConnectionError, check_download)(data, protocol='https', verify=False)
|
||||||
|
|
||||||
|
|
||||||
def test_https_big_download(run_servefile, datadir):
|
def test_https_big_download(run_servefile, datadir):
|
||||||
|
@ -352,10 +432,9 @@ def test_https_big_download(run_servefile, datadir):
|
||||||
data = "x" * (10 * 1024 ** 2)
|
data = "x" * (10 * 1024 ** 2)
|
||||||
p = datadir({'testfile': data}) / 'testfile'
|
p = datadir({'testfile': data}) / 'testfile'
|
||||||
run_servefile(['--ssl', str(p)])
|
run_servefile(['--ssl', str(p)])
|
||||||
time.sleep(0.2) # time for generating ssl certificates
|
|
||||||
|
|
||||||
urllib3.disable_warnings()
|
urllib3.disable_warnings()
|
||||||
check_download(data, protocol='https', verify=False)
|
_retry_while(ConnectionError, check_download)(data, protocol='https', verify=False)
|
||||||
|
|
||||||
|
|
||||||
def test_abort_download(run_servefile, datadir):
|
def test_abort_download(run_servefile, datadir):
|
||||||
|
@ -368,7 +447,7 @@ def test_abort_download(run_servefile, datadir):
|
||||||
# provoke a connection abort
|
# provoke a connection abort
|
||||||
# hopefully the buffers will not fill up with all of the 10mb
|
# hopefully the buffers will not fill up with all of the 10mb
|
||||||
sock = socket.socket(socket.AF_INET)
|
sock = socket.socket(socket.AF_INET)
|
||||||
sock.connect(("localhost", 8080))
|
_retry_while(connrefused_exc, sock.connect)(("localhost", SERVEFILE_DEFAULT_PORT))
|
||||||
sock.send(b"GET /testfile HTTP/1.0\n\n")
|
sock.send(b"GET /testfile HTTP/1.0\n\n")
|
||||||
resp = sock.recv(100)
|
resp = sock.recv(100)
|
||||||
assert resp != b''
|
assert resp != b''
|
||||||
|
|
14
tox.ini
14
tox.ini
|
@ -1,9 +1,19 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py27,py36,py37,py38
|
envlist = py27,py37,py38,py39,py310,py311,pep8
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
pathlib2; python_version<"3"
|
pathlib2; python_version<"3"
|
||||||
pytest
|
pytest
|
||||||
requests
|
requests
|
||||||
commands = pytest --tb=short {posargs}
|
flake8
|
||||||
|
commands = pytest -v --tb=short {posargs}
|
||||||
|
|
||||||
|
[testenv:pep8]
|
||||||
|
commands = flake8 servefile/ {posargs}
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
show-source = True
|
||||||
|
max-line-length = 120
|
||||||
|
ignore = E123,E125,E241,E402,E741,W503,W504,H301
|
||||||
|
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
||||||
|
|
Loading…
Reference in New Issue