You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
12 KiB
Python
359 lines
12 KiB
Python
1 year ago
|
################################################################################
|
||
|
# Copyright (C) 2013-2014 by gempa GmbH
|
||
|
#
|
||
|
# HTTP -- Utility methods which generate HTTP result strings
|
||
|
#
|
||
|
# Author: Stephan Herrnkind
|
||
|
# Email: herrnkind@gempa.de
|
||
|
################################################################################
|
||
|
|
||
|
from __future__ import absolute_import, division, print_function
|
||
|
|
||
|
import base64
|
||
|
import datetime
|
||
|
import hashlib
|
||
|
import json
|
||
|
import time
|
||
|
import dateutil.parser
|
||
|
|
||
|
from twisted.web import http, resource, server, static, util
|
||
|
|
||
|
import seiscomp.core
|
||
|
import seiscomp.logging
|
||
|
|
||
|
from . import gnupg
|
||
|
from .utils import accessLog, b_str, u_str, py3ustr, py3bstr, writeTSBin
|
||
|
|
||
|
VERSION = "1.2.4"
|
||
|
|
||
|
################################################################################
|
||
|
|
||
|
|
||
|
class HTTP:
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
@staticmethod
|
||
|
def renderErrorPage(request, code, msg, version=VERSION, ro=None):
|
||
|
resp = b"""\
|
||
|
Error %i: %s
|
||
|
|
||
|
%s
|
||
|
|
||
|
Usage details are available from %s
|
||
|
|
||
|
Request:
|
||
|
%s
|
||
|
|
||
|
Request Submitted:
|
||
|
%s
|
||
|
|
||
|
Service Version:
|
||
|
%s
|
||
|
"""
|
||
|
|
||
|
noContent = code == http.NO_CONTENT
|
||
|
|
||
|
# rewrite response code if requested and no data was found
|
||
|
if noContent and ro is not None:
|
||
|
code = ro.noData
|
||
|
|
||
|
# set response code
|
||
|
request.setResponseCode(code)
|
||
|
|
||
|
# status code 204 requires no message body
|
||
|
if code == http.NO_CONTENT:
|
||
|
response = b""
|
||
|
else:
|
||
|
request.setHeader('Content-Type', 'text/plain')
|
||
|
|
||
|
reference = b"%s/" % request.path.rpartition(b'/')[0]
|
||
|
|
||
|
codeStr = http.RESPONSES[code]
|
||
|
date = py3bstr(seiscomp.core.Time.GMT().toString("%FT%T.%f"))
|
||
|
response = resp % (code, codeStr, py3bstr(msg), reference,
|
||
|
request.uri, date, py3bstr(version))
|
||
|
if not noContent:
|
||
|
seiscomp.logging.warning("responding with error: %i (%s)" % (
|
||
|
code, py3ustr(codeStr)))
|
||
|
|
||
|
accessLog(request, ro, code, len(response), msg)
|
||
|
return response
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
@staticmethod
|
||
|
def renderNotFound(request, version=VERSION):
|
||
|
msg = "The requested resource does not exist on this server."
|
||
|
return HTTP.renderErrorPage(request, http.NOT_FOUND, msg, version)
|
||
|
|
||
|
# ---------------------------------------------------------------------------
|
||
|
@staticmethod
|
||
|
def renderNotModified(request, ro=None):
|
||
|
code = http.NOT_MODIFIED
|
||
|
request.setResponseCode(code)
|
||
|
request.responseHeaders.removeHeader('content-type')
|
||
|
accessLog(request, ro, code, 0, None)
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class ServiceVersion(resource.Resource):
|
||
|
isLeaf = True
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def __init__(self, version):
|
||
|
resource.Resource.__init__(self)
|
||
|
self.version = version
|
||
|
self.type = 'text/plain'
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def render(self, request):
|
||
|
request.setHeader('content-type', 'text/plain')
|
||
|
return py3bstr(self.version)
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class WADLFilter(static.Data):
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def __init__(self, path, paramNameFilterList):
|
||
|
data = ""
|
||
|
removeParam = False
|
||
|
for line in open(path):
|
||
|
lineStripped = line.strip().replace(' ', '')
|
||
|
if removeParam:
|
||
|
if '</param>' in lineStripped:
|
||
|
removeParam = False
|
||
|
continue
|
||
|
|
||
|
valid = True
|
||
|
if '<param' in lineStripped:
|
||
|
for f in paramNameFilterList:
|
||
|
if 'name="{}"'.format(f) in lineStripped:
|
||
|
valid = False
|
||
|
if lineStripped[-2:] != '/>':
|
||
|
removeParam = True
|
||
|
break
|
||
|
|
||
|
if valid:
|
||
|
data += line
|
||
|
|
||
|
static.Data.__init__(self, py3bstr(data), 'application/xml')
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class BaseResource(resource.Resource):
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def __init__(self, version=VERSION):
|
||
|
resource.Resource.__init__(self)
|
||
|
self.version = version
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def renderErrorPage(self, request, code, msg, ro=None):
|
||
|
return HTTP.renderErrorPage(request, code, msg, self.version, ro)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def writeErrorPage(self, request, code, msg, ro=None):
|
||
|
data = self.renderErrorPage(request, code, msg, ro)
|
||
|
if data:
|
||
|
writeTSBin(request, data)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def returnNotModified(self, request, ro=None):
|
||
|
HTTP.renderNotModified(request, ro)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
# Renders error page if the result set exceeds the configured maximum number
|
||
|
# objects
|
||
|
def checkObjects(self, request, objCount, maxObj):
|
||
|
if objCount <= maxObj:
|
||
|
return True
|
||
|
|
||
|
msg = "The result set of your request exceeds the configured maximum " \
|
||
|
"number of objects (%i). Refine your request parameters." % maxObj
|
||
|
self.writeErrorPage(request, http.REQUEST_ENTITY_TOO_LARGE, msg)
|
||
|
return False
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class NoResource(BaseResource):
|
||
|
isLeaf = True
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def __init__(self, version=VERSION):
|
||
|
BaseResource.__init__(self, version)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def render(self, request):
|
||
|
return HTTP.renderNotFound(request, self.version)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def getChild(self, path, request):
|
||
|
return self
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class ListingResource(BaseResource):
|
||
|
|
||
|
html = u"""<!doctype html>
|
||
|
<html lang="en">
|
||
|
<head>
|
||
|
<meta charset="utf-8">
|
||
|
<meta name="author" content="gempa GmbH">
|
||
|
<title>SeisComP FDSNWS Implementation</title>
|
||
|
</head>
|
||
|
<body>
|
||
|
<p><a href="../">Parent Directory</a></p>
|
||
|
<h1>SeisComP FDSNWS Web Service</h1>
|
||
|
<p>Index of %s</p>
|
||
|
<ul>
|
||
|
%s
|
||
|
</ul>
|
||
|
</body>"""
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def __init__(self, version=VERSION):
|
||
|
BaseResource.__init__(self, version)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def render(self, request):
|
||
|
lis = u""
|
||
|
if request.path[-1:] != b'/':
|
||
|
return util.redirectTo(request.path + b'/', request)
|
||
|
|
||
|
for k, v in self.children.items():
|
||
|
if v.isLeaf:
|
||
|
continue
|
||
|
if hasattr(v, 'hideInListing') and v.hideInListing:
|
||
|
continue
|
||
|
name = u_str(k)
|
||
|
lis += u'<li><a href="{0}/">{0}/</a></li>\n'.format(name)
|
||
|
return b_str(ListingResource.html % (u_str(request.path), lis))
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def getChild(self, path, request):
|
||
|
if not path:
|
||
|
return self
|
||
|
return NoResource(self.version)
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class DirectoryResource(static.File):
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def __init__(self, fileName, version=VERSION):
|
||
|
static.File.__init__(self, fileName)
|
||
|
self.version = version
|
||
|
self.childNotFound = NoResource(self.version)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def render(self, request):
|
||
|
if request.path[-1:] != b'/':
|
||
|
return util.redirectTo(request.path + b'/', request)
|
||
|
return static.File.render(self, request)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def getChild(self, path, request):
|
||
|
if not path:
|
||
|
return self
|
||
|
return NoResource(self.version)
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class AuthResource(BaseResource):
|
||
|
isLeaf = True
|
||
|
|
||
|
def __init__(self, version, gnupghome, userdb):
|
||
|
BaseResource.__init__(self, version)
|
||
|
self.__gpg = gnupg.GPG(gnupghome=gnupghome)
|
||
|
self.__userdb = userdb
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def render_POST(self, request):
|
||
|
request.setHeader('Content-Type', 'text/plain')
|
||
|
|
||
|
try:
|
||
|
verified = self.__gpg.decrypt(request.content.getvalue())
|
||
|
|
||
|
except OSError as e:
|
||
|
msg = "gpg decrypt error"
|
||
|
seiscomp.logging.warning("%s: %s" % (msg, str(e)))
|
||
|
return self.renderErrorPage(request, http.INTERNAL_SERVER_ERROR, msg)
|
||
|
|
||
|
except Exception as e:
|
||
|
msg = "invalid token"
|
||
|
seiscomp.logging.warning("%s: %s" % (msg, str(e)))
|
||
|
return self.renderErrorPage(request, http.BAD_REQUEST, msg)
|
||
|
|
||
|
if verified.trust_level is None or verified.trust_level < verified.TRUST_FULLY:
|
||
|
msg = "token has invalid signature"
|
||
|
seiscomp.logging.warning(msg)
|
||
|
return self.renderErrorPage(request, http.BAD_REQUEST, msg)
|
||
|
|
||
|
try:
|
||
|
attributes = json.loads(py3ustr(verified.data))
|
||
|
td = dateutil.parser.parse(attributes['valid_until']) - \
|
||
|
datetime.datetime.now(dateutil.tz.tzutc())
|
||
|
lifetime = td.seconds + td.days * 24 * 3600
|
||
|
|
||
|
except Exception as e:
|
||
|
msg = "token has invalid validity"
|
||
|
seiscomp.logging.warning("%s: %s" % (msg, str(e)))
|
||
|
return self.renderErrorPage(request, http.BAD_REQUEST, msg)
|
||
|
|
||
|
if lifetime <= 0:
|
||
|
msg = "token is expired"
|
||
|
seiscomp.logging.warning(msg)
|
||
|
return self.renderErrorPage(request, http.BAD_REQUEST, msg)
|
||
|
|
||
|
userid = base64.urlsafe_b64encode(
|
||
|
hashlib.sha256(verified.data).digest()[:18])
|
||
|
password = self.__userdb.addUser(
|
||
|
userid, attributes, time.time() + min(lifetime, 24 * 3600),
|
||
|
py3ustr(verified.data))
|
||
|
accessLog(request, None, http.OK, len(userid)+len(password)+1, None)
|
||
|
return userid + b':' + password
|
||
|
|
||
|
|
||
|
################################################################################
|
||
|
class Site(server.Site):
|
||
|
def __init__(self, res, corsOrigins):
|
||
|
server.Site.__init__(self, res)
|
||
|
self._corsOrigins = corsOrigins
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def getResourceFor(self, request):
|
||
|
seiscomp.logging.debug("request (%s): %s" % (
|
||
|
request.getClientIP(), py3ustr(request.uri)))
|
||
|
request.setHeader('Server', 'SeisComP-FDSNWS/%s' % VERSION)
|
||
|
request.setHeader('Access-Control-Allow-Headers', 'Authorization')
|
||
|
request.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate')
|
||
|
|
||
|
self.setAllowOrigin(request)
|
||
|
|
||
|
return server.Site.getResourceFor(self, request)
|
||
|
|
||
|
#---------------------------------------------------------------------------
|
||
|
def setAllowOrigin(self, req):
|
||
|
# no allowed origin: no response header
|
||
|
lenOrigins = len(self._corsOrigins)
|
||
|
if lenOrigins == 0:
|
||
|
return
|
||
|
|
||
|
# one origin: add header
|
||
|
if lenOrigins == 1:
|
||
|
req.setHeader('Access-Control-Allow-Origin', self._corsOrigins[0])
|
||
|
return
|
||
|
|
||
|
# more than one origin: check current origin against allowed origins
|
||
|
# and return the current origin on match.
|
||
|
origin = req.getHeader('Origin')
|
||
|
if origin in self._corsOrigins:
|
||
|
req.setHeader('Access-Control-Allow-Origin', origin)
|
||
|
|
||
|
# Set Vary header to let the browser know that the response depends
|
||
|
# on the request. Certain cache strategies should be disabled.
|
||
|
req.setHeader('Vary', 'Origin')
|
||
|
|
||
|
|
||
|
# vim: ts=4 et
|