1124 lines
43 KiB
Python
1124 lines
43 KiB
Python
#! /usr/bin/python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Droopy (http://stackp.online.fr/droopy)
|
||
Copyright 2008-2013 (c) Pierre Duquesne <stackp@online.fr>
|
||
Licensed under the New BSD License.
|
||
|
||
Changelog
|
||
20151025 * Global variables removed
|
||
* Code refactoring and re-layout
|
||
* Python 2 and 3 compatibility
|
||
* Efficiency and Security improvements
|
||
* Added --config-file option.
|
||
* Retains backwards compatibility.
|
||
20131121 * Update HTML/CSS for mobile devices
|
||
* Add HTTPS support
|
||
* Add HTTP basic authentication
|
||
* Add option to change uploaded file permissions
|
||
* Add support for HTML5 multiple file upload
|
||
20120108 * Taiwanese translation by Li-cheng Hsu.
|
||
20110928 * Correctly save message with --save-config. Fix by Sven Radde.
|
||
20110708 * Polish translation by Jacek Politowski.
|
||
20110625 * Fix bug regarding filesystem name encoding.
|
||
* Save the --dl option when --save-config is passed.
|
||
20110501 * Add the --dl option to let clients download files.
|
||
* CSS speech bubble.
|
||
20101130 * CSS and HTML update. Switch to the new BSD License.
|
||
20100523 * Simplified Chinese translation by Ye Wei.
|
||
20100521 * Hungarian translation by Csaba Szigetvári.
|
||
* Russian translation by muromec.
|
||
* Use %APPDATA% Windows environment variable -- fix by Maik.
|
||
20091229 * Brazilian Portuguese translation by
|
||
Carlos Eduardo Moreira dos Santos and Toony Poony.
|
||
* IE layout fix by Carlos Eduardo Moreira dos Santos.
|
||
* Galician translation by Miguel Anxo Bouzada.
|
||
20090721 * Indonesian translation by Kemas.
|
||
20090205 * Japanese translation by Satoru Matsumoto.
|
||
* Slovak translation by CyberBoBaK.
|
||
20090203 * Norwegian translation by Preben Olav Pedersen.
|
||
20090202 * Korean translation by xissy.
|
||
* Fix for unicode filenames by xissy.
|
||
* Relies on 127.0.0.1 instead of "localhost" hostname.
|
||
20090129 * Serbian translation by kotnik.
|
||
20090125 * Danish translation by jan.
|
||
20081210 * Greek translation by n2j3.
|
||
20081128 * Slovene translation by david.
|
||
* Romanian translation by Licaon.
|
||
20081022 * Swedish translation by David Eurenius.
|
||
20081001 * Droopy gets pretty (css and html rework).
|
||
* Finnish translation by ipppe.
|
||
20080926 * Configuration saving and loading.
|
||
20080906 * Extract the file base name (some browsers send the full path).
|
||
20080905 * File is uploaded directly into the specified directory.
|
||
20080904 * Arabic translation by Djalel Chefrour.
|
||
* Italian translation by fabius and d1s4st3r.
|
||
* Dutch translation by Tonio Voerman.
|
||
* Portuguese translation by Pedro Palma.
|
||
* Turkish translation by Heartsmagic.
|
||
20080727 * Spanish translation by Federico Kereki.
|
||
20080624 * Option -d or --directory to specify the upload directory.
|
||
20080622 * File numbering to avoid overwriting.
|
||
20080620 * Czech translation by Jiří.
|
||
* German translation by Michael.
|
||
20080408 * First release.
|
||
"""
|
||
from __future__ import print_function
|
||
import sys
|
||
if sys.version_info >= (3, 0):
|
||
from http import server as httpserver
|
||
import socketserver
|
||
from urllib import parse as urllibparse
|
||
unicode = str
|
||
else:
|
||
import BaseHTTPServer as httpserver
|
||
import SocketServer as socketserver
|
||
import urllib as urllibparse
|
||
|
||
import cgi
|
||
import os
|
||
import posixpath
|
||
import ntpath
|
||
import argparse
|
||
import mimetypes
|
||
import shutil
|
||
import tempfile
|
||
import socket
|
||
import base64
|
||
import functools
|
||
import datetime
|
||
|
||
def _decode_str_if_py2(inputstr, encoding='utf-8'):
|
||
"Will return decoded with given encoding *if* input is a string and it's Py2."
|
||
if sys.version_info < (3,) and isinstance(inputstr, str):
|
||
return inputstr.decode(encoding)
|
||
else:
|
||
return inputstr
|
||
|
||
def _encode_str_if_py2(inputstr, encoding='utf-8'):
|
||
"Will return encoded with given encoding *if* input is a string and it's Py2"
|
||
if sys.version_info < (3,) and isinstance(inputstr, str):
|
||
return inputstr.encode(encoding)
|
||
else:
|
||
return inputstr
|
||
|
||
def fullpath(path):
|
||
"Shortcut for os.path abspath(expanduser())"
|
||
return os.path.abspath(os.path.expanduser(path))
|
||
|
||
def basename(path):
|
||
"Extract the file base name (some browsers send the full file path)."
|
||
for mod in posixpath, ntpath:
|
||
path = mod.basename(path)
|
||
return path
|
||
|
||
def check_auth(method):
|
||
"Wraps methods on the request handler to require simple auth checks."
|
||
def decorated(self, *pargs):
|
||
"Reject if auth fails."
|
||
if self.auth:
|
||
# TODO: Between minor versions this handles str/bytes differently
|
||
received = self.get_case_insensitive_header('Authorization', None)
|
||
expected = 'Basic ' + base64.b64encode(self.auth.encode("utf-8")).decode("utf-8")
|
||
# TODO: Timing attack?
|
||
if received != expected:
|
||
self.send_response(401)
|
||
self.send_header('WWW-Authenticate', 'Basic realm=\"Droopy\"')
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
else:
|
||
method(self, *pargs)
|
||
else:
|
||
method(self, *pargs)
|
||
functools.update_wrapper(decorated, method)
|
||
return decorated
|
||
|
||
|
||
class Abort(Exception):
|
||
"Used by handle to rethrow exceptions in ThreadedHTTPServer."
|
||
|
||
|
||
class DroopyFieldStorage(cgi.FieldStorage):
|
||
"""
|
||
The file is created in the destination directory and its name is
|
||
stored in the tmpfilename attribute.
|
||
|
||
Adds a keyword-argument "directory", which is where files are to be
|
||
stored. Because of CGI magic this might not be thread-safe.
|
||
"""
|
||
|
||
TMPPREFIX = 'tmpdroopy'
|
||
|
||
# Would love to do a **kwargs job here but cgi has some recursive
|
||
# magic that passes all possible arguments positionally..
|
||
def __init__(self, fp=None, headers=None, outerboundary=b'',
|
||
environ=os.environ, keep_blank_values=0, strict_parsing=0,
|
||
limit=None, encoding='utf-8', errors='replace',
|
||
max_num_fields=None, separator='&', directory='.'):
|
||
"""
|
||
Adds 'directory' argument to FieldStorage.__init__.
|
||
Retains compatibility with FieldStorage.__init__ (which involves magic)
|
||
"""
|
||
self.directory = directory
|
||
# Not only is cgi.FieldStorage full of magic, it's DIFFERENT
|
||
# magic in Py2/Py3. Here's a case of the core library making
|
||
# life difficult, in a class that's *supposed to be subclassed*!
|
||
if sys.version_info >= (3, 9):
|
||
cgi.FieldStorage.__init__(self, fp, headers, outerboundary,
|
||
environ, keep_blank_values,
|
||
strict_parsing, limit, encoding, errors,
|
||
max_num_fields, separator)
|
||
else:
|
||
cgi.FieldStorage.__init__(self, fp, headers, outerboundary,
|
||
environ, keep_blank_values,
|
||
strict_parsing, limit, encoding, errors)
|
||
|
||
# Binary is passed in Py2 but not Py3.
|
||
def make_file(self, binary=None):
|
||
"Overrides builtin method to store tempfile in the set directory."
|
||
fd, name = tempfile.mkstemp(dir=self.directory, prefix=self.TMPPREFIX)
|
||
# Pylint doesn't like these if they're not declared in __init__ first,
|
||
# but setting tmpfile there leads to odd errors where it's never re-set
|
||
# to a file descriptor.
|
||
self.tmpfile = os.fdopen(fd, 'w+b')
|
||
self.tmpfilename = name
|
||
return self.tmpfile
|
||
|
||
|
||
class HTTPUploadHandler(httpserver.BaseHTTPRequestHandler):
|
||
"The guts of Droopy-a custom handler that accepts files & serves templates"
|
||
|
||
@property
|
||
def templates(self):
|
||
"Ensure provided."
|
||
raise NotImplementedError("Must set class with a templates dict!")
|
||
|
||
@property
|
||
def localisations(self):
|
||
"Ensure provided."
|
||
raise NotImplementedError("Must set class with a localisations dict!")
|
||
|
||
@property
|
||
def directory(self):
|
||
"Ensure provided."
|
||
raise NotImplementedError("Must provide directory to host.")
|
||
|
||
message = ''
|
||
picture = ''
|
||
publish_files = False
|
||
file_mode = None
|
||
protocol_version = 'HTTP/1.0'
|
||
form_field = 'upfile'
|
||
auth = ''
|
||
certfile = None
|
||
divpicture = '<div class="box"><img src="/__droopy/picture"/></div>'
|
||
|
||
def get_case_insensitive_header(self, hdrname, default):
|
||
"Python 2 and 3 differ in header capitalisation!"
|
||
lc_hdrname = hdrname.lower()
|
||
lc_headers = dict((h.lower(), h) for h in self.headers.keys())
|
||
if lc_hdrname in lc_headers:
|
||
return self.headers[lc_headers[lc_hdrname]]
|
||
else:
|
||
return default
|
||
|
||
@staticmethod
|
||
def prefcode_tuple(prefcode):
|
||
"Parse language preferences into (preference, language) tuples."
|
||
prefbits = prefcode.split(";q=")
|
||
if len(prefbits) == 1:
|
||
return (1, prefbits[0])
|
||
else:
|
||
return (float(prefbits[1]), prefbits[0])
|
||
|
||
def parse_accepted_languages(self):
|
||
"Parse accept-language header"
|
||
lhdr = self.get_case_insensitive_header('accept-language', default='')
|
||
if lhdr:
|
||
accepted = [self.prefcode_tuple(lang) for lang in lhdr.split(',')]
|
||
accepted.sort()
|
||
accepted.reverse()
|
||
return [x[1] for x in accepted]
|
||
else:
|
||
return []
|
||
|
||
def choose_language(self):
|
||
"Choose localisation based on accept-language header (default 'en')"
|
||
accepted = self.parse_accepted_languages()
|
||
# -- Choose the appropriate translation dictionary (default is english)
|
||
lang = "en"
|
||
for alang in accepted:
|
||
if alang in self.localisations:
|
||
lang = alang
|
||
break
|
||
return self.localisations[lang]
|
||
|
||
def html(self, page):
|
||
"""
|
||
page can be "main", "success", or "error"
|
||
returns an html page (in the appropriate language) as a string
|
||
"""
|
||
dico = {}
|
||
dico.update(self.choose_language())
|
||
# -- Set message and picture
|
||
if self.message:
|
||
dico['message'] = '<div id="message">{0}</div>'.format(self.message)
|
||
else:
|
||
dico["message"] = ''
|
||
# The default appears to be missing/broken, so needs a bit of love anyway.
|
||
if self.picture:
|
||
dico["divpicture"] = self.divpicture
|
||
else:
|
||
dico["divpicture"] = ''
|
||
# -- Possibly provide download links
|
||
# TODO: Sanity-check for injections
|
||
links = ''
|
||
if self.publish_files:
|
||
for name in self.published_files():
|
||
encoded_name = urllibparse.quote(_encode_str_if_py2(name))
|
||
|
||
# JEFFREY PATCH: Ajout de la datetime d'upload dans le nom affiché
|
||
stat_info = os.stat(name)
|
||
timestamp = getattr(stat_info, 'st_birthtime', stat_info.st_ctime)
|
||
dt_object = datetime.datetime.fromtimestamp(timestamp)
|
||
mytime = dt_object.strftime("%d/%m/%Y at %H:%M:%S")
|
||
name = f'{name} (uploaded: {mytime})'
|
||
links += '<a href="/{0}">{1}</a>'.format(encoded_name, name)
|
||
links = '<div id="files">' + links + '</div>'
|
||
dico["files"] = links
|
||
# -- Add a link to discover the url
|
||
if self.client_address[0] == "127.0.0.1":
|
||
dico["port"] = self.server.server_port
|
||
dico["ssl"] = int(self.certfile is not None)
|
||
dico["linkurl"] = self.templates['linkurl'] % dico
|
||
else:
|
||
dico["linkurl"] = ''
|
||
return self.templates[page] % dico
|
||
|
||
@check_auth
|
||
def do_GET(self):
|
||
"Standard method to override in this Server object."
|
||
name = self.path.lstrip('/')
|
||
name = urllibparse.unquote(name)
|
||
name = _decode_str_if_py2(name, 'utf-8')
|
||
|
||
# TODO: Refactor special-method handling to make more modular?
|
||
# Include ability to self-define "special method" prefix path?
|
||
if self.picture != None and self.path == '/__droopy/picture':
|
||
# send the picture
|
||
self.send_file(self.picture)
|
||
# TODO Verify that this is path-injection proof
|
||
elif name in self.published_files():
|
||
localpath = os.path.join(self.directory, name)
|
||
self.send_file(localpath)
|
||
else:
|
||
self.send_html(self.html("main"))
|
||
|
||
@check_auth
|
||
def do_POST(self):
|
||
"Standard method to override in this Server object."
|
||
try:
|
||
self.log_message("Started file transfer")
|
||
# -- Save file (numbered to avoid overwriting, ex: foo-3.png)
|
||
form = DroopyFieldStorage(fp=self.rfile,
|
||
directory=self.directory,
|
||
headers=self.headers,
|
||
environ={'REQUEST_METHOD': self.command})
|
||
file_items = form[self.form_field]
|
||
#-- Handle multiple file upload
|
||
if not isinstance(file_items, list):
|
||
file_items = [file_items]
|
||
for item in file_items:
|
||
filename = _decode_str_if_py2(basename(item.filename), "utf-8")
|
||
if filename == "":
|
||
continue
|
||
localpath = _encode_str_if_py2(os.path.join(self.directory, filename), "utf-8")
|
||
root, ext = os.path.splitext(localpath)
|
||
|
||
# JEFFREY PATCH: forcer l'écrasement d'un fichier existant.
|
||
if os.path.exists(localpath):
|
||
os.remove(localpath)
|
||
|
||
if hasattr(item, 'tmpfile'):
|
||
# DroopyFieldStorage.make_file() has been called
|
||
item.tmpfile.close()
|
||
shutil.move(item.tmpfilename, localpath)
|
||
else:
|
||
# no temporary file, self.file is a StringIO()
|
||
# see cgi.FieldStorage.read_lines()
|
||
with open(localpath, "wb") as fout:
|
||
shutil.copyfileobj(item.file, fout)
|
||
if self.file_mode is not None:
|
||
os.chmod(localpath, self.file_mode)
|
||
self.log_message("Received: %s", os.path.basename(localpath))
|
||
|
||
# -- Reply
|
||
if self.publish_files:
|
||
# The file list gives a feedback for the upload success
|
||
self.send_resp_headers(301, {'Location': '/'}, end=True)
|
||
else:
|
||
self.send_html(self.html("success"))
|
||
|
||
except Exception as e:
|
||
self.log_message(repr(e))
|
||
self.send_html(self.html("error"))
|
||
# raise e # Dev only
|
||
|
||
def send_resp_headers(self, response_code, headers_dict, end=False):
|
||
"Just a shortcut for a common operation."
|
||
self.send_response(response_code)
|
||
for k, v in headers_dict.items():
|
||
self.send_header(k, v)
|
||
if end:
|
||
self.end_headers()
|
||
|
||
def send_html(self, htmlstr):
|
||
"Simply returns htmlstr with the appropriate content-type/status."
|
||
self.send_resp_headers(200, {'Content-type': 'text/html; charset=utf-8'}, end=True)
|
||
self.wfile.write(htmlstr.encode("utf-8"))
|
||
|
||
def send_file(self, localpath):
|
||
"Does what it says on the tin! Includes correct content-type/length."
|
||
with open(localpath, 'rb') as f:
|
||
self.send_resp_headers(200,
|
||
{'Content-length': os.fstat(f.fileno())[6],
|
||
'Content-type': mimetypes.guess_type(localpath)[0]},
|
||
end=True)
|
||
shutil.copyfileobj(f, self.wfile)
|
||
|
||
def published_files(self):
|
||
"Returns the list of files that should appear as download links."
|
||
names = []
|
||
# In py2, listdir() returns strings when the directory is a string.
|
||
for name in os.listdir(unicode(self.directory)):
|
||
if name.startswith(DroopyFieldStorage.TMPPREFIX):
|
||
continue
|
||
npath = os.path.join(self.directory, name)
|
||
if os.path.isfile(npath):
|
||
names.append(name)
|
||
names.sort(key=lambda s: s.lower())
|
||
return names
|
||
|
||
def handle(self):
|
||
"Lets parent object handle, but redirects socket exceptions as 'Abort's."
|
||
try:
|
||
httpserver.BaseHTTPRequestHandler.handle(self)
|
||
except socket.error as e:
|
||
self.log_message(str(e))
|
||
raise Abort(str(e))
|
||
|
||
|
||
class ThreadedHTTPServer(socketserver.ThreadingMixIn,
|
||
httpserver.HTTPServer):
|
||
"Allows propagation of socket.error in HTTPUploadHandler.handle"
|
||
def handle_error(self, request, client_address):
|
||
"Override socketserver.handle_error"
|
||
exctype = sys.exc_info()[0]
|
||
if not exctype is Abort:
|
||
httpserver.HTTPServer.handle_error(self, request, client_address)
|
||
|
||
|
||
def run(hostname='',
|
||
port=80,
|
||
templates=None,
|
||
localisations=None,
|
||
directory='.',
|
||
timeout=3*60,
|
||
picture=None,
|
||
message='',
|
||
file_mode=None,
|
||
publish_files=False,
|
||
auth='',
|
||
certfile=None,
|
||
permitted_ciphers=(
|
||
'ECDH+AESGCM:ECDH+AES256:ECDH+AES128:ECDH+3DES'
|
||
':RSA+AESGCM:RSA+AES:RSA+3DES'
|
||
':!aNULL:!MD5:!DSS')):
|
||
"""
|
||
certfile should be the path of a PEM TLS certificate.
|
||
|
||
permitted_ciphers, if a TLS cert is provided, is an OpenSSL cipher string.
|
||
The default here is taken from:
|
||
https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
|
||
..with DH-only ciphers removed because of precomputation hazard.
|
||
"""
|
||
if templates is None or localisations is None:
|
||
raise ValueError("Must provide templates *and* localisations.")
|
||
socket.setdefaulttimeout(timeout)
|
||
HTTPUploadHandler.templates = templates
|
||
HTTPUploadHandler.directory = directory
|
||
HTTPUploadHandler.localisations = localisations
|
||
HTTPUploadHandler.certfile = certfile
|
||
HTTPUploadHandler.publish_files = publish_files
|
||
HTTPUploadHandler.picture = picture
|
||
HTTPUploadHandler.message = message
|
||
HTTPUploadHandler.file_mode = file_mode
|
||
HTTPUploadHandler.auth = auth
|
||
httpd = ThreadedHTTPServer((hostname, port), HTTPUploadHandler)
|
||
# TODO: Specify TLS1.2 only?
|
||
if certfile:
|
||
try:
|
||
import ssl
|
||
except:
|
||
print("Error: Could not import module 'ssl', exiting.")
|
||
sys.exit(2)
|
||
httpd.socket = ssl.wrap_socket(
|
||
httpd.socket,
|
||
certfile=certfile,
|
||
ciphers=permitted_ciphers,
|
||
server_side=True)
|
||
httpd.serve_forever()
|
||
|
||
# -- Dato
|
||
|
||
# -- HTML templates
|
||
|
||
style = '''
|
||
<meta name="viewport"
|
||
content="width=device-width,initial-scale=1,maximum-scale=1" />
|
||
<style type="text/css">
|
||
<!--
|
||
* {margin: 0; padding: 0;}
|
||
body {text-align: center; background-color: #eee; font-family: sans-serif;
|
||
color:#777;}
|
||
div {word-wrap: break-word;}
|
||
img {max-width: 100%%;}
|
||
a {color: #4499cc; text-decoration: none;}
|
||
.container {max-width: 700px; margin: auto; background-color: #fff;}
|
||
.box {padding-top: 20px; padding-bottom: 20px;}
|
||
#linkurl {background-color: #333;}
|
||
#linkurl a {color: #ddd; text-decoration: none;}
|
||
#linkurl a:hover {color: #fff;}
|
||
#message {padding: 5px 0; font-size: 2em; font-weight: lighter;
|
||
letter-spacing: -2px; line-height: 50px; color: #aaa;}
|
||
#sending {display: none; font-style: italic;}
|
||
#sending .text {padding-top: 10px; color: #bbb; font-size: 0.8em;}
|
||
#wrapform {height: 90px; padding-top:40px;}
|
||
#progress {display: inline; border-collapse: separate; empty-cells: show;
|
||
border-spacing: 24px 0; padding: 0; vertical-align: bottom;}
|
||
#progress td {height: 17px; width: 17px; background-color: #eee;
|
||
padding: 0px; border-radius: 90px; box-shadow: 0 0 3px #bbb;}
|
||
#userinfo {padding-bottom: 20px;}
|
||
#files {
|
||
margin: auto;
|
||
padding: 13px 0;
|
||
text-align: left;
|
||
overflow: auto;
|
||
margin-bottom: 20px;
|
||
}
|
||
#files a {text-decoration: none; display: block; padding: 10px 20px;}
|
||
#files a:nth-child(2n+1) {background-color: #F7F7F7;}
|
||
#files a:link {color: #4499cc}
|
||
#files a:visited {color: #a0c0e0}
|
||
#files a:hover {background-color:#f0f0f0}
|
||
--></style>'''
|
||
|
||
userinfo = '''
|
||
<div id="userinfo">
|
||
%(message)s
|
||
%(divpicture)s
|
||
</div>
|
||
'''
|
||
|
||
maintmpl = '''
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<title>%(maintitle)s</title>
|
||
''' + style + '''
|
||
<script language="JavaScript">
|
||
function swap() {
|
||
document.getElementById("form").style.display = "none";
|
||
document.getElementById("sending").style.display = "block";
|
||
pulse(0);
|
||
}
|
||
|
||
function pulse(i) {
|
||
var NUMCELL = 5;
|
||
var cell = document.getElementById("cell-" + (i %% NUMCELL));
|
||
var prev = document.getElementById("cell-"+((i - 1 + NUMCELL) %% NUMCELL));
|
||
cell.style.backgroundColor = "#7ac";
|
||
prev.style.backgroundColor = "#eee";
|
||
setTimeout(function() {pulse(i+1);}, 300);
|
||
}
|
||
|
||
function onunload() {
|
||
document.getElementById("form").style.display = "block";
|
||
document.getElementById("sending").style.display = "none";
|
||
}
|
||
</script></head>
|
||
<body>
|
||
%(linkurl)s
|
||
<div class="container">
|
||
<div id="wrapform">
|
||
<div id="form" class="box">
|
||
<form method="post" enctype="multipart/form-data" action="">
|
||
<input name="upfile" type="file" multiple="yes">
|
||
<input value="%(submit)s" onclick="swap()" type="submit">
|
||
</form>
|
||
</div>
|
||
<div id="sending" class="box">
|
||
<table id="progress">
|
||
<tr>
|
||
<td id="cell-0"/><td id="cell-1"/><td id="cell-2"/>
|
||
<td id="cell-3"/><td id="cell-4"/>
|
||
</tr>
|
||
</table>
|
||
<div class="text">%(sending)s</div>
|
||
</div>
|
||
</div>
|
||
''' + userinfo + '''
|
||
%(files)s
|
||
</div>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
successtmpl = '''
|
||
<!doctype html>
|
||
<html>
|
||
<head><title> %(successtitle)s </title>
|
||
''' + style + '''
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div id="wrapform">
|
||
<div class="box">
|
||
%(received)s
|
||
<a href="/"> %(another)s </a>
|
||
</div>
|
||
</div>
|
||
''' + userinfo + '''
|
||
</div>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
errortmpl = '''
|
||
<!doctype html>
|
||
<html>
|
||
<head><title> %(errortitle)s </title>
|
||
''' + style + '''
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div id="wrapform">
|
||
<div class="box">
|
||
%(problem)s
|
||
<a href="/"> %(retry)s </a>
|
||
</div>
|
||
</div>
|
||
''' + userinfo + '''
|
||
</div>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
linkurltmpl = '''<div id="linkurl" class="box">
|
||
<a href="http://stackp.online.fr/droopy-ip.php?port=%(port)d&ssl=%(ssl)d"> %(discover)s
|
||
</a></div>'''
|
||
|
||
|
||
default_templates = {
|
||
"main": maintmpl,
|
||
"success": successtmpl,
|
||
"error": errortmpl,
|
||
"linkurl": linkurltmpl}
|
||
|
||
# -- Translations
|
||
default_localisations = {
|
||
'ar' : {
|
||
"maintitle": u"إرسال ملف",
|
||
"submit": u"إرسال",
|
||
"sending": u"الملف قيد الإرسال",
|
||
"successtitle": u"تم استقبال الملف",
|
||
"received": u"تم استقبال الملف !",
|
||
"another": u"إرسال ملف آخر",
|
||
"errortitle": u"مشكلة",
|
||
"problem": u"حدثت مشكلة !",
|
||
"retry": u"إعادة المحاولة",
|
||
"discover": u"اكتشاف عنوان هذه الصفحة"},
|
||
'cs' : {
|
||
"maintitle": u"Poslat soubor",
|
||
"submit": u"Poslat",
|
||
"sending": u"Posílám",
|
||
"successtitle": u"Soubor doručen",
|
||
"received": u"Soubor doručen !",
|
||
"another": u"Poslat další soubor",
|
||
"errortitle": u"Chyba",
|
||
"problem": u"Stala se chyba !",
|
||
"retry": u"Zkusit znova.",
|
||
"discover": u"Zjistit adresu stránky"},
|
||
'da' : {
|
||
"maintitle": u"Send en fil",
|
||
"submit": u"Send",
|
||
"sending": u"Sender",
|
||
"successtitle": u"Fil modtaget",
|
||
"received": u"Fil modtaget!",
|
||
"another": u"Send en fil til.",
|
||
"errortitle": u"Problem",
|
||
"problem": u"Det er opstået en fejl!",
|
||
"retry": u"Forsøg igen.",
|
||
"discover": u"Find adressen til denne side"},
|
||
'de' : {
|
||
"maintitle": "Datei senden",
|
||
"submit": "Senden",
|
||
"sending": "Sendet",
|
||
"successtitle": "Datei empfangen",
|
||
"received": "Datei empfangen!",
|
||
"another": "Weitere Datei senden",
|
||
"errortitle": "Fehler",
|
||
"problem": "Ein Fehler ist aufgetreten!",
|
||
"retry": "Wiederholen",
|
||
"discover": "Internet-Adresse dieser Seite feststellen"},
|
||
'el' : {
|
||
"maintitle": u"Στείλε ένα αρχείο",
|
||
"submit": u"Αποστολή",
|
||
"sending": u"Αποστέλλεται...",
|
||
"successtitle": u"Επιτυχής λήψη αρχείου ",
|
||
"received": u"Λήψη αρχείου ολοκληρώθηκε",
|
||
"another": u"Στείλε άλλο ένα αρχείο",
|
||
"errortitle": u"Σφάλμα",
|
||
"problem": u"Παρουσιάστηκε σφάλμα",
|
||
"retry": u"Επανάληψη",
|
||
"discover": u"Βρες την διεύθυνση της σελίδας"},
|
||
'en' : {
|
||
"maintitle": "Send a file",
|
||
"submit": "Send",
|
||
"sending": "Sending",
|
||
"successtitle": "File received",
|
||
"received": "File received!",
|
||
"another": "Send another file.",
|
||
"errortitle": "Problem",
|
||
"problem": "There has been a problem!",
|
||
"retry": "Retry.",
|
||
"discover": "Discover the address of this page"},
|
||
'es' : {
|
||
"maintitle": u"Enviar un archivo",
|
||
"submit": u"Enviar",
|
||
"sending": u"Enviando",
|
||
"successtitle": u"Archivo recibido",
|
||
"received": u"¡Archivo recibido!",
|
||
"another": u"Enviar otro archivo.",
|
||
"errortitle": u"Error",
|
||
"problem": u"¡Hubo un problema!",
|
||
"retry": u"Reintentar",
|
||
"discover": u"Descubrir la dirección de esta página"},
|
||
'fi' : {
|
||
"maintitle": u"Lähetä tiedosto",
|
||
"submit": u"Lähetä",
|
||
"sending": u"Lähettää",
|
||
"successtitle": u"Tiedosto vastaanotettu",
|
||
"received": u"Tiedosto vastaanotettu!",
|
||
"another": u"Lähetä toinen tiedosto.",
|
||
"errortitle": u"Virhe",
|
||
"problem": u"Virhe lahetettäessä tiedostoa!",
|
||
"retry": u"Uudelleen.",
|
||
"discover": u"Näytä tämän sivun osoite"},
|
||
'fr' : {
|
||
"maintitle": u"Envoyer un fichier",
|
||
"submit": u"Envoyer",
|
||
"sending": u"Envoi en cours",
|
||
"successtitle": u"Fichier reçu",
|
||
"received": u"Fichier reçu !",
|
||
"another": u"Envoyer un autre fichier.",
|
||
"errortitle": u"Problème",
|
||
"problem": u"Il y a eu un problème !",
|
||
"retry": u"Réessayer.",
|
||
"discover": u"Découvrir l'adresse de cette page"},
|
||
'gl' : {
|
||
"maintitle": u"Enviar un ficheiro",
|
||
"submit": u"Enviar",
|
||
"sending": u"Enviando",
|
||
"successtitle": u"Ficheiro recibido",
|
||
"received": u"Ficheiro recibido!",
|
||
"another": u"Enviar outro ficheiro.",
|
||
"errortitle": u"Erro",
|
||
"problem": u"Xurdíu un problema!",
|
||
"retry": u"Reintentar",
|
||
"discover": u"Descubrir o enderezo desta páxina"},
|
||
'hu' : {
|
||
"maintitle": u"Állomány küldése",
|
||
"submit": u"Küldés",
|
||
"sending": u"Küldés folyamatban",
|
||
"successtitle": u"Az állomány beérkezett",
|
||
"received": u"Az állomány beérkezett!",
|
||
"another": u"További állományok küldése",
|
||
"errortitle": u"Hiba",
|
||
"problem": u"Egy hiba lépett fel!",
|
||
"retry": u"Megismételni",
|
||
"discover": u"Az oldal Internet-címének megállapítása"},
|
||
'id' : {
|
||
"maintitle": "Kirim sebuah berkas",
|
||
"submit": "Kirim",
|
||
"sending": "Mengirim",
|
||
"successtitle": "Berkas diterima",
|
||
"received": "Berkas diterima!",
|
||
"another": "Kirim berkas yang lain.",
|
||
"errortitle": "Permasalahan",
|
||
"problem": "Telah ditemukan sebuah kesalahan!",
|
||
"retry": "Coba kembali.",
|
||
"discover": "Kenali alamat IP dari halaman ini"},
|
||
'it' : {
|
||
"maintitle": u"Invia un file",
|
||
"submit": u"Invia",
|
||
"sending": u"Invio in corso",
|
||
"successtitle": u"File ricevuto",
|
||
"received": u"File ricevuto!",
|
||
"another": u"Invia un altro file.",
|
||
"errortitle": u"Errore",
|
||
"problem": u"Si è verificato un errore!",
|
||
"retry": u"Riprova.",
|
||
"discover": u"Scopri l’indirizzo di questa pagina"},
|
||
'ja' : {
|
||
"maintitle": u"ファイル送信",
|
||
"submit": u"送信",
|
||
"sending": u"送信中",
|
||
"successtitle": u"受信完了",
|
||
"received": u"ファイルを受信しました!",
|
||
"another": u"他のファイルを送信する",
|
||
"errortitle": u"問題発生",
|
||
"problem": u"問題が発生しました!",
|
||
"retry": u"リトライ",
|
||
"discover": u"このページのアドレスを確認する"},
|
||
'ko' : {
|
||
"maintitle": u"파일 보내기",
|
||
"submit": u"보내기",
|
||
"sending": u"보내는 중",
|
||
"successtitle": u"파일이 받아졌습니다",
|
||
"received": u"파일이 받아졌습니다!",
|
||
"another": u"다른 파일 보내기",
|
||
"errortitle": u"문제가 발생했습니다",
|
||
"problem": u"문제가 발생했습니다!",
|
||
"retry": u"다시 시도",
|
||
"discover": u"이 페이지 주소 알아보기"},
|
||
'nl' : {
|
||
"maintitle": "Verstuur een bestand",
|
||
"submit": "Verstuur",
|
||
"sending": "Bezig met versturen",
|
||
"successtitle": "Bestand ontvangen",
|
||
"received": "Bestand ontvangen!",
|
||
"another": "Verstuur nog een bestand.",
|
||
"errortitle": "Fout",
|
||
"problem": "Er is een fout opgetreden!",
|
||
"retry": "Nog eens.",
|
||
"discover": "Vind het adres van deze pagina"},
|
||
'no' : {
|
||
"maintitle": u"Send en fil",
|
||
"submit": u"Send",
|
||
"sending": u"Sender",
|
||
"successtitle": u"Fil mottatt",
|
||
"received": u"Fil mottatt !",
|
||
"another": u"Send en ny fil.",
|
||
"errortitle": u"Feil",
|
||
"problem": u"Det har skjedd en feil !",
|
||
"retry": u"Send på nytt.",
|
||
"discover": u"Finn addressen til denne siden"},
|
||
'pl' : {
|
||
"maintitle": u"Wyślij plik",
|
||
"submit": u"Wyślij",
|
||
"sending": u"Wysyłanie",
|
||
"successtitle": u"Plik wysłany",
|
||
"received": u"Plik wysłany!",
|
||
"another": u"Wyślij kolejny plik.",
|
||
"errortitle": u"Problem",
|
||
"problem": u"Wystąpił błąd!",
|
||
"retry": u"Spróbuj ponownie.",
|
||
"discover": u"Znajdź adres tej strony"},
|
||
'pt' : {
|
||
"maintitle": u"Enviar um ficheiro",
|
||
"submit": u"Enviar",
|
||
"sending": u"A enviar",
|
||
"successtitle": u"Ficheiro recebido",
|
||
"received": u"Ficheiro recebido !",
|
||
"another": u"Enviar outro ficheiro.",
|
||
"errortitle": u"Erro",
|
||
"problem": u"Ocorreu um erro !",
|
||
"retry": u"Tentar novamente.",
|
||
"discover": u"Descobrir o endereço desta página"},
|
||
'pt-br' : {
|
||
"maintitle": u"Enviar um arquivo",
|
||
"submit": u"Enviar",
|
||
"sending": u"Enviando",
|
||
"successtitle": u"Arquivo recebido",
|
||
"received": u"Arquivo recebido!",
|
||
"another": u"Enviar outro arquivo.",
|
||
"errortitle": u"Erro",
|
||
"problem": u"Ocorreu um erro!",
|
||
"retry": u"Tentar novamente.",
|
||
"discover": u"Descobrir o endereço desta página"},
|
||
'ro' : {
|
||
"maintitle": u"Trimite un fişier",
|
||
"submit": u"Trimite",
|
||
"sending": u"Se trimite",
|
||
"successtitle": u"Fişier recepţionat",
|
||
"received": u"Fişier recepţionat !",
|
||
"another": u"Trimite un alt fişier.",
|
||
"errortitle": u"Problemă",
|
||
"problem": u"A intervenit o problemă !",
|
||
"retry": u"Reîncearcă.",
|
||
"discover": u"Descoperă adresa acestei pagini"},
|
||
'ru' : {
|
||
"maintitle": u"Отправить файл",
|
||
"submit": u"Отправить",
|
||
"sending": u"Отправляю",
|
||
"successtitle": u"Файл получен",
|
||
"received": u"Файл получен !",
|
||
"another": u"Отправить другой файл.",
|
||
"errortitle": u"Ошибка",
|
||
"problem": u"Произошла ошибка !",
|
||
"retry": u"Повторить.",
|
||
"discover": u"Посмотреть адрес этой страницы"},
|
||
'sk' : {
|
||
"maintitle": u"Pošli súbor",
|
||
"submit": u"Pošli",
|
||
"sending": u"Posielam",
|
||
"successtitle": u"Súbor prijatý",
|
||
"received": u"Súbor prijatý !",
|
||
"another": u"Poslať ďalší súbor.",
|
||
"errortitle": u"Chyba",
|
||
"problem": u"Vyskytla sa chyba!",
|
||
"retry": u"Skúsiť znova.",
|
||
"discover": u"Zisti adresu tejto stránky"},
|
||
'sl' : {
|
||
"maintitle": u"Pošlji datoteko",
|
||
"submit": u"Pošlji",
|
||
"sending": u"Pošiljam",
|
||
"successtitle": u"Datoteka prejeta",
|
||
"received": u"Datoteka prejeta !",
|
||
"another": u"Pošlji novo datoteko.",
|
||
"errortitle": u"Napaka",
|
||
"problem": u"Prišlo je do napake !",
|
||
"retry": u"Poizkusi ponovno.",
|
||
"discover": u"Poišči naslov na tej strani"},
|
||
'sr' : {
|
||
"maintitle": u"Pošalji fajl",
|
||
"submit": u"Pošalji",
|
||
"sending": u"Šaljem",
|
||
"successtitle": u"Fajl primljen",
|
||
"received": u"Fajl primljen !",
|
||
"another": u"Pošalji još jedan fajl.",
|
||
"errortitle": u"Problem",
|
||
"problem": u"Desio se problem !",
|
||
"retry": u"Pokušaj ponovo.",
|
||
"discover": u"Otkrij adresu ove stranice"},
|
||
'sv' : {
|
||
"maintitle": u"Skicka en fil",
|
||
"submit": u"Skicka",
|
||
"sending": u"Skickar...",
|
||
"successtitle": u"Fil mottagen",
|
||
"received": u"Fil mottagen !",
|
||
"another": u"Skicka en fil till.",
|
||
"errortitle": u"Fel",
|
||
"problem": u"Det har uppstått ett fel !",
|
||
"retry": u"Försök igen.",
|
||
"discover": u"Ta reda på adressen till denna sida"},
|
||
'tr' : {
|
||
"maintitle": u"Dosya gönder",
|
||
"submit": u"Gönder",
|
||
"sending": u"Gönderiliyor...",
|
||
"successtitle": u"Gönderildi",
|
||
"received": u"Gönderildi",
|
||
"another": u"Başka bir dosya gönder.",
|
||
"errortitle": u"Problem.",
|
||
"problem": u"Bir problem oldu !",
|
||
"retry": u"Yeniden dene.",
|
||
"discover": u"Bu sayfanın adresini bul"},
|
||
'zh-cn' : {
|
||
"maintitle": u"发送文件",
|
||
"submit": u"发送",
|
||
"sending": u"发送中",
|
||
"successtitle": u"文件已收到",
|
||
"received": u"文件已收到!",
|
||
"another": u"发送另一个文件。",
|
||
"errortitle": u"问题",
|
||
"problem": u"出现问题!",
|
||
"retry": u"重试。",
|
||
"discover": u"查看本页面的地址"},
|
||
'zh-tw' : {
|
||
"maintitle": u"上傳檔案",
|
||
"submit": u"上傳",
|
||
"sending": u"傳送中...",
|
||
"successtitle": u"已收到檔案",
|
||
"received": u"已收到檔案!",
|
||
"another": u"上傳另一個檔案。",
|
||
"errortitle": u"錯誤",
|
||
"problem": u"出現錯誤!",
|
||
"retry": u"重試。",
|
||
"discover": u"查閱本網頁的網址"}
|
||
} # Ends default_localisations dictionary.
|
||
|
||
|
||
# -- Options
|
||
|
||
def default_configfile():
|
||
"Returns appropriate absolute path to configfile, per-platform."
|
||
appname = 'droopy'
|
||
if os.name == 'posix':
|
||
# JEFFREY PATCH: celle ligne ne fonctionne pas si droopy est lancé en tant que service via systemctl, donc on force un path valide, /tmp/
|
||
#filename = os.path.join(os.environ['HOME'], "." + appname)
|
||
filename = "/tmp/." + appname)
|
||
elif os.name == 'mac':
|
||
filename = os.path.join(os.environ['HOME'], 'Library', 'Application Support', appname)
|
||
elif os.name == 'nt':
|
||
filename = os.path.join(os.environ['APPDATA'], appname)
|
||
else:
|
||
# Exaggerated shrug
|
||
filename = './' + appname
|
||
return filename
|
||
|
||
|
||
def save_options(cfg):
|
||
"Dumps sys.argv with one argument per line."
|
||
with open(cfg, "w") as O:
|
||
ignorenext = False
|
||
for opt in sys.argv[1:]:
|
||
if ignorenext:
|
||
ignorenext = False
|
||
continue
|
||
if opt.startswith("-"):
|
||
if opt.strip() in ("--save-config", "--delete-config"):
|
||
continue
|
||
if opt.strip() == '--config-file':
|
||
ignorenext = True
|
||
continue
|
||
O.write("\n")
|
||
else:
|
||
O.write(" ")
|
||
O.write(opt)
|
||
|
||
|
||
def load_options(cfg_loc):
|
||
"""
|
||
Attempts to open location, piece lines back together into a terminal-style
|
||
invocation, and pass to parse_args.
|
||
"""
|
||
try:
|
||
with open(cfg_loc) as f:
|
||
cmd = []
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
if line.startswith("-"):
|
||
if " " in line:
|
||
opt, rest = line.split(" ", 1)
|
||
cmd.extend((opt, rest))
|
||
else:
|
||
cmd.append(line)
|
||
else:
|
||
cmd.append(line)
|
||
return parse_args(cmd)
|
||
except IOError:
|
||
return {}
|
||
|
||
|
||
def parse_args(cmd=None, ignore_defaults=False):
|
||
"Parse terminal-style args list, or sys.argv[1:] if no argument is passed."
|
||
parser = argparse.ArgumentParser(
|
||
description="Usage: droopy [options] [PORT]",
|
||
epilog='Example:\n droopy -m "Hi, this is Bob. You can send me a file." -p avatar.png'
|
||
)
|
||
parser.add_argument("port", type=int, nargs='?', default=8000,
|
||
help='port number to host droopy upon')
|
||
parser.add_argument('-d', '--directory', type=str, default='.',
|
||
help='set the directory to upload files to')
|
||
parser.add_argument('-m', '--message', type=str, default='',
|
||
help='set the message')
|
||
parser.add_argument('-p', '--picture', type=str, default='',
|
||
help='set the picture')
|
||
parser.add_argument('--publish-files', '--dl', action='store_true', default=False,
|
||
help='provide download links')
|
||
parser.add_argument('-a', '--auth', type=str, default='',
|
||
help='set the authentication credentials, in form USER:PASS')
|
||
parser.add_argument('--ssl', type=str, default='',
|
||
help='set up https using the certificate file')
|
||
parser.add_argument('--chmod', type=str, default=None,
|
||
help='set the file permissions (octal value)')
|
||
parser.add_argument('--save-config', action='store_true', default=False,
|
||
help='save options in a configuration file')
|
||
parser.add_argument('--delete-config', action='store_true', default=False,
|
||
help='delete the configuration file and exit')
|
||
parser.add_argument('--config-file', default=default_configfile(),
|
||
help='configuration file to load terminal arguments from.')
|
||
args = parser.parse_args(cmd)
|
||
if args.picture:
|
||
if os.path.exists(args.picture):
|
||
args.picture = fullpath(args.picture)
|
||
else:
|
||
print("Picture not found: '{0}'".format(args.picture))
|
||
if args.delete_config:
|
||
filename = default_configfile()
|
||
os.remove(filename)
|
||
print('Deleted ' + filename)
|
||
sys.exit(0)
|
||
if args.auth:
|
||
if ':' not in args.auth:
|
||
print("Error: authentication credentials must be "
|
||
"specified as USER:PASSWORD")
|
||
sys.exit(1)
|
||
if args.ssl:
|
||
if not os.path.isfile(args.ssl):
|
||
print("PEM file not found: '{0}'".format(args.ssl))
|
||
sys.exit(1)
|
||
args.ssl = fullpath(args.ssl)
|
||
if args.chmod is not None:
|
||
try:
|
||
args.chmod = int(args.chmod, 8)
|
||
except ValueError:
|
||
print("Invalid octal value passed to chmod option: '{0}'".format(args.chmod))
|
||
sys.exit(1)
|
||
# Needs to be set after de-defaulting because CWD varies, obviously. :)
|
||
args.directory = fullpath(args.directory)
|
||
d_args = vars(args)
|
||
if ignore_defaults:
|
||
default_set = parse_args([])
|
||
for k, v in default_set.items():
|
||
if v == d_args[k]:
|
||
del d_args[k]
|
||
return d_args
|
||
|
||
|
||
def main():
|
||
"Encapsulating main prevents scope leakage and pleases linters."
|
||
print('''\
|
||
_____
|
||
| \.----.-----.-----.-----.--.--.
|
||
| -- | _| _ | _ | _ | | |
|
||
|_____/|__| |_____|_____| __|___ |
|
||
|__| |_____|
|
||
''')
|
||
term_args = parse_args(ignore_defaults=True)
|
||
cfg = term_args.get('config_file', default_configfile())
|
||
args = load_options(cfg)
|
||
if args:
|
||
print("Configuration found in {0}".format(cfg))
|
||
args.update(term_args)
|
||
else:
|
||
print("No configuration file found")
|
||
args.update(parse_args(ignore_defaults=False))
|
||
if args['save_config']:
|
||
cfg = args.get('config_file', default_configfile())
|
||
save_options(cfg)
|
||
print("Options saved in {0}".format(cfg))
|
||
print("Files will be uploaded to {0}\n".format(args['directory']))
|
||
proto = 'https' if args['ssl'] else 'http'
|
||
print("HTTP server starting...",
|
||
"Check it out at {0}://localhost:{1}".format(proto, args['port']))
|
||
try:
|
||
run(port=args['port'],
|
||
certfile=args['ssl'],
|
||
picture=args['picture'],
|
||
message=args['message'],
|
||
directory=args['directory'],
|
||
file_mode=args['chmod'],
|
||
publish_files=args['publish_files'],
|
||
auth=args['auth'],
|
||
templates=default_templates,
|
||
localisations=default_localisations)
|
||
except KeyboardInterrupt:
|
||
print('^C received, awaiting termination of remaining server threads..')
|
||
|
||
if __name__ == '__main__':
|
||
main()
|