Skip to content

Instantly share code, notes, and snippets.

@Francesco149
Last active February 14, 2019 10:38
Show Gist options
  • Save Francesco149/8e5b29091bee8b7f630772cc459d8bcc to your computer and use it in GitHub Desktop.
Save Francesco149/8e5b29091bee8b7f630772cc459d8bcc to your computer and use it in GitHub Desktop.
minimal file sharing/upload server example in python 2.7
#!/usr/bin/env python
"""
minimal example of a file sharing service in python 2.7 using only built-ins
there is no error check or security, this is meant to be as a base/reference
to quickly get started
This is free and unencumbered software released into the public domain.
http://unlicense.org/
usage:
$ chmod +x ./minupload
$ ./minupload
$ curl --form file=@/path/to/file.png http://localhost:8080/upload
{
"status": 200,
"data": {"url": "http://localhost:8080/f/PJ28rniy.png"},
"success": true
}
$ curl --head --location http://localhost:8080/f/PJ28rniy.png
HTTP/1.0 200 OK
Server: BaseHTTP/0.3 Python/2.7.10
Date: Wed, 28 Jun 2017 00:43:39 GMT
Content-Type: image/png
Content-Encoding: None
Content-Length: 25182
ShareX / ShareNix settings:
{
"Name": "localhost",
"RequestType": "POST",
"RequestURL": "http://localhost:8080/upload",
"FileFormName": "file",
"ResponseType": "Text",
"URL": "$json:data.url$"
}
to make it public, change host to an empty string and bind port 8080 to
port 80 from your firewall
if you don't understand something in the code, detailed documentation is in
the attached q_documentation.txt file
"""
import os
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
import mimetypes as memetypes
import shutil
import cgi
import random
import string
import json
SV_HOST="localhost"
SV_PORT=8080
SITE_ROOT="http://localhost:8080"
BASE62_CHARSET=string.ascii_lowercase + string.digits + string.ascii_uppercase
def rand_string(n=8, charset=BASE62_CHARSET):
res = ""
for i in range(n):
res += random.choice(charset)
return res
class Handler(BaseHTTPRequestHandler):
def do_HEAD(self):
self.send_headers()
def send_headers(self):
npath = os.path.normpath(self.path)
npath = npath[1:]
path_elements = npath.split('/')
if path_elements[0] == "f":
reqfile = path_elements[1]
if not os.path.isfile(reqfile) or not os.access(reqfile, os.R_OK):
self.send_error(404, "file not found")
return None
content, encoding = memetypes.MimeTypes().guess_type(reqfile)
if content is None:
content = "application/octet-stream"
info = os.stat(reqfile)
self.send_response(200)
self.send_header("Content-Type", content)
self.send_header("Content-Encoding", encoding)
self.send_header("Content-Length", info.st_size)
self.end_headers()
elif path_elements[0] == "upload":
self.send_response(200)
self.send_header("Content-Type", "text/json; charset=utf-8")
self.end_headers()
else:
self.send_error(404, "fuck")
return None
return path_elements
def do_GET(self):
elements = self.send_headers()
if elements is None:
return
reqfile = elements[1]
f = open(reqfile, 'rb')
shutil.copyfileobj(f, self.wfile)
f.close()
def do_POST(self):
elements = self.send_headers()
if elements is None or elements[0] != "upload":
return
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": self.headers['Content-Type']
})
_, ext = os.path.splitext(form["file"].filename)
fname = rand_string() + ext
while os.path.isfile(fname):
fname = rand_string() + ext
fdst = open(fname, "wb")
shutil.copyfileobj(form["file"].file, fdst)
fdst.close()
result = {
"data": { "url": SITE_ROOT + "/f/" + fname },
"success": True,
"status": 200,
}
self.wfile.write(json.dumps(result))
HTTPServer((SV_HOST, SV_PORT), Handler).serve_forever()
HTTP headers:
one or more lines separated by a CRLF ("\r\n").
requests:
first line is "method /path http-version"
http://google.com/images -> GET /images HTTP/1.1
responses:
first line is "http-version code message"
HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
remaining lines are named fields defined as
FieldName: value
the headers are terminated by an empty line
raw data can optionally follow
example:
POST /upload HTTP/1.1<CR><LF>
Content-Length: 12<CR><LF>
<CR><LF>
Hello World!
https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html
BaseHTTPRequesthandler:
generic interface provided by python to handle standard HTTP requests.
send_response(code):
sends the first line of a response
send_response(200) -> HTTP/1.1 200 OK
send_header(name, value):
sends a request/response field
send_header("Content-Length", 11) -> Content-Length: 11
end_headers():
terminates headers with an empty line
send_error(code, message):
sends an error response and displays a predefined error
page with the given message
send_error(404, "blahblah") -> HTTP/1.1 404 Not Found ...
path:
contains the requested path
http://google.com/images -> /images
because of the leading slash, we do
npath = npath[1:]
in send_headers to strip it and turn it
into a relative path
wfile:
a file stream to write data to the client. the response
headers and the data that can follow them will be written here
rfile:
a file stream to read data from the client. the request
headers and the data that can follow them will be read from here
do_HEAD:
automatically called when a HTTP HEAD request is received.
HEAD is requested when the client just wants to know if the
file exists, how big it is and stuff like that, so we just send
the headers but not the content
do_GET:
automatically called when a HTTP GET request is received.
GET expects the same stuff as HEAD, plus the actual content.
because we want to send the same headers on GET, HEAD, POST,
the code that figures out what response headers to send is
separated into send_headers() which sends said headers and
returns an array with the requested path split into elements
HTTPServer((host, port), handler).serve_forever():
creates a HTTPServer object bound to given ip and port
and calls serve_forever which starts listening and calls methods
from our handler when requests are made
potential issues:
content, encoding = memetypes.MimeTypes().guess_type(reqfile)
if content is None:
content = "application/octet-stream"
this check is very weak. it guesses the file type from the file
extension, which can be easily faked or mistakenly named with the
wrong one.
a better method is to find a library that does "mime sniffing",
which looks at the beginning of the file for known patterns to
guess the file type.
.
fname = rand_string() + ext
while os.path.isfile(fname):
fname = rand_string() + ext
fdst = open(fname, "wb")
shutil.copyfileobj(form["file"].file, fdst)
fdst.close()
potential race condition if two users upload a file at the same time
and happen to get the same file extension and same random string.
very unlikely
also, not very deterministic performance if you have a lot of files
and the random string happens to collide many times with existing files
better ways include:
- sequential id (0, 1, 2, ... a, b, c, ...) in some numeric base
- some hash that has decent collision resistance
adding a front-end:
this example only provides a POST api to upload that returns json
(designed for ShareX and other applications) and the /f/ endpoint
to view uploaded files.
adding a front-end would be as simple as setting Content-Type to
text/html on certain urls, such as / for the homepage, and
writing a .html file following the response headers.
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment