Commit 2a54030b authored by Xavier Thompson's avatar Xavier Thompson

simplehttpserver: Allow disabling write access

See merge request nexedi/slapos!1685
parents 5c1234dc 658becec
...@@ -29,46 +29,37 @@ import string, random ...@@ -29,46 +29,37 @@ import string, random
import os import os
from six.moves import range from six.moves import range
class Recipe(GenericBaseRecipe): from zc.buildout import UserError
from zc.buildout.buildout import bool_option
def __init__(self, buildout, name, options):
base_path = options['base-path'] def issubpathof(subpath, path):
if options.get('use-hash-url', 'True') in ['true', 'True']: subpath = os.path.abspath(subpath)
pool = string.ascii_letters + string.digits path = os.path.abspath(path)
hash_string = ''.join(random.choice(pool) for i in range(64)) relpath = os.path.relpath(subpath, start=path)
path = os.path.join(base_path, hash_string) return not relpath.startswith(os.pardir)
if os.path.exists(base_path):
path_list = os.listdir(base_path)
if len(path_list) == 1:
hash_string = path_list[0]
path = os.path.join(base_path, hash_string)
elif len(path_list) > 1:
raise ValueError("Folder %s should contain 0 or 1 element." % base_path)
options['root-dir'] = path
options['path'] = hash_string
else:
options['root-dir'] = base_path
options['path'] = ''
return GenericBaseRecipe.__init__(self, buildout, name, options)
def install(self): class Recipe(GenericBaseRecipe):
def __init__(self, buildout, name, options):
host, port, socketpath, abstract = (
options.get(k) for k in ('host', 'port', 'socketpath', 'abstract'))
oneof = host, socketpath, abstract
if sum(bool(v) for v in oneof) != 1 or bool(host) != bool(port):
raise UserError("Specify one of (host, port) | socketpath | abstract")
address = (host, int(port)) if host else socketpath or '\0' + abstract
options['address'] = address
return GenericBaseRecipe.__init__(self, buildout, name, options)
if not os.path.exists(self.options['root-dir']): def install(self):
os.mkdir( self.options['root-dir'] )
parameters = { parameters = {
'host': self.options['host'], 'address': self.options['address'],
'port': int(self.options['port']),
'cwd': self.options['base-path'], 'cwd': self.options['base-path'],
'log-file': self.options['log-file'], 'log-file': self.options['log-file'],
'cert-file': self.options.get('cert-file', ''), 'cert-file': self.options.get('cert-file'),
'key-file': self.options.get('key-file', ''), 'key-file': self.options.get('key-file'),
'root-dir': self.options['root-dir'] 'allow-write': bool_option(self.options, 'allow-write', 'false')
} }
return self.createPythonScript( return self.createPythonScript(
self.options['wrapper'].strip(), self.options['wrapper'].strip(),
__name__ + '.simplehttpserver.run', __name__ + '.simplehttpserver.run',
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler
from six.moves.BaseHTTPServer import HTTPServer from six.moves.socketserver import TCPServer
import ssl
import os import cgi
import contextlib
import errno
import logging import logging
from netaddr import valid_ipv4, valid_ipv6 import os
import ssl
import socket import socket
import cgi, errno
from slapos.util import str2bytes from slapos.util import str2bytes
from . import issubpathof
class ServerHandler(SimpleHTTPRequestHandler): class ServerHandler(SimpleHTTPRequestHandler):
base_path = None # set by run
restrict_write = True # set by run
_additional_logs = None
@contextlib.contextmanager
def _log_extra(self, msg):
self._additional_logs = msg
try:
yield
finally:
self._additional_logs = None
def _log(self, level, msg, *args):
if self._additional_logs:
msg += self._additional_logs
logging.log(level, '%s - - ' + msg, self.client_address[0], *args)
def log_message(self, msg, *args):
self._log(logging.INFO, msg, *args)
def log_error(self, msg, *args):
self._log(logging.ERROR, msg, *args)
document_path = '' def log_request(self, *args):
restrict_root_folder = True with self._log_extra('\n' + str(self.headers)):
SimpleHTTPRequestHandler.log_request(self, *args)
def respond(self, code=200, type='text/html'): def respond(self, code=200, type='text/html'):
self.send_response(code) self.send_response(code)
self.send_header("Content-type", type) self.send_header("Content-type", type)
self.end_headers() self.end_headers()
def restrictedRootAccess(self): def restrictedWriteAccess(self):
if self.restrict_root_folder and self.path and self.path == '/': if self.restrict_write and self.command not in ('GET', 'HEAD'):
# no access to root path # no write access
self.respond(403) self.respond(403)
self.wfile.write(b"Forbidden") self.wfile.write(b"Forbidden")
return True return True
return False return False
def do_GET(self):
logging.info('%s - GET: %s \n%s' % (self.client_address[0], self.path, self.headers))
if self.restrictedRootAccess():
return
SimpleHTTPRequestHandler.do_GET(self)
def do_POST(self): def do_POST(self):
"""Write to a file on the server. """Write to a file on the server.
...@@ -45,8 +66,7 @@ class ServerHandler(SimpleHTTPRequestHandler): ...@@ -45,8 +66,7 @@ class ServerHandler(SimpleHTTPRequestHandler):
request can be encoded as application/x-www-form-urlencoded or multipart/form-data request can be encoded as application/x-www-form-urlencoded or multipart/form-data
""" """
logging.info('%s - POST: %s \n%s' % (self.client_address[0], self.path, self.headers)) if self.restrictedWriteAccess():
if self.restrictedRootAccess():
return return
form = cgi.FieldStorage( form = cgi.FieldStorage(
...@@ -67,64 +87,76 @@ class ServerHandler(SimpleHTTPRequestHandler): ...@@ -67,64 +87,76 @@ class ServerHandler(SimpleHTTPRequestHandler):
file_open_mode = 'wb' if ('clear' in form and form['clear'].value in ('1', b'1')) else 'ab' file_open_mode = 'wb' if ('clear' in form and form['clear'].value in ('1', b'1')) else 'ab'
self.writeFile(file_path, file_content, file_open_mode) self.writeFile(file_path, file_content, file_open_mode)
self.respond(200, type=self.headers['Content-Type'])
self.wfile.write(b"Content written to %s" % str2bytes(file_path))
def writeFile(self, filename, content, method='ab'): def writeFile(self, filename, content, method='ab'):
file_path = os.path.abspath(os.path.join(self.document_path, filename)) file_path = os.path.abspath(os.path.join(self.base_path, filename))
if not file_path.startswith(self.document_path): # Check writing there is allowed
if not issubpathof(file_path, self.base_path):
self.respond(403, 'text/plain') self.respond(403, 'text/plain')
self.wfile.write(b"Forbidden") self.wfile.write(b"Forbidden")
return
# Create missing directories if needed
try: try:
os.makedirs(os.path.dirname(file_path)) os.makedirs(os.path.dirname(file_path))
except OSError as exception: except OSError as exception:
if exception.errno != errno.EEXIST: if exception.errno != errno.EEXIST:
logging.error('Failed to create file in %s. The error is \n%s' % ( self.log_error('Failed to create file in %s. The error is \n%s',
file_path, str(exception))) file_path, exception)
# Write content to file
logging.info('Writing recieved content to file %s' % file_path) self.log_message('Writing received content to file %s', file_path)
try: try:
with open(file_path, method) as myfile: with open(file_path, method) as myfile:
myfile.write(content) myfile.write(content)
logging.info('Done.') self.log_message('Done.')
except IOError as e: except IOError as e:
logging.error('Something happened while processing \'writeFile\'. The message is %s' % self.log_error(
str(e)) 'Something happened while processing \'writeFile\'. The message is %s',
e)
class HTTPServerV6(HTTPServer): self.respond(200, type=self.headers['Content-Type'])
address_family = socket.AF_INET6 self.wfile.write(b"Content written to %s" % str2bytes(filename))
def run(args): def run(args):
# minimal web server. serves files relative to the current directory.
# minimal web server. serves files relative to the logging.basicConfig(
# current directory. format="%(asctime)s %(levelname)s - %(message)s",
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", filename=args['log-file'],
filename=args['log-file'] ,level=logging.INFO) level=logging.INFO)
port = args['port'] address = args['address']
host = args['host'] cwd = args['cwd']
os.chdir(args['cwd'])
os.chdir(cwd)
Handler = ServerHandler Handler = ServerHandler
Handler.document_path = args['root-dir'] Handler.base_path = cwd
Handler.restrict_root_folder = (args['root-dir'] != args['cwd']) Handler.restrict_write = not args['allow-write']
if valid_ipv6(host): try:
server = HTTPServerV6 host, port = address
else: family, _, _, _, _ = socket.getaddrinfo(host, port)[0]
server = HTTPServer except ValueError:
family = socket.AF_UNIX
httpd = server((host, port), Handler)
scheme = 'http' class Server(TCPServer):
if 'cert-file' in args and 'key-file' in args and \ allow_reuse_address = 1 # for tests, HTTPServer in stdlib sets it too
os.path.exists(args['cert-file']) and os.path.exists(args['key-file']): address_family = family
scheme = 'https'
httpd.socket = ssl.wrap_socket (httpd.socket, httpd = Server(address, Handler)
server_side=True,
certfile=args['cert-file'], certfile = args['cert-file']
keyfile=args['key-file']) if certfile: # keyfile == None signifies key is in certfile
PROTOCOL_TLS_SERVER = getattr(ssl, 'PROTOCOL_TLS_SERVER', None)
logging.info("Starting simple http server at %s://%s:%s" % (scheme, host, port)) if PROTOCOL_TLS_SERVER:
sslcontext = ssl.SSLContext(PROTOCOL_TLS_SERVER)
sslcontext.load_cert_chain(certfile, args['key-file'])
httpd.socket = sslcontext.wrap_socket(httpd.socket, server_side=True)
else: # BBB Py2, Py<3.6
httpd.socket = ssl.wrap_socket(
httpd.socket,
server_side=True,
certfile=certfile,
keyfile=args['key-file'])
logging.info("Starting simple http server at %s", address)
httpd.serve_forever() httpd.serve_forever()
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment