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

simplehttpserver: Allow disabling write access

See merge request !1685
parents 5c1234dc 658becec
Pipeline #39274 failed with stage
in 0 seconds
......@@ -29,46 +29,37 @@ import string, random
import os
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']
if options.get('use-hash-url', 'True') in ['true', 'True']:
pool = string.ascii_letters + string.digits
hash_string = ''.join(random.choice(pool) for i in range(64))
path = os.path.join(base_path, hash_string)
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 issubpathof(subpath, path):
subpath = os.path.abspath(subpath)
path = os.path.abspath(path)
relpath = os.path.relpath(subpath, start=path)
return not relpath.startswith(os.pardir)
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']):
os.mkdir( self.options['root-dir'] )
def install(self):
parameters = {
'host': self.options['host'],
'port': int(self.options['port']),
'address': self.options['address'],
'cwd': self.options['base-path'],
'log-file': self.options['log-file'],
'cert-file': self.options.get('cert-file', ''),
'key-file': self.options.get('key-file', ''),
'root-dir': self.options['root-dir']
'cert-file': self.options.get('cert-file'),
'key-file': self.options.get('key-file'),
'allow-write': bool_option(self.options, 'allow-write', 'false')
}
return self.createPythonScript(
self.options['wrapper'].strip(),
__name__ + '.simplehttpserver.run',
......
# -*- coding: utf-8 -*-
from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler
from six.moves.BaseHTTPServer import HTTPServer
import ssl
import os
from six.moves.socketserver import TCPServer
import cgi
import contextlib
import errno
import logging
from netaddr import valid_ipv4, valid_ipv6
import os
import ssl
import socket
import cgi, errno
from slapos.util import str2bytes
from . import issubpathof
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 = ''
restrict_root_folder = True
def log_request(self, *args):
with self._log_extra('\n' + str(self.headers)):
SimpleHTTPRequestHandler.log_request(self, *args)
def respond(self, code=200, type='text/html'):
self.send_response(code)
self.send_header("Content-type", type)
self.end_headers()
def restrictedRootAccess(self):
if self.restrict_root_folder and self.path and self.path == '/':
# no access to root path
def restrictedWriteAccess(self):
if self.restrict_write and self.command not in ('GET', 'HEAD'):
# no write access
self.respond(403)
self.wfile.write(b"Forbidden")
return True
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):
"""Write to a file on the server.
......@@ -45,8 +66,7 @@ class ServerHandler(SimpleHTTPRequestHandler):
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.restrictedRootAccess():
if self.restrictedWriteAccess():
return
form = cgi.FieldStorage(
......@@ -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'
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'):
file_path = os.path.abspath(os.path.join(self.document_path, filename))
if not file_path.startswith(self.document_path):
file_path = os.path.abspath(os.path.join(self.base_path, filename))
# Check writing there is allowed
if not issubpathof(file_path, self.base_path):
self.respond(403, 'text/plain')
self.wfile.write(b"Forbidden")
return
# Create missing directories if needed
try:
os.makedirs(os.path.dirname(file_path))
except OSError as exception:
if exception.errno != errno.EEXIST:
logging.error('Failed to create file in %s. The error is \n%s' % (
file_path, str(exception)))
logging.info('Writing recieved content to file %s' % file_path)
self.log_error('Failed to create file in %s. The error is \n%s',
file_path, exception)
# Write content to file
self.log_message('Writing received content to file %s', file_path)
try:
with open(file_path, method) as myfile:
myfile.write(content)
logging.info('Done.')
self.log_message('Done.')
except IOError as e:
logging.error('Something happened while processing \'writeFile\'. The message is %s' %
str(e))
class HTTPServerV6(HTTPServer):
address_family = socket.AF_INET6
self.log_error(
'Something happened while processing \'writeFile\'. The message is %s',
e)
self.respond(200, type=self.headers['Content-Type'])
self.wfile.write(b"Content written to %s" % str2bytes(filename))
def run(args):
# minimal web server. serves files relative to the
# current directory.
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
filename=args['log-file'] ,level=logging.INFO)
port = args['port']
host = args['host']
os.chdir(args['cwd'])
# minimal web server. serves files relative to the current directory.
logging.basicConfig(
format="%(asctime)s %(levelname)s - %(message)s",
filename=args['log-file'],
level=logging.INFO)
address = args['address']
cwd = args['cwd']
os.chdir(cwd)
Handler = ServerHandler
Handler.document_path = args['root-dir']
Handler.restrict_root_folder = (args['root-dir'] != args['cwd'])
if valid_ipv6(host):
server = HTTPServerV6
else:
server = HTTPServer
httpd = server((host, port), Handler)
scheme = 'http'
if 'cert-file' in args and 'key-file' in args and \
os.path.exists(args['cert-file']) and os.path.exists(args['key-file']):
scheme = 'https'
httpd.socket = ssl.wrap_socket (httpd.socket,
server_side=True,
certfile=args['cert-file'],
keyfile=args['key-file'])
logging.info("Starting simple http server at %s://%s:%s" % (scheme, host, port))
Handler.base_path = cwd
Handler.restrict_write = not args['allow-write']
try:
host, port = address
family, _, _, _, _ = socket.getaddrinfo(host, port)[0]
except ValueError:
family = socket.AF_UNIX
class Server(TCPServer):
allow_reuse_address = 1 # for tests, HTTPServer in stdlib sets it too
address_family = family
httpd = Server(address, Handler)
certfile = args['cert-file']
if certfile: # keyfile == None signifies key is in certfile
PROTOCOL_TLS_SERVER = getattr(ssl, 'PROTOCOL_TLS_SERVER', None)
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()
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