297 lines
9.8 KiB
Python
297 lines
9.8 KiB
Python
################################################################################
|
|
# Copyright (C) 2013-2014 by gempa GmbH
|
|
#
|
|
# HTTP -- Utility methods which generate HTTP result strings
|
|
#
|
|
# Author: Stephan Herrnkind
|
|
# Email: herrnkind@gempa.de
|
|
################################################################################
|
|
|
|
from twisted.web import http, resource, server, static, util
|
|
|
|
import seiscomp.core
|
|
import seiscomp.logging
|
|
|
|
from .utils import accessLog, b_str, u_str, writeTSBin
|
|
|
|
VERSION = "1.2.5"
|
|
|
|
################################################################################
|
|
|
|
|
|
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; charset=utf-8")
|
|
|
|
reference = b"%s/" % request.path.rpartition(b"/")[0]
|
|
|
|
codeStr = http.RESPONSES[code]
|
|
date = b_str(seiscomp.core.Time.GMT().toString("%FT%T.%f"))
|
|
response = resp % (
|
|
code,
|
|
codeStr,
|
|
b_str(msg),
|
|
reference,
|
|
request.uri,
|
|
date,
|
|
b_str(version),
|
|
)
|
|
if not noContent:
|
|
seiscomp.logging.warning(
|
|
f"responding with error: {code} ({u_str(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):
|
|
super().__init__()
|
|
|
|
self.version = version
|
|
self.type = "text/plain"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
def render(self, request):
|
|
request.setHeader("Content-Type", "text/plain; charset=utf-8")
|
|
return b_str(self.version)
|
|
|
|
|
|
################################################################################
|
|
class WADLFilter(static.Data):
|
|
# ---------------------------------------------------------------------------
|
|
def __init__(self, path, paramNameFilterList):
|
|
data = ""
|
|
removeParam = False
|
|
with open(path, "r", encoding="utf-8") as fp:
|
|
for line in fp:
|
|
lineStripped = line.strip().replace(" ", "")
|
|
if removeParam:
|
|
if "</param>" in lineStripped:
|
|
removeParam = False
|
|
continue
|
|
|
|
valid = True
|
|
if "<param" in lineStripped:
|
|
for f in paramNameFilterList:
|
|
if f'name="{f}"' in lineStripped:
|
|
valid = False
|
|
if lineStripped[-2:] != "/>":
|
|
removeParam = True
|
|
break
|
|
|
|
if valid:
|
|
data += line
|
|
|
|
super().__init__(b_str(data), "application/xml; charset=utf-8")
|
|
|
|
|
|
################################################################################
|
|
class BaseResource(resource.Resource):
|
|
# ---------------------------------------------------------------------------
|
|
def __init__(self, version=VERSION):
|
|
super().__init__()
|
|
|
|
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 "
|
|
f"number of objects ({maxObj}). Refine your request parameters."
|
|
)
|
|
self.writeErrorPage(request, http.REQUEST_ENTITY_TOO_LARGE, msg)
|
|
return False
|
|
|
|
|
|
################################################################################
|
|
class NoResource(BaseResource):
|
|
isLeaf = True
|
|
|
|
# ---------------------------------------------------------------------------
|
|
def render(self, request):
|
|
return HTTP.renderNotFound(request, self.version)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
def getChild(self, _path, _request):
|
|
return self
|
|
|
|
|
|
################################################################################
|
|
class ListingResource(BaseResource):
|
|
html = """<!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 render(self, request):
|
|
lis = ""
|
|
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 += f'<li><a href="{name}/">{name}/</a></li>\n'
|
|
|
|
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):
|
|
super().__init__(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 Site(server.Site):
|
|
def __init__(self, res, corsOrigins):
|
|
super().__init__(res)
|
|
|
|
self._corsOrigins = corsOrigins
|
|
|
|
# ---------------------------------------------------------------------------
|
|
def getResourceFor(self, request):
|
|
seiscomp.logging.debug(
|
|
f"request ({request.getClientIP()}): {u_str(request.uri)}"
|
|
)
|
|
request.setHeader("Server", f"SeisComP-FDSNWS/{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")
|