Last active
February 14, 2019 10:38
-
-
Save Francesco149/8e5b29091bee8b7f630772cc459d8bcc to your computer and use it in GitHub Desktop.
minimal file sharing/upload server example in python 2.7
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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