def parse_http_headers(environ): h = Headers([]) for k, v in environ.items(): if k.startswith('HTTP_'): name = k[5:] h.add_header(name, v) return h
class Response(object): def __init__(self, response=None, status=200, charset='utf-8', content_type='text/html'): self.response = [] if response is None else response self._status = status self.charset = charset self.headers = Headers() content_type = '{content_type}; charset={charset}'.format( content_type=content_type, charset=charset) self.headers.add_header('content_type', content_type) @property def status(self): status_string = http.client.responses.get( self._status, 'UNKNOWN') # 如果没有status, 就回复UNKNOWN return '{status} {status_string}'.format(status=self._status, status_string=status_string) def __iter__(self): # 如果response里面的值是bytes的话,就返回, 否则转换成设定的字符编码再返回。 for val in self.response: if isinstance(val, bytes): yield val else: yield val.encode(self.charset)
class Response: default_status = 200 default_charset = 'utf-8' default_content_type = 'text/html; charset=UTF-8' def __init__(self, body='', status=None, headers=None, charset=None): self._body = body self.status = status or self.default_status self.headers = Headers() self.charset = charset or self.default_charset if headers: for name, value in headers.items(): self.headers.add_header(name, value) @property def status_code(self): return "%d %s" % (self.status, http_responses[self.status]) @property def header_list(self): if 'Content-Type' not in self.headers: self.headers.add_header('Content-Type', self.default_content_type) return self.headers.items() @property def body(self): if isinstance(self._body, str): return [self._body.encode(self.charset)] return [self._body]
class BaseResponse(object): def __init__(self, status_code=None, reason_phrase=None, content_type=None, charset=None): try: if not status_code: status_code = 200 reason_phrase = "OK" self.status_code = int(status_code) self.reason_phrase = reason_phrase except (TypeError, ValueError): # TODO: add traceback self.status_code = 500 self.reason_phrase = "Internal Server Error" self.cookies = [] self.charset = charset or "utf-8" self._handler_class = None self._content = [] self.__set_default_headers() content_type = "text/html" if not content_type else content_type.strip( " \r\n") if (content_type.split("/", 1)[0] in ("text", "application") and "charset" not in content_type.lower()): content_type = "%s; charset=%s" % (content_type.rstrip("; \r\n"), self.charset) self.headers.add_header("Content-Type", content_type) def __set_default_headers(self): self.headers = Headers([ ("Server", "MadLiar/%s" % __version__), ("X-Frame-Options", "SAMEORIGIN"), ]) @property def content(self): return b''.join(self._content) @content.setter def content(self, value): # Consume iterators upon assignment to allow repeated iteration. if not hasattr(value, '__iter__'): value = [value] content = b''.join( map( lambda x: bytes(x) if isinstance(x, bytes) else bytes(x.encode(self.charset)), value)) self.headers.add_header("Content-length", str(len(content))) self._content = [content] def __iter__(self): return iter(self._content)
def testExtras(self): h = Headers([]) self.assertEqual(str(h), "\r\n") h.add_header("foo", "bar", baz="spam") self.assertEqual(h["foo"], 'bar; baz="spam"') self.assertEqual(str(h), 'foo: bar; baz="spam"\r\n\r\n') h.add_header("Foo", "bar", cheese=None) self.assertEqual(h.get_all("foo"), ['bar; baz="spam"', "bar; cheese"]) self.assertEqual(str(h), 'foo: bar; baz="spam"\r\n' "Foo: bar; cheese\r\n" "\r\n")
class Response(threading.local): """ Represents a single response using thread-local namespace. """ def bind(self, app): """ Clears old data and creates a brand new Response object """ self._COOKIES = None self.status = 200 self.header_list = [] self.header = HeaderWrapper(self.header_list) self.charset = 'UTF-8' self.content_type = 'text/html; charset=UTF-8' self.error = None self.app = app def add_header(self, key, value): self.header.add_header(key.title(), str(value)) def wsgiheaders(self): ''' Returns a wsgi conform list of header/value pairs ''' for c in self.COOKIES.values(): self.add_header('Set-Cookie', c.OutputString()) return self.header_list @property def COOKIES(self): if not self._COOKIES: self._COOKIES = SimpleCookie() return self._COOKIES def set_cookie(self, key, value, **kargs): """ Sets a Cookie. Optional settings: expires, path, comment, domain, max-age, secure, version, httponly """ if not isinstance(value, basestring): sec = self.app.config['securecookie.key'] value = cookie_encode(value, sec) self.COOKIES[key] = value for k, v in kargs.iteritems(): self.COOKIES[key][k] = v def get_content_type(self): """ Get the current 'Content-Type' header. """ return self.header['Content-Type'] def set_content_type(self, value): if 'charset=' in value: self.charset = value.split('charset=')[-1].split(';')[0].strip() self.header['Content-Type'] = value content_type = property(get_content_type, set_content_type, None, get_content_type.__doc__)
class Request: id: str environ: dict wsgi_input: BufferedReader path: str method: str port: str host: str protocol: str server_name: str query: dict headers: Headers body: Dict raw_body: bytes params: Dict[str, Any] def __init__(self, environ: dict): self.id = ''.join(random.choice(chars) for i in range(30)) self.body = dict() self.environ = environ self.wsgi_input: BufferedReader = cast(BufferedReader, environ.get('wsgi.input')) self.path = environ.get('PATH_INFO', '') self.method = environ.get('REQUEST_METHOD', '') self.port = environ.get('PORT', '') self.host = environ.get('HTTP_HOST', '') self.protocol = environ.get('HTTP_PROTOCOL', '') self.server_name = environ.get('SERVER_NAME', '') self.query = parse_qs(environ.get('QUERY_STRING', '')) self.params = dict() self.headers = Headers() self._parse_http_headers() self._read_request_body() def _parse_http_headers(self: 'Request'): for key in self.environ.keys(): replaced_key: str = key.replace('HTTP_', '') value = self.environ.get(key) if replaced_key in HTTP_HEADERS: final_key: str = capwords(replaced_key, '_').replace('_', '-') self.headers.add_header(final_key, value) def _read_request_body(self: 'Request'): try: request_body_size = int(self.headers.get(CONTENT_LENGTH, '0')) except ValueError: request_body_size = 0 self.raw_body = self.wsgi_input.read(request_body_size)
class Static(Route): def __init__(self, status, content_type): self.__status = status self.__ct = content_type self.__header = Headers() @property def status_code(self): return '{} {}'.format(self.__status, responses[self.__status]) @property def headers(self): self.__header.add_header('Content-type', self.__ct) return self.__header.items()
def testExtras(self): h = Headers() self.assertEqual(str(h), '\r\n') h.add_header('foo', 'bar', baz="spam") self.assertEqual(h['foo'], 'bar; baz="spam"') self.assertEqual(str(h), 'foo: bar; baz="spam"\r\n\r\n') h.add_header('Foo', 'bar', cheese=None) self.assertEqual(h.get_all('foo'), ['bar; baz="spam"', 'bar; cheese']) self.assertEqual( str(h), 'foo: bar; baz="spam"\r\n' 'Foo: bar; cheese\r\n' '\r\n')
def __call__(self, environ, start_response): key_morsel = Cookie(environ.get("HTTP_COOKIE", "")).get(self.toggle_key) # useful vars query = query_str2dict(environ.get("QUERY_STRING")) enable_by_cookie = key_morsel.value == self.enable_value if key_morsel else False enable_by_query = query.get(self.toggle_key) == self.enable_value # pop toggle_key from query dic to avoid case: '?_profile=on&_profile=' disable = query.pop(self.toggle_key, None) == "" # only can be disabled by query enable = not disable and (enable_by_query or enable_by_cookie) run_app, resp_body, saved_ss_args = self._intercept_call() # processing cookies and queries so = query.pop(self.SIMPLE_OUTPUT_TOGGLE_KEY, None) if so is not None: self.simple_output = so == "True" cookie_to_set = None if enable_by_query and not enable_by_cookie: cookie_to_set = "%s=%s; Path=/; HttpOnly" % (self.toggle_key, self.enable_value) elif disable: cookie_to_set = "%s=; Path=/; Max-Age=1; HttpOnly" % self.toggle_key if enable: start = time.time() profile = Profile() profile.runcall(run_app, environ) # here we call the WSGI app elapsed = time.time() - start else: profile = elapsed = None # for annoying IDE run_app(environ) status, headers = saved_ss_args[:2] headers_dic = Headers(headers) if cookie_to_set: headers_dic.add_header("Set-Cookie", cookie_to_set) # insert result into response content_type = headers_dic.get("Content-Type", "") if enable and status.startswith("200") and content_type.startswith("text/html"): environ["QUERY_STRING"] = dict2query_str(query) matched = _find_charset.match(content_type) encoding = matched.group(1) if matched else "ascii" rendered = self.render_result(profile, elapsed, environ).encode(encoding, "replace") resp_body = [insert_into_body(rendered, b"".join(resp_body))] headers_dic["Content-Length"] = str(len(resp_body[0])) start_response(status, headers, saved_ss_args[2] if len(saved_ss_args) == 3 else None) return resp_body
def _parse_headers(environ): """ Parse the environmental variables, looking for HTTP request headers. :param environ: environmental variables :type environ: dict :return: request headers :rtype: dict """ headers = Headers([]) for key, value in environ.items(): match = _HTTP_HEADER_REGEX.match(key) if match is None: continue name = _normalize_header_name(match.group(0)) headers.add_header(name, value) return headers
def static_file_view(env, start_response, filename, block_size, charset): method = env['REQUEST_METHOD'].upper() if method not in ('HEAD', 'GET'): start_response('405 METHOD NOT ALLOWED', [('Content-Type', 'text/plain; UTF-8')]) return [b''] mimetype, encoding = mimetypes.guess_type(filename) headers = Headers([]) headers.add_header('Content-Encodings', encoding) headers.add_header('Content-Type', get_content_type(mimetype, charset)) headers.add_header('Content-Length', get_content_length(filename)) headers.add_header('Last-Modified', generate_last_modified()) headers.add_header("Accept-Ranges", "bytes") start_response('200 OK', headers.items()) return _get_body(filename, method, block_size, charset)
def testBytes(self): h = Headers([(b"Content-Type", b"text/plain; charset=utf-8")]) self.assertEqual("text/plain; charset=utf-8", h.get("Content-Type")) h[b"Foo"] = bytes(b"bar") self.assertEqual("bar", h.get("Foo")) self.assertEqual("bar", h.get(b"Foo")) h.setdefault(b"Bar", b"foo") self.assertEqual("foo", h.get("Bar")) self.assertEqual("foo", h.get(b"Bar")) h.add_header(b"content-disposition", b"attachment", filename=b"bud.gif") self.assertEqual('attachment; filename="bud.gif"', h.get("content-disposition")) del h["content-disposition"] self.assertTrue(b"content-disposition" not in h)
def __call__(self): """Set the headers and return the body. It is not necessary to supply Content-length, because this is added by the caller. """ headers = Headers([]) content_type_params = {} if self.charset is not None: content_type_params['charset'] = self.charset headers.add_header( 'Content-Type', self.content_type, **content_type_params) headers.add_header( 'Content-Disposition', 'attachment', filename=self.filename) for key, value in headers.items(): self.request.response.setHeader(key, value) return self.getBody()
def testExtras(self): h = Headers([]) self.assertEqual(str(h),'\r\n') h.add_header('foo','bar',baz="spam") self.assertEqual(h['foo'], 'bar; baz="spam"') self.assertEqual(str(h),'foo: bar; baz="spam"\r\n\r\n') h.add_header('Foo','bar',cheese=None) self.assertEqual(h.get_all('foo'), ['bar; baz="spam"', 'bar; cheese']) self.assertEqual(str(h), 'foo: bar; baz="spam"\r\n' 'Foo: bar; cheese\r\n' '\r\n' )
def http_request_header(self, range): scheme, netloc, path, params, query, fragment = list(self._urlparse) query = query + ''.join( [i + '=' + j for i, j in self._rangef.getquery(range).items()]) query = query.lstrip('&') if query: path = path + '?' + query headers = Headers(self.headers.items()) for i, j in self._rangef.getheader(range).items(): headers.add_header(i, j) if self.cookie: headers.add_header('Cookie', self.cookie) path = path.strip().replace(' ', '%20') return path, headers
class Response(threading.local): """ Represents a single response using thread-local namespace. """ def bind(self): """ Clears old data and creates a brand new Response object """ self._COOKIES = None self.status = 200 self.header_list = [] self.header = HeaderWrapper(self.header_list) self.content_type = 'text/html' self.error = None self.charset = 'utf8' def wsgiheaders(self): ''' Returns a wsgi conform list of header/value pairs ''' for c in self.COOKIES.itervalues(): self.header.add_header('Set-Cookie', c.OutputString()) return [(h.title(), str(v)) for h, v in self.header.items()] @property def COOKIES(self): if not self._COOKIES: self._COOKIES = SimpleCookie() return self._COOKIES def set_cookie(self, key, value, **kargs): """ Sets a Cookie. Optional settings: expires, path, comment, domain, max-age, secure, version, httponly """ self.COOKIES[key] = value for k, v in kargs.iteritems(): self.COOKIES[key][k] = v def get_content_type(self): """ Get the current 'Content-Type' header. """ return self.header['Content-Type'] def set_content_type(self, value): if 'charset=' in value: self.charset = value.split('charset=')[-1].split(';')[0].strip() self.header['Content-Type'] = value content_type = property(get_content_type, set_content_type, None, get_content_type.__doc__)
class Response(threading.local): """ Represents a single response using thread-local namespace. """ def bind(self): """ Clears old data and creates a brand new Response object """ self._COOKIES = None self.status = 200 self.header_list = [] self.header = HeaderWrapper(self.header_list) self.content_type = "text/html" self.error = None self.charset = "utf8" def wsgiheaders(self): """ Returns a wsgi conform list of header/value pairs """ for c in self.COOKIES.itervalues(): self.header.add_header("Set-Cookie", c.OutputString()) return [(h.title(), str(v)) for h, v in self.header.items()] @property def COOKIES(self): if not self._COOKIES: self._COOKIES = SimpleCookie() return self._COOKIES def set_cookie(self, key, value, **kargs): """ Sets a Cookie. Optional settings: expires, path, comment, domain, max-age, secure, version, httponly """ self.COOKIES[key] = value for k, v in kargs.iteritems(): self.COOKIES[key][k] = v def get_content_type(self): """ Get the current 'Content-Type' header. """ return self.header["Content-Type"] def set_content_type(self, value): if "charset=" in value: self.charset = value.split("charset=")[-1].split(";")[0].strip() self.header["Content-Type"] = value content_type = property(get_content_type, set_content_type, None, get_content_type.__doc__)
class Response: def __init__(self, response=None, status=200, charset='utf-8', content_type='text/html'): self.response = [] if response is None else response self.charset = charset self.headers = Headers() ctype = f'{content_type}; charset={charset}' self.headers.add_header('content-type', ctype) self._status = status @property def status(self): status_string = http.client.responses[self._status] or 'UNKNOWN' return f'{self._status} {status_string}' def __iter__(self): for k in self.response: if isinstance(k, bytes): yield k else: yield k.encode(self.charset)
def testBytes(self): h = Headers([ (b"Content-Type", b"text/plain; charset=utf-8"), ]) self.assertEqual("text/plain; charset=utf-8", h.get("Content-Type")) h[b"Foo"] = bytes(b"bar") self.assertEqual("bar", h.get("Foo")) self.assertEqual("bar", h.get(b"Foo")) h.setdefault(b"Bar", b"foo") self.assertEqual("foo", h.get("Bar")) self.assertEqual("foo", h.get(b"Bar")) h.add_header(b'content-disposition', b'attachment', filename=b'bud.gif') self.assertEqual('attachment; filename="bud.gif"', h.get("content-disposition")) del h['content-disposition'] self.assertTrue(b'content-disposition' not in h)
class Response(threading.local): """ Represents a single response using thread-local namespace. """ def bind(self): """ Clears old data and creates a brand new Response object """ self._COOKIES = None self.status = 200 self.header_list = [] self.header = HeaderWrapper(self.header_list) self.content_type = 'text/html' self.error = None def wsgiheaders(self): ''' Returns a wsgi conform list of header/value pairs ''' for c in self.COOKIES.itervalues(): self.header.add_header('Set-Cookie', c.OutputString()) return [(h.title(), str(v)) for h, v in self.header_list] @property def COOKIES(self): if not self._COOKIES: self._COOKIES = SimpleCookie() return self._COOKIES def set_cookie(self, key, value, **kargs): """ Sets a Cookie. Optional settings: expires, path, comment, domain, max-age, secure, version, httponly """ self.COOKIES[key] = value for k in kargs: self.COOKIES[key][k] = kargs[k] def get_content_type(self): '''Gives access to the 'Content-Type' header and defaults to 'text/html'.''' return self.header['Content-Type'] def set_content_type(self, value): self.header['Content-Type'] = value content_type = property(get_content_type, set_content_type, None, get_content_type.__doc__)
class Response(object): """ Wrapper class around the start_response and return iterable of the WSGI protocol. :ivar status: HTTP status of the response :type status: int :ivar headers: response headers :type headers: dict :ivar cookies: cookies to set :type cookies: dict """ def __init__(self): self.status = None self.__header_list = [] self.headers = Headers(self.__header_list) self.headers['date'] = rfc1123_date() self.cookies = SimpleCookie() def start_response_args(self, body): """ Return a status string and a list of headers for this response, appropriate as arguments to the start_response function. :param body: response body :type body: str :return: tuple of status string and list of header tuples :rtype: (str, list) """ if self.status is None: raise RuntimeError('response status was not set') self.headers['content-length'] = str(len(body)) for cookie in self.cookies: self.headers.add_header('set-cookie', cookie.output(header='')) status_str = '%d %s' % (self.status, httplib.responses[self.status]) return (status_str, self.__header_list)
def do_GET(self, environ, start_response): headers = Headers() video_url = "{PATH_INFO}?{QUERY_STRING}".format(**environ).strip("/") if video_url == "?": raise self.HTTPError(400, more="no URL provided") try: videos = self.downloader.get_videos(video_url) except CannotDownload as cad: raise self.HTTPError(400, "Cannot download", more=str(cad)) if len(videos) != 1: raise self.HTTPError(400, more="playlists not supported yet") video = videos[0] audio_file = self.downloader.cache_dir / video.path assert audio_file.exists() filesize = audio_file.stat().st_size headers.add_header("Content-Disposition", "attachment", filename=video.title) headers.add_header("Content-Type", "audio/mpeg") headers.add_header("Content-Length", str(filesize)) start_response("200 OK", headers.items()) return FileWrapper(audio_file.open("rb"))
def __call__(self, environ, start_response): '''Main function for handling a single request. Follows the WSGI API. @param environ: dictionary with environment variables for the request and some special variables. See the PEP for expected variables. @param start_response: a function that can be called to set the http response and headers. For example:: start_response(200, [('Content-Type', 'text/plain')]) @returns: the html page content as a list of lines ''' headerlist = [] headers = Headers(headerlist) path = environ.get('PATH_INFO', '/') try: methods = ('GET', 'HEAD') if not environ['REQUEST_METHOD'] in methods: raise WWWError('405', headers=[('Allow', ', '.join(methods))]) # cleanup path #~ print 'INPUT', path path = path.replace('\\', '/') # make it windows save isdir = path.endswith('/') parts = [p for p in path.split('/') if p and not p == '.'] if [p for p in parts if p.startswith('.')]: # exclude .. and all hidden files from possible paths raise WebPathNotValidError() path = '/' + '/'.join(parts) if isdir and not path == '/': path += '/' #~ print 'PATH', path if not path: path = '/' elif path == '/favicon.ico': path = '/+resources/favicon.ico' else: path = urllib.unquote(path) if path == '/': headers.add_header('Content-Type', 'text/html', charset='utf-8') content = self.render_index() elif path.startswith('/+docs/'): dir = self.notebook.document_root if not dir: raise WebPageNotFoundError(path) file = dir.file(path[7:]) content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() elif path.startswith('/+file/'): file = self.notebook.dir.file(path[7:]) # TODO: need abstraction for getting file from top level dir ? content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() elif path.startswith('/+resources/'): if self.template.resources_dir: file = self.template.resources_dir.file(path[12:]) if not file.exists(): file = data_file('pixmaps/%s' % path[12:]) else: file = data_file('pixmaps/%s' % path[12:]) if file: content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() else: raise WebPageNotFoundError(path) else: # Must be a page or a namespace (html file or directory path) headers.add_header('Content-Type', 'text/html', charset='utf-8') if path.endswith('.html'): pagename = path[:-5].replace('/', ':') elif path.endswith('/'): pagename = path[:-1].replace('/', ':') else: raise WebPageNotFoundError(path) path = self.notebook.pages.lookup_from_user_input(pagename) try: page = self.notebook.get_page(path) if page.hascontent: content = self.render_page(page) elif page.haschildren: content = self.render_index(page) else: raise WebPageNotFoundError(path) except PageNotFoundError: raise WebPageNotFoundError(path) except Exception as error: headerlist = [] headers = Headers(headerlist) headers.add_header('Content-Type', 'text/plain', charset='utf-8') if isinstance(error, (WWWError, FileNotFoundError)): logger.error(error.msg) if isinstance(error, FileNotFoundError): error = WebPageNotFoundError(path) # show url path instead of file path if error.headers: for key, value in error.headers: headers.add_header(key, value) start_response(error.status, headerlist) content = unicode(error).splitlines(True) # TODO also handle template errors as special here else: # Unexpected error - maybe a bug, do not expose output on bugs # to the outside world logger.exception('Unexpected error:') start_response('500 Internal Server Error', headerlist) content = ['Internal Server Error'] if environ['REQUEST_METHOD'] == 'HEAD': return [] else: return [string.encode('utf-8') for string in content] else: start_response('200 OK', headerlist) if environ['REQUEST_METHOD'] == 'HEAD': return [] elif 'utf-8' in headers['Content-Type']: return [string.encode('utf-8') for string in content] else: return content
class BeanServer(object): "A really, really simple application server." default_headers = [('Content-Type', 'text/html')] def __init__(self, ledger, opts): self.ledger = ledger self.data = [] self.load() # Map of session to dict. self.cookiejar = {} # Prototype for context object. ctx = self.ctx = Context() self.opts = ctx.opts = opts ctx.debug = opts.debug def setHeader(self, name, value): self.headers[name] = value def write(self, data): assert isinstance(data, str), data self.data.append(data) def load(self): "Load the application pages." import app reload(app) self.mapper = app.mapper def __call__(self, environ, start_response): if self.ctx.debug: self.load() self.environ = environ self.response = start_response del self.data[:] self.headers = Headers(self.default_headers) ctx = copy(self.ctx) # shallow ctx.ledger = self.ledger path = environ['PATH_INFO'] ishtml = '.' not in basename(path) or path.endswith('.html') if ishtml: # Load cookie (session is only in memory). cookie = Cookie.SimpleCookie(environ.get('HTTP_COOKIE', '')) has_cookie = (bool(cookie) and 'session' in cookie and cookie["session"].value in self.cookiejar) if has_cookie: session_id = cookie["session"].value session = self.cookiejar[session_id] else: session_id = '%x' % randint(0, 16**16) cookie["session"] = session_id session = self.cookiejar[session_id] = {} ctx.session = session try: # Linear search in the regexp to match the request path. page, vardict = self.mapper.match(path) if page is None: raise HttpNotFound(path) else: # Update the context object with components of the request and # with the query parameters. ctx.environ = environ form = cgi.parse(environ=environ) ## FIXME: make this wsgi compatible. ## conlen = int(self.environ['CONTENT_LENGTH']) ## s = self.environ['wsgi.input'].read(conlen) ## form = cgi.parse_qs(s) ctx.__dict__.update(form) ctx.__dict__.update(vardict) page(self, ctx) # Add session cookie to headers, if necessary. if ishtml and not has_cookie: for k, v in sorted(cookie.items()): self.headers.add_header('Set-Cookie', v.OutputString()) start_response('200 OK', self.headers.items()) return self.data except HttpRedirect, e: location = e.message start_response(e.status, [('Location', location)]) return [str(e)] except HttpError, e: status = getattr(e, 'status', '500 Internal Server Error') start_response(status, [('Content-Type', 'text/html')]) return [str(e)]
class Handler(object): badidre = re.compile(r"[<>\s]") def __init__(self, service, filemap, wsgienv, start_resp): self._svc = service self._fmap = filemap self._env = wsgienv self._start = start_resp self._meth = wsgienv.get('REQUEST_METHOD', 'GET') self._hdr = Headers([]) self._code = 0 self._msg = "unknown status" def send_error(self, code, message): status = "{0} {1}".format(str(code), message) self._start(status, [], sys.exc_info()) def add_header(self, name, value): # Caution: HTTP does not support Unicode characters (see # https://www.python.org/dev/peps/pep-0333/#unicode-issues); # thus, this will raise a UnicodeEncodeError if the input strings # include Unicode (char code > 255). e = "ISO-8859-1" self._hdr.add_header(name.encode(e), value.encode(e)) def set_response(self, code, message): self._code = code self._msg = message def end_headers(self): status = "{0} {1}".format(str(self._code), self._msg) ###DEBUG: log.debug("sending header: %s", str(self._hdr.items())) ###DEBUG: self._start(status, self._hdr.items()) def handle(self): meth_handler = 'do_' + self._meth path = self._env.get('PATH_INFO', '/')[1:] if hasattr(self, meth_handler): return getattr(self, meth_handler)(path) else: return self.send_error( 403, self._meth + " not supported on this resource") def do_GET(self, path): if not path: self.code = 403 self.send_error(self.code, "No identifier given") return ["Server ready\n"] if path.startswith('/'): path = path[1:] parts = path.split('/') if parts[0] == "ark:": # support full ark identifiers if len(parts) > 2 and parts[1] == NIST_ARK_NAAN: dsid = parts[2] else: dsid = '/'.join(parts[:3]) filepath = "/".join(parts[3:]) else: dsid = parts[0] filepath = "/".join(parts[1:]) if self.badidre.search(dsid): self.send_error(400, "Unsupported SIP identifier: " + dsid) return [] if filepath: return self.get_datafile(dsid, filepath) return self.get_metadata(dsid) def get_metadata(self, dsid): try: mdata = self._svc.resolve_id(dsid) except IDNotFound as ex: self.send_error(404, "Dataset with ID={0} not available".format(dsid)) return [] except SIPDirectoryNotFound as ex: # shouldn't happen self.send_error(404, "Dataset with ID={0} not available".format(dsid)) return [] except Exception as ex: log.exception("Internal error: " + str(ex)) self.send_error(500, "Internal error") return [] self.set_response(200, "Identifier found") self.add_header('Content-Type', 'application/json') self.end_headers() return [json.dumps(mdata, indent=4, separators=(',', ': '))] def get_datafile(self, id, filepath): try: loc, mtype = self._svc.locate_data_file(id, filepath) except IDNotFound as ex: self.send_error(404, "Dataset with ID={0} not available".format(id)) return [] except SIPDirectoryNotFound as ex: # shouldn't happen self.send_error(404, "Dataset with ID={0} not available".format(id)) return [] except Exception as ex: log.exception("Internal error: " + str(ex)) self.send_error(500, "Internal error") return [] if not loc: self.send_error( 404, "Dataset (ID={0}) does not contain file={1}".format( id, filepath)) xsend = None prfx = [p for p in self._fmap.keys() if loc.startswith(p + '/')] if len(prfx) > 0: xsend = self._fmap[prfx[0]] + loc[len(prfx[0]):] log.debug("Sending file via X-Accel-Redirect: %s", xsend) self.set_response(200, "Data file found") self.add_header('Content-Type', mtype) if xsend: self.add_header('X-Accel-Redirect', xsend) self.end_headers() if xsend: return [] return self.iter_file(loc) def iter_file(self, loc): # this is the backup, inefficient way to send a file with open(loc, 'rb') as fd: buf = fd.read(5000000) yield buf def do_HEAD(self, path): self.do_GET(path) return []
def __call__(self, environ, start_response): '''Main function for handling a single request. Follows the WSGI API. @param environ: dictionary with environment variables for the request and some special variables. See the PEP for expected variables. @param start_response: a function that can be called to set the http response and headers. For example:: start_response(200, [('Content-Type', 'text/plain')]) @returns: the html page content as a list of lines ''' headerlist = [] headers = Headers(headerlist) path = environ.get('PATH_INFO', '/') try: methods = ('GET', 'HEAD') if not environ['REQUEST_METHOD'] in methods: raise WWWError('405', headers=[('Allow', ', '.join(methods))]) # cleanup path #~ print 'INPUT', path path = path.replace('\\', '/') # make it windows save isdir = path.endswith('/') parts = [p for p in path.split('/') if p and not p == '.'] if [p for p in parts if p.startswith('.')]: # exclude .. and all hidden files from possible paths raise PathNotValidError() path = '/' + '/'.join(parts) if isdir and not path == '/': path += '/' #~ print 'PATH', path if not path: path = '/' elif path == '/favicon.ico': path = '/+resources/favicon.ico' else: path = urllib.unquote(path) if path == '/': headers.add_header('Content-Type', 'text/html', charset='utf-8') content = self.render_index() elif path.startswith('/+docs/'): dir = self.notebook.document_root if not dir: raise PageNotFoundError(path) file = dir.file(path[7:]) content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() elif path.startswith('/+file/'): file = self.notebook.dir.file(path[7:]) # TODO: need abstraction for getting file from top level dir ? content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() elif path.startswith('/+resources/'): if self.template.resources_dir: file = self.template.resources_dir.file(path[12:]) if not file.exists(): file = data_file('pixmaps/%s' % path[12:]) else: file = data_file('pixmaps/%s' % path[12:]) if file: content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() else: raise PageNotFoundError(path) else: # Must be a page or a namespace (html file or directory path) headers.add_header('Content-Type', 'text/html', charset='utf-8') if path.endswith('.html'): pagename = path[:-5].replace('/', ':') elif path.endswith('/'): pagename = path[:-1].replace('/', ':') else: raise PageNotFoundError(path) path = self.notebook.resolve_path(pagename) page = self.notebook.get_page(path) if page.hascontent: content = self.render_page(page) elif page.haschildren: content = self.render_index(page) else: raise PageNotFoundError(page) except Exception, error: headerlist = [] headers = Headers(headerlist) headers.add_header('Content-Type', 'text/plain', charset='utf-8') if isinstance(error, (WWWError, FileNotFoundError)): logger.error(error.msg) if isinstance(error, FileNotFoundError): error = PageNotFoundError(path) # show url path instead of file path if error.headers: for key, value in error.headers: headers.add_header(key, value) start_response(error.status, headerlist) content = unicode(error).splitlines(True) # TODO also handle template errors as special here else: # Unexpected error - maybe a bug, do not expose output on bugs # to the outside world logger.exception('Unexpected error:') start_response('500 Internal Server Error', headerlist) content = ['Internal Server Error'] if environ['REQUEST_METHOD'] == 'HEAD': return [] else: return [string.encode('utf-8') for string in content]
class SimIngestHandler(object): def __init__(self, wsgienv, start_resp): self._env = wsgienv self._start = start_resp self._meth = wsgienv.get('REQUEST_METHOD', 'GET') self._hdr = Headers([]) self._code = 0 self._msg = "unknown status" self._auth = (authmeth, authkey) def send_error(self, code, message): status = "{0} {1}".format(str(code), message) self._start(status, [], sys.exc_info()) return [] def add_header(self, name, value): self._hdr.add_header(name, value) def set_response(self, code, message): self._code = code self._msg = message def end_headers(self): status = "{0} {1}".format(str(self._code), self._msg) self._start(status, self._hdr.items()) def handle(self, env, start_resp): meth_handler = 'do_' + self._meth path = self._env.get('PATH_INFO', '/')[1:] params = cgi.parse_qs(self._env.get('QUERY_STRING', '')) print("AUTH METHOD: %s" % self._auth[0], file=sys.stderr) if not self.authorize(): return self.send_unauthorized() if hasattr(self, meth_handler): return getattr(self, meth_handler)(path, params) else: return self.send_error( 403, self._meth + " not supported on this resource") def authorize(self): if self._auth[0] == 'header': return self.authorize_via_headertoken() else: return self.authorize_via_queryparam() def authorize_via_queryparam(self): params = cgi.parse_qs(self._env.get('QUERY_STRING', '')) auths = params.get('auth', []) if self._auth[1]: # match the last value provided return len(auths) > 0 and self._auth[1] == auths[-1] if len(auths) > 0: log.warn( "Authorization key provided, but none has been configured") return len(auths) == 0 def authorize_via_headertoken(self): authhdr = self._env.get('HTTP_AUTHORIZATION', "") print("Request HTTP_AUTHORIZATION: %s" % authhdr, file=sys.stderr) parts = authhdr.split() if self._auth[1]: return len(parts) > 1 and parts[0] == "Bearer" and \ self._auth[1] == parts[1] if authhdr: log.warn( "Authorization key provided, but none has been configured") return authhdr == "" def send_unauthorized(self): self.set_response(401, "Not authorized") if self._auth[0] == 'header': self.add_header('WWW-Authenticate', 'Bearer') self.end_headers() return [] def do_GET(self, path, params=None): path = path.strip('/') if not path: try: out = json.dumps(["nerdm", "invalid"]) + '\n' except Exception, ex: return self.send_error(500, "Internal error") self.set_response(200, "Supported Record Types") self.add_header('Content-Type', 'application/json') self.end_headers() return [out] elif path in "nerdm invalid".split(): self.set_response(200, "Service is ready") self.add_header('Content-Type', 'application/json') self.end_headers() return ["Service ready\n"]
class Handler(object): def __init__(self, loaders, wsgienv, start_resp, archdir, auth=None, postexec=None): self._env = wsgienv self._start = start_resp self._meth = wsgienv.get('REQUEST_METHOD', 'GET') self._hdr = Headers([]) self._code = 0 self._msg = "unknown status" self._auth = auth self._archdir = archdir self._postexec = postexec self._loaders = loaders def send_error(self, code, message): status = "{0} {1}".format(str(code), message) self._start(status, [], sys.exc_info()) return [] def add_header(self, name, value): self._hdr.add_header(name, value) def set_response(self, code, message): self._code = code self._msg = message def end_headers(self): status = "{0} {1}".format(str(self._code), self._msg) self._start(status, list(self._hdr.items())) def handle(self): meth_handler = 'do_'+self._meth path = self._env.get('PATH_INFO', '/')[1:] if not self.authorize(): return self.send_unauthorized() if hasattr(self, meth_handler): return getattr(self, meth_handler)(path) else: return self.send_error(403, self._meth + " not supported on this resource") def authorize(self): if self._auth[0] == 'header': return self.authorize_via_headertoken() else: return self.authorize_via_queryparam() def authorize_via_queryparam(self): params = parse_qs(self._env.get('QUERY_STRING', '')) auths = params.get('auth',[]) if self._auth[1]: # match the last value provided return len(auths) > 0 and self._auth[1] == auths[-1] if len(auths) > 0: log.warning("Authorization key provided, but none has been configured") return len(auths) == 0 def authorize_via_headertoken(self): authhdr = self._env.get('HTTP_AUTHORIZATION', "") log.debug("Request HTTP_AUTHORIZATION: %s", authhdr) parts = authhdr.split() if self._auth[1]: return len(parts) > 1 and parts[0] == "Bearer" and \ self._auth[1] == parts[1] if authhdr: log.warning("Authorization key provided, but none has been configured") return authhdr == "" def send_unauthorized(self): self.set_response(401, "Not authorized") if self._auth[0] == 'header': self.add_header('WWW-Authenticate', 'Bearer') self.end_headers() return [] def do_GET(self, path): path = path.strip('/') if not path: try: out = json.dumps(list(self._loaders.keys())) + '\n' out = out.encode() except Exception as ex: log.exception("Internal error: "+str(ex)) return self.send_error(500, "Internal error") self.set_response(200, "Supported Record Types") self.add_header('Content-Type', 'application/json') self.add_header('Content-Length', str(len(out))) self.end_headers() return [out] elif path in self._loaders: self.set_response(200, "Service is ready") self.add_header('Content-Type', 'application/json') self.end_headers() return [b"Service ready\n"] else: return self.send_error(404, "resource does not exist") def do_POST(self, path): path = path.strip('/') steps = path.split('/') if len(steps) == 0: return self.send_error(405, "POST not supported on this resource") elif len(steps) == 1: if steps[0] == 'nerdm': return self.ingest_nerdm_record() else: return self.send_error(403, "new records are not allowed for " + "submission to this resource") else: return self.send_error(404, "resource does not exist") def nerdm_archive_cache(self, rec): """ cache a NERDm record into a local disk archive. The cache is for records that have been accepted but not ingested. """ try: arkid = re.sub(r'/.*$', '', re.sub(r'ark:/\d+/', '', rec['@id'])) ver = rec.get('version', '1.0.0').replace('.', '_') recid = "%s-v%s" % (os.path.basename(arkid), ver) outfile = os.path.join(self._archdir, '_cache', recid+".json") with open(outfile, 'w') as fd: json.dump(rec, fd, indent=2) return recid except KeyError as ex: # this shouldn't happen if the record was already validated raise RecordIngestError("submitted record is missing the @id "+ "property") except ValueError as ex: # this shouldn't happen if the record was already validated raise RecordIngestError("submitted record is apparently invalid; "+ "unable to submit") except OSError as ex: raise RuntimeError("Failed to cache record ({0}): {1}" .format(arkid, str(ex))) def nerdm_archive_commit(self, recid): """ commit a previously cached record to the local disk archive. This method is called after the record has been successfully ingested to the RMM's database. """ outfile = os.path.join(self._archdir, '_cache', recid+".json") if not os.path.exists(outfile): raise RuntimeError("record to commit ({0}) not found in cache: {1}" .format(recid, outfile)) try: os.rename(outfile, os.path.join(self._archdir, os.path.basename(outfile))) except OSError as ex: raise RuntimeError("Failed to archvie record ({0}): {1}" .format(recid, str(ex))) def ingest_nerdm_record(self): """ Accept a NERDm record for ingest into the RMM """ loader = self._loaders['nerdm'] try: clen = int(self._env['CONTENT_LENGTH']) except KeyError as ex: log.exception("Content-Length not provided for input record") return self.send_error(411, "Content-Length is required") except ValueError as ex: log.exception("Failed to parse input JSON record: "+str(e)) return self.send_error(400, "Content-Length is not an integer") try: bodyin = self._env['wsgi.input'] doc = bodyin.read(clen) rec = json.loads(doc) except Exception as ex: log.exception("Failed to parse input JSON record: "+str(ex)) log.warning("Input document starts...\n{0}...\n...{1} ({2}/{3} chars)" .format(doc[:75], doc[-20:], len(doc), clen)) return self.send_error(400, "Failed to load input record (bad format?): "+ str(ex)) try: recid = self.nerdm_archive_cache(rec) res = loader.load(rec, validate=True) if res.failure_count > 0: res = res.failures()[0] logmsg = "Failed to load record with "+str(res.key) for e in res.errs: logmsg += "\n "+str(e) log.error(logmsg) self.set_response(400, "Input record is not valid") self.add_header('Content-Type', 'application/json') self.end_headers() out = json.dumps([str(e) for e in res.errs]) + '\n' return [ out.encode() ] except RecordIngestError as ex: log.exception("Failed to load posted record: "+str(ex)) self.set_response(400, "Input record is not valid (missing @id)") self.add_header('Content-Type', 'application/json') self.end_headers() out = json.dumps([ "Record is missing @id property" ]) + '\n' return [ out.encode() ] except Exception as ex: log.exception("Loading error: "+str(ex)) return self.send_error(500, "Load failure due to internal error") try: self.nerdm_archive_commit(recid) except Exception as ex: log.exception("Commit error: "+str(ex)) if self._postexec: # run post-commit script try: self.nerdm_post_commit(recid) except Exception as ex: log.exception("Post-commit error: "+str(ex)) log.info("Accepted record %s with @id=%s", rec.get('ediid','?'), rec.get('@id','?')) self.set_response(200, "Record accepted") self.end_headers() return [] def nerdm_post_commit(self, recid): """ run an external executable for further processing after the record is commited to the database (e.g. update an external index) """ cmd = _mkpostcomm(self._postexec, recid) try: log.debug("Executing post-commit script:\n %s", " ".join(cmd)) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() if p.returncode != 0: log.error("Error occurred while running post-commit script:\n"+(err or out)) except OSError as ex: log.error("Failed to execute post-commit script:\n %s\n%s", " ".join(cmd), str(ex)) except Exception as ex: log.error("Unexpected failure executing post-commit script:\n %s\n%s", " ".join(cmd), str(ex))
class application(object): # don't serve static by default static_serve = False static_alias = {'': 'ketcher.html'} static_root = None indigo = None indigo_inchi = None def __init__(self, environ, start_response): self.path = environ['PATH_INFO'].strip('/') self.method = environ['REQUEST_METHOD'] self.content_type = environ.get('CONTENT_TYPE', '') self.fields = FieldStorage(fp=environ['wsgi.input'], environ=environ, keep_blank_values=True) self.FileWrapper = environ.get('wsgi.file_wrapper', FileWrapper) self.headers = Headers([]) if self.static_serve: self.headers['Access-Control-Allow-Origin'] = '*' route = getattr(self, 'on_' + self.path, None) if route is None: route = self.serve_static if self.method == 'GET' and \ self.static_serve else self.notsupported status = "200 OK" try: self.response = route() except self.HttpException as e: status = e.args[0] self.response = [e.args[1]] self.headers.setdefault('Content-Type', 'text/plain') start_response(status, self.headers.items()) def __iter__(self): for chunk in self.response: yield chunk if sys.version_info.major < 3 or \ not hasattr(chunk, 'encode') else chunk.encode() def notsupported(self): raise self.HttpException("405 Method Not Allowed", "Request not supported") def indigo_required(method): def wrapper(self, **args): if not self.indigo: raise self.HttpException("501 Not Implemented", "Indigo libraries are not found") try: return method(self, **args) except indigo.IndigoException as e: message = str(sys.exc_info()[1]) if 'indigoLoad' in message: # error on load message = "Cannot load the specified " + \ "structure: %s " % str(e) raise self.HttpException("400 Bad Request", message) return wrapper @indigo_required def on_knocknock(self): return ["You are welcome!"] @indigo_required def on_layout(self): moldata = None if self.method == 'GET' and 'smiles' in self.fields: moldata = self.fields.getfirst('smiles') elif self.is_form_request() and 'moldata' in self.fields: moldata = self.fields.getfirst('moldata') selective = 'selective' in self.fields if moldata: if '>>' in moldata or moldata.startswith('$RXN'): rxn = self.indigo.loadQueryReaction(moldata) if selective: for mol in rxn.iterateMolecules(): self.selective_layout(mol) else: rxn.layout() return ["Ok.\n", rxn.rxnfile()] elif moldata.startswith('InChI'): mol = self.indigo_inchi.loadMolecule(moldata) mol.layout() return ["Ok.\n", mol.molfile()] else: mol = self.indigo.loadQueryMolecule(moldata) if selective: for rg in mol.iterateRGroups(): for frag in rg.iterateRGroupFragments(): self.selective_layout(frag) self.selective_layout(mol) else: mol.layout() return ["Ok.\n", mol.molfile()] self.notsupported() @indigo_required def on_smiles(self): moldata = self.fields.getfirst('moldata') if moldata: if '>>' in moldata or moldata.startswith('$RXN'): rxn = self.indigo.loadQueryReaction(moldata) return ["Ok.\n", rxn.smiles()] elif moldata.startswith('InChI'): mol = self.indigo_inchi.loadMolecule(moldata) return ["Ok.\n", mol.smiles()] else: mol = self.indigo.loadQueryMolecule(moldata) return ["Ok.\n", mol.smiles()] self.notsupported() @indigo_required def on_getinchi(self): md, is_rxn = self.load_moldata() inchi = self.indigo_inchi.getInchi(md) return ["Ok.\n", inchi] @indigo_required def on_getmolfile(self): md, is_rxn = self.load_moldata() return ["Ok.\n", md.molfile()] @indigo_required def on_automap(self): moldata = None if self.method == 'GET' and 'smiles' in self.fields: moldata = self.fields.getfirst('smiles') elif self.is_form_request() and 'moldata' in self.fields: moldata = self.fields.getfirst('moldata') if moldata: mode = self.fields.getfirst('mode', 'discard') rxn = self.indigo.loadQueryReaction(moldata) if not moldata.startswith('$RXN'): rxn.layout() rxn.automap(mode) return ["Ok.\n", rxn.rxnfile()] self.notsupported() @indigo_required def on_aromatize(self): try: md, is_rxn = self.load_moldata() except: message = str(sys.exc_info()[1]) if message.startswith("\"molfile loader:") and \ message.endswith("queries\""): # hack to avoid user confusion md, is_rxn = self.load_moldata(True) else: raise md.aromatize() return ["Ok.\n", md.rxnfile() if is_rxn else md.molfile()] @indigo_required def on_dearomatize(self): try: md, is_rxn = self.load_moldata() except: # TODO: test for query features presence raise self.HttpException("400 Bad Request", "Molecules and reactions " + \ "containing query features " + \ "cannot be dearomatized yet.") md.dearomatize() return ["Ok.\n", md.rxnfile() if is_rxn else md.molfile()] @indigo_required def on_calculate_cip(self): application.indigo.setOption('molfile-saving-add-stereo-desc', True) try: md, is_rxn = self.load_moldata() except: message = str(sys.exc_info()[1]) if message.startswith("\"molfile loader:") and \ message.endswith("queries\""): # hack to avoid user confusion md, is_rxn = self.load_moldata(True) else: raise result = md.rxnfile() if is_rxn else md.molfile() application.indigo.setOption('molfile-saving-add-stereo-desc', False) return ["Ok.\n", result] def on_open(self): if self.is_form_request(): self.headers.add_header('Content-Type', 'text/html') return [ '<html><body onload="parent.ui.loadMoleculeFromFile()" title="', b64encode("Ok.\n"), b64encode(self.fields.getfirst('filedata')), '"></body></html>' ] self.notsupported() def on_save(self): if self.is_form_request(): type, data = self.fields.getfirst('filedata').split('\n', 1) type = type.strip() if type == 'smi': self.headers.add_header('Content-Type', 'chemical/x-daylight-smiles') elif type == 'mol': if data.startswith('$RXN'): type = 'rxn' self.headers.add_header('Content-Type', 'chemical/x-mdl-%sfile' % type) self.headers.add_header('Content-Length', str(len(data))) self.headers.add_header('Content-Disposition', 'attachment', filename='ketcher.%s' % type) return [data] self.notsupported() class HttpException(Exception): pass def load_moldata(self, is_query=False): moldata = self.fields.getfirst('moldata') is_rxn = False if moldata.startswith('$RXN'): if is_query: md = self.indigo.loadQueryReaction(moldata) else: md = self.indigo.loadReaction(moldata) is_rxn = True elif moldata.startswith('InChI'): md = self.indigo_inchi.loadMolecule(moldata) md.layout() else: if is_query: md = self.indigo.loadQueryMolecule(moldata) else: md = self.indigo.loadMolecule(moldata) return md, is_rxn def selective_layout(self, mol): dsgs = [dsg for dsg in mol.iterateDataSGroups() \ if dsg.description() == '_ketcher_selective_layout' and \ dsg.data() == '1'] atoms = sorted([atom.index() for dsg in dsgs \ for atom in dsg.iterateAtoms()]) for dsg in dsgs: dsg.remove() mol.getSubmolecule(atoms).layout() return mol def serve_static(self): root = path.realpath(self.static_root or getcwd()) fpath = self.static_alias.get(self.path, self.path) fpath = path.realpath(path.join(root, fpath)) if not fpath.startswith(root + path.sep) or not path.isfile(fpath) \ or fpath == path.realpath(__file__): raise self.HttpException("404 Not Found", "Requested file isn't accessible") self.headers['Content-Type'] = guess_type(fpath)[0] or 'text/plain' try: fd = open(fpath, 'rb') return self.FileWrapper(fd) if self.method == 'GET' else [''] except (IOError, OSError): raise self.HttpException( "402 Payment Required", # or 403, hmm.. "Must get more money for overtime") def is_form_request(self): return self.method == 'POST' and \ (self.content_type.startswith('application/x-www-form-urlencoded') or self.content_type.startswith('multipart/form-data'))
class SimDistribHandler(object): def __init__(self, archive, wsgienv, start_resp): self.arch = archive self._env = wsgienv self._start = start_resp self._meth = wsgienv.get('REQUEST_METHOD', 'GET') self._hdr = Headers([]) self._code = 0 self._msg = "unknown status" def send_error(self, code, message): status = "{0} {1}".format(str(code), message) self._start(status, [], sys.exc_info()) return [] def add_header(self, name, value): self._hdr.add_header(name, value) def set_response(self, code, message): self._code = code self._msg = message def end_headers(self): status = "{0} {1}".format(str(self._code), self._msg) self._start(status, self._hdr.items()) def handle(self, env, start_resp): meth_handler = 'do_' + self._meth path = self._env.get('PATH_INFO', '/')[1:] params = cgi.parse_qs(self._env.get('QUERY_STRING', '')) if hasattr(self, meth_handler): return getattr(self, meth_handler)(path, params) else: return self.send_error( 403, self._meth + " not supported on this resource") def do_HEAD(self, path, params=None, forhead=False): return self.do_GET(path, params, True) def do_GET(self, path, params=None, forhead=False): aid = None vers = None path = path.strip('/') if path.startswith("od/ds/"): path = path[len("od/ds/"):] print("processing " + path) # refresh the archive self.arch.loadinfo() if not path: try: out = json.dumps(self.arch.aipids) + '\n' except Exception as ex: return self.send_error(500, "Internal error") self.set_response(200, "AIP Identifiers") self.add_header('Content-Type', 'application/json') self.add_header('Content-Length', str(len(out))) self.end_headers() if forhead: return [] return [out] elif path.startswith("_aip/"): # requesting a bag file path = path[len("_aip/"):].strip('/') filepath = os.path.join(self.arch.dir, path) if os.path.isfile(filepath): self.set_response(200, "Bag file found") self.add_header('Content-Type', "application/zip") self.end_headers() if forhead: return [] return self.iter_file(filepath) else: return self.send_error(404, "bag file does not exist") elif '/' in path: parts = path.split('/', 1) aid = parts[0] path = (len(parts) > 1 and parts[1]) or '' print("accessing " + aid) elif path: aid = path path = '' else: return self.send_error(404, "resource does not exist") # path-info is now captured as aid and path if aid not in self.arch._aips: return self.send_error(404, "resource does not exist") if not path: self.set_response(200, "AIP Identifier exists") self.add_header('Content-Type', 'application/json') self.add_header('Content-Length', str(len(aid) + 4)) self.end_headers() if forhead: return [] return ['["' + aid + '"]'] elif path == "_aip": try: out = json.dumps(self.arch.list_bags(aid)) + '\n' except Exception, ex: return self.send_error(500, "Internal error") self.set_response(200, "All bags for ID") self.add_header('Content-Type', 'application/json') self.add_header('Content-Length', str(len(out))) self.end_headers() if forhead: return [] return [out] elif path == "_aip/_head": try: out = self.arch.head_for(aid) if out: out = json.dumps(out) + '\n' except Exception, ex: print( "Failed to create JSON output for head bag, aid={0}: {2}". format(aid, vers, str(ex))) return self.send_error(500, "Internal error")
class Response: default_status = 200 default_content_type = 'text/html; charset=UTF-8' def __init__(self, body: str='', status: int=None, headers: Dict=None, **more_headers) -> None: self.headers = Headers() self.body = body self._status_code = status or self.default_status self._cookies = SimpleCookie() # type: ignore if headers: for name, value in headers.items(): self.headers.add_header(name, value) if more_headers: for name, value in more_headers.items(): self.headers.add_header(name, value) @property def status_code(self): """ The HTTP status code as an integer (e.g. 404).""" return self._status_code @property def status(self): """ The HTTP status line as a string (e.g. ``404 Not Found``).""" if not 100 <= self._status_code <= 999: raise ValueError('Status code out of range.') status = _HTTP_STATUS_LINES.get(self._status_code) return str(status or ('{} Unknown'.format(self._status_code))) @status.setter def status(self, status_code: int): if not 100 <= status_code <= 999: raise ValueError('Status code out of range.') self._status_code = status_code @property def headerlist(self) -> List[Tuple[str, str]]: """ WSGI conform list of (header, value) tuples. """ out = [] # type: List[Tuple[str, str]] if 'Content-Type' not in self.headers: self.headers.add_header('Content-Type', self.default_content_type) out += [(key, value) for key in self.headers.keys() for value in self.headers.get_all(key)] if self._cookies: for c in self._cookies.values(): out.append(('Set-Cookie', c.OutputString())) return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] def set_cookie(self, key: str, value: Any, expires: str=None, path: str=None, **options: Dict[str, Any]) -> None: from datetime import timedelta, datetime, date import time self._cookies[key] = value if expires: self._cookies[key]['expires'] = expires if path: self._cookies[key]['path'] = path for k, v in options.items(): if k == 'max_age': if isinstance(v, timedelta): v = v.seconds + v.days * 24 * 3600 # type: ignore if k == 'expires': if isinstance(v, (date, datetime)): v = v.timetuple() # type: ignore elif isinstance(v, (int, float)): v = v.gmtime(value) # type: ignore v = time.strftime("%a, %d %b %Y %H:%M:%S GMT", v) # type: ignore self._cookies[key][k.replace('_', '-')] = v # type: ignore def delete_cookie(self, key, **kwargs) -> None: kwargs['max_age'] = -1 kwargs['expires'] = 0 self.set_cookie(key, '', **kwargs) def apply(self, other): self.status = other._status_code self._cookies = other._cookies self.headers = other.headers self.body = other.body
def __call__(self, environ, start_response): '''Main function for handling a single request. Arguments are the file handle to write the output to and the path to serve. Any exceptions will result in a error response being written. First argument is a dictionary with environment variables and some special variables. See the PEP for expected variables. The second argument is a function that can be called for example like: start_response(200, [('Content-Type', 'text/plain')]) This method is supposed to take care of sending the response line and the headers. The return value of this call is a list of lines with the content to be served. ''' headerlist = [] headers = Headers(headerlist) path = environ.get('PATH_INFO', '/') try: methods = ('GET', 'HEAD') if not environ['REQUEST_METHOD'] in methods: raise WWWError('405', headers=[('Allow', ', '.join(methods))]) # TODO clean up path from any '../' (and ..\) if not path: path = '/' elif path == '/favicon.ico': path = '/+icons/favicon.ico' elif path in icons: # TODO FIXME HACK - this translation needs to be done when exporting path = '/+icons/' + icons[path] if self.notebook is None: raise NoConfigError elif path == '/': headers.add_header('Content-Type', 'text/html', charset='utf-8') content = self.render_index() elif path.startswith('/+docs/'): pass # TODO document root elif path.startswith('/+file/'): pass # TODO attachment or raw source elif path.startswith('/+icons/'): # TODO check if favicon is overridden or something file = data_file('pixmaps/%s' % path[8:]) if path.endswith('.png'): headers['Content-Type'] = 'image/png' elif path.endswith('.ico'): headers['Content-Type'] = 'image/vnd.microsoft.icon' content = [file.read(encoding=None)] else: # Must be a page or a namespace (html file or directory path) headers.add_header('Content-Type', 'text/html', charset='utf-8') if path.endswith('.html'): pagename = path[:-5].replace('/', ':') elif path.endswith('/'): pagename = path[:-1].replace('/', ':') else: raise PageNotFoundError(path) pagename = urllib.unquote(pagename) path = self.notebook.resolve_path(pagename) page = self.notebook.get_page(path) if page.hascontent: content = self.render_page(page) elif page.haschildren: content = self.render_index(page) else: raise PageNotFoundError(page) except Exception, error: headerlist = [] headers = Headers(headerlist) headers.add_header('Content-Type', 'text/plain', charset='utf-8') if isinstance(error, WWWError): logger.error(error.msg) if error.headers: header.extend(error.headers) start_response(error.status, headerlist) content = unicode(error).splitlines(True) # TODO also handle template errors as special here else: # Unexpected error - maybe a bug, do not expose output on bugs # to the outside world logger.exception('Unexpected error:') start_response('500 Internal Server Error', headerlist) content = ['Internal Server Error'] if environ['REQUEST_METHOD'] == 'HEAD': return [] else: return [string.encode('utf-8') for string in content]
def make_headers(self, headers): h = Headers([("Allow", "GET, HEAD")]) for item in headers: h.add_header(item[0], item[1]) return h.items()
def __call__(self, environ, start_response): '''Main function for handling a single request. Follows the WSGI API. @param environ: dictionary with environment variables for the request and some special variables. See the PEP for expected variables. @param start_response: a function that can be called to set the http response and headers. For example:: start_response(200, [('Content-Type', 'text/plain')]) @returns: the html page content as a list of lines ''' if self.auth_creds: import base64 def bad_auth(): body = 'Please authenticate' realm = 'zimAuth' logger.info('Requesting Basic HTTP-Authentication') headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(body))), ('WWW-Authenticate', 'Basic realm="%s"' % realm)] start_response('401 Unauthorized', headers) return [body.encode()] auth = environ.get('HTTP_AUTHORIZATION') if auth: scheme, data = auth.split(None, 1) assert scheme.lower() == 'basic' username, password = base64.b64decode(data).decode( 'UTF-8').split(':') if username != self.auth_creds[ 0] or password != self.auth_creds[1]: return bad_auth() environ['REMOTE_USER'] = username del environ['HTTP_AUTHORIZATION'] else: return bad_auth() headerlist = [] headers = Headers(headerlist) path = environ.get('PATH_INFO', '/') path = path.encode('iso-8859-1').decode('UTF-8') # The WSGI standard mandates iso-8859-1, but we want UTF-8. See: # - https://www.python.org/dev/peps/pep-3333/#unicode-issues # - https://code.djangoproject.com/ticket/19468 try: methods = ('GET', 'HEAD') if not environ['REQUEST_METHOD'] in methods: raise WWWError('405', headers=[('Allow', ', '.join(methods))]) # cleanup path path = path.replace('\\', '/') # make it windows save isdir = path.endswith('/') parts = [p for p in path.split('/') if p and not p == '.'] if [p for p in parts if p.startswith('.')]: # exclude .. and all hidden files from possible paths raise WebPathNotValidError() path = '/' + '/'.join(parts) if isdir and not path == '/': path += '/' if not path: path = '/' elif path == '/favicon.ico': path = '/+resources/favicon.ico' else: path = urllib.parse.unquote(path) if path == '/': headers.add_header('Content-Type', 'text/html', charset='utf-8') content = self.render_index() elif path.startswith('/+docs/'): dir = self.notebook.document_root if not dir: raise WebPageNotFoundError(path) file = dir.file(path[7:]) content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() elif path.startswith('/+file/'): file = self.notebook.folder.file(path[7:]) # TODO: need abstraction for getting file from top level dir ? content = [file.read_binary()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.mimetype() elif path.startswith('/+resources/'): if self.template.resources_dir: file = self.template.resources_dir.file(path[12:]) if not file.exists(): file = data_file('pixmaps/%s' % path[12:]) else: file = data_file('pixmaps/%s' % path[12:]) if file: content = [file.raw()] # Will raise FileNotFound when file does not exist headers['Content-Type'] = file.get_mimetype() else: raise WebPageNotFoundError(path) else: # Must be a page or a namespace (html file or directory path) headers.add_header('Content-Type', 'text/html', charset='utf-8') if path.endswith('.html'): pagename = path[:-5].replace('/', ':') elif path.endswith('/'): pagename = path[:-1].replace('/', ':') else: raise WebPageNotFoundError(path) path = self.notebook.pages.lookup_from_user_input(pagename) try: page = self.notebook.get_page(path) if page.hascontent: content = self.render_page(page) elif page.haschildren: content = self.render_index(page) else: raise WebPageNotFoundError(path) except PageNotFoundError: raise WebPageNotFoundError(path) except Exception as error: headerlist = [] headers = Headers(headerlist) headers.add_header('Content-Type', 'text/plain', charset='utf-8') if isinstance(error, (WWWError, FileNotFoundError)): logger.error(error.msg) if isinstance(error, FileNotFoundError): error = WebPageNotFoundError(path) # show url path instead of file path if error.headers: for key, value in error.headers: headers.add_header(key, value) start_response(error.status, headerlist) content = str(error).splitlines(True) # TODO also handle template errors as special here else: # Unexpected error - maybe a bug, do not expose output on bugs # to the outside world logger.exception('Unexpected error:') start_response('500 Internal Server Error', headerlist) content = ['Internal Server Error'] if environ['REQUEST_METHOD'] == 'HEAD': return [] else: return [c.encode('UTF-8') for c in content] else: start_response('200 OK', headerlist) if environ['REQUEST_METHOD'] == 'HEAD': return [] elif content and isinstance(content[0], str): return [c.encode('UTF-8') for c in content] else: return content
def static_file_view(env, start_response, filename, block_size, charset, CACHE_DURATION): method = env['REQUEST_METHOD'].upper() if method not in ('HEAD', 'GET'): start_response('405 METHOD NOT ALLOWED', [('Content-Type', 'text/plain; UTF-8')]) return [b''] mimetype, encoding = mimetypes.guess_type(filename) headers = Headers([]) cache_days = CACHE_DURATION.get(mimetype, 0) expires = datetime.datetime.utcnow() + datetime.timedelta(cache_days) headers.add_header('Cache-control', f'public, max-age={expires.strftime(RFC_1123_DATE)}') headers.add_header('Expires', expires.strftime(RFC_1123_DATE)) if env.get('HTTP_IF_MODIFIED_SINCE'): if env.get('HTTP_IF_MODIFIED_SINCE') >= generate_last_modified(filename): start_response('304 ok', headers.items()) return [b'304'] headers.add_header('Content-Encodings', encoding) if mimetype: headers.add_header('Content-Type', get_content_type(mimetype, charset)) headers.add_header('Content-Length', get_content_length(filename)) headers.add_header('Last-Modified', generate_last_modified(filename)) headers.add_header("Accept-Ranges", "bytes") start_response('200 OK', headers.items()) return _get_body(filename, method, block_size, charset)
class SimRMMHandler(object): def __init__(self, archive, wsgienv, start_resp): self.arch = archive self._env = wsgienv self._start = start_resp self._meth = wsgienv.get('REQUEST_METHOD', 'GET') self._hdr = Headers([]) self._code = 0 self._msg = "unknown status" def send_error(self, code, message): status = "{0} {1}".format(str(code), message) self._start(status, [], sys.exc_info()) return [] def add_header(self, name, value): self._hdr.add_header(name, value) def set_response(self, code, message): self._code = code self._msg = message def end_headers(self): status = "{0} {1}".format(str(self._code), self._msg) self._start(status, self._hdr.items()) def handle(self, env, start_resp): meth_handler = 'do_'+self._meth path = self._env.get('PATH_INFO', '/')[1:] params = cgi.parse_qs(self._env.get('QUERY_STRING', '')) if hasattr(self, meth_handler): return getattr(self, meth_handler)(path, params) else: return self.send_error(403, self._meth + " not supported on this resource") def do_GET(self, path, params=None): if path: path = path.rstrip('/') if path.startswith("records"): path = path[len("records"):].lstrip('/') id = None print("path="+str(path)+"; params="+str(params)) if not path and params and "@id" in params: path = params["@id"] path = (len(path) > 0 and path[0]) or '' if path: if path.startswith("ark:/88434/"): id = path[len("ark:/88434/"):] else: self.arch.loadlu() id = self.arch.ediid_to_id(path) if id: mdfile = os.path.join(self.arch.dir, id+".json") if not id or not os.path.exists(mdfile): if not id: id = "resource" return self.send_error(404, id + " does not exist") try: with open(mdfile) as fd: data = json.load(fd, object_pairs_hook=OrderedDict) data["_id"] ={"timestamp":1521220572,"machineIdentifier":3325465} if params and "@id" in params: data = { "ResultCount": 1, "PageSize": 0, "ResultData": [ data ] } except Exception as ex: print(str(ex)) return self.send_error(500, "Internal error") self.set_response(200, "Identifier exists") self.add_header('Content-Type', 'application/json') self.end_headers() return [ json.dumps(data, indent=2) + "\n" ]
class BaseResponse: """Base class for Response.""" default_status = 200 default_content_type = 'text/plain;' def __init__(self, body=None, status=None, headers=None): self._body = body if body else [b''] self._status_code = status or self.default_status self.headers = Headers() self._cookies = SimpleCookie() if headers: for name, value in headers.items(): self.headers.add_header(name, value) @property def body(self): return self._body @property def status_code(self): """ The HTTP status code as an integer (e.g. 404).""" return self._status_code @property def status(self): """ The HTTP status line as a string (e.g. ``404 Not Found``).""" status = _HTTP_STATUS_LINES.get(self._status_code) return str(status or ('{} Unknown'.format(self._status_code))) @status.setter def status(self, status_code): if not 100 <= status_code <= 999: raise ValueError('Status code out of range.') self._status_code = status_code @property def headerlist(self): """ WSGI conform list of (header, value) tuples. """ if 'Content-Type' not in self.headers: self.headers.add_header('Content-Type', self.default_content_type) if self._cookies: for c in self._cookies.values(): self.headers.add_header('Set-Cookie', c.OutputString()) return self.headers.items() def set_cookie(self, key, value, expires=None, max_age=None, path='/', secret=None, digestmod=hashlib.sha256): from kobin.app import current_config if secret is None: secret = current_config('SECRET_KEY') if secret: if isinstance(secret, str): secret = secret.encode('utf-8') encoded = base64.b64encode( pickle.dumps((key, value), pickle.HIGHEST_PROTOCOL)) sig = base64.b64encode( hmac.new(secret, encoded, digestmod=digestmod).digest()) value_bytes = b'!' + sig + b'?' + encoded value = value_bytes.decode('utf-8') self._cookies[key] = value if len(key) + len(value) > 3800: raise ValueError('Content does not fit into a cookie.') if max_age is not None: if isinstance(max_age, int): max_age_value = max_age else: max_age_value = max_age.seconds + max_age.days * 24 * 3600 self._cookies[key]['max-age'] = max_age_value if expires is not None: if isinstance(expires, int): expires_value = expires else: expires_value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", expires.timetuple()) self._cookies[key]['expires'] = expires_value if path: self._cookies[key]['path'] = path def delete_cookie(self, key, **kwargs): kwargs['max_age'] = -1 kwargs['expires'] = 0 self.set_cookie(key, '', **kwargs)
class Response(threading.local): def setup(self): self.status = http_status['200'] self.headers = Headers([('Content-type','text/html; charset=UTF-8')]) self.out = b'' def download(self, dir, args, cd=False): ims = request.env.get('HTTP_IF_MODIFIED_SINCE', '') file = os.path.join(dir, *args) if not os.access(file, os.R_OK): self.error(mesg='File not found',raise_exc=True) mimetype, encoding = mimetypes.guess_type(file) if mimetype: self.headers.add_header('Content-Type',mimetype) if encoding: self.headers.add_header('Content-Encoding',encoding) if cd: self.headers.add_header('Content-Disposition','attachment',filename=args[-1]) stats = os.stat(file) self.headers.add_header('Content-Length', str(stats.st_size)) time_fmt = "%a, %d %b %Y %H:%M:%S GMT" last_modified = strftime(time_fmt, gmtime(stats.st_mtime)) self.headers.add_header('Last-Modified', last_modified) if ims: ims = strptime(ims.split(";")[0].strip(), time_fmt) else: ims = None if ims is not None and ims >= gmtime(stats.st_mtime-2): date = strftime(time_fmt, gmtime()) self.headers.add_header('Date', date) self.status = http_status['304'] elif request.method == 'HEAD': self.out = '' else: self.out = open(file,'rb').read() def error(self, mesg='', status='404', raise_exc=True): self.status = http_status[status] self.out = mesg self.headers = Headers([('Content-type','text/plain')]) if raise_exc: raise Exception('ResponseError NotFound') def output(self): self.headers.add_header('Content-Length', str(len(self.out))) if type(self.out)==str: return self.out.encode('utf-8') return self.out
class Handler(object): badidre = re.compile(r"[<>\s]") def __init__(self, service, siptype, wsgienv, start_resp, auth=None): self._svc = service self._env = wsgienv self._start = start_resp self._meth = wsgienv.get('REQUEST_METHOD', 'GET') self._hdr = Headers([]) self._code = 0 self._msg = "unknown status" self._auth = auth def send_error(self, code, message): stat = "{0} {1}".format(str(code), message) self._start(stat, [], sys.exc_info()) def add_header(self, name, value): # Caution: HTTP does not support Unicode characters (see # https://www.python.org/dev/peps/pep-0333/#unicode-issues); # thus, this will raise a UnicodeEncodeError if the input strings # include Unicode (char code > 255). e = "ISO-8859-1" self._hdr.add_header(name.encode(e), value.encode(e)) def set_response(self, code, message): self._code = code self._msg = message def end_headers(self): stat = "{0} {1}".format(str(self._code), self._msg) self._start(stat, self._hdr.items()) def handle(self): meth_handler = 'do_' + self._meth path = self._env.get('PATH_INFO', '/').strip('/') if not self.authorize(): return self.send_unauthorized() if hasattr(self, meth_handler): out = getattr(self, meth_handler)(path) if isinstance(out, list) and len(out) > 0: out.append('\n') return out else: return self.send_error( 403, self._meth + " not supported on this resource") def authorize(self): if self._auth[0] == 'header': return self.authorize_via_headertoken() else: return self.authorize_via_queryparam() def authorize_via_queryparam(self): params = cgi.parse_qs(self._env.get('QUERY_STRING', '')) auths = params.get('auth', []) if self._auth[1]: # match the last value provided return len(auths) > 0 and self._auth[1] == auths[-1] if len(auths) > 0: log.warn( "Authorization key provided, but none has been configured") return len(auths) == 0 def authorize_via_headertoken(self): authhdr = self._env.get('HTTP_AUTHORIZATION', "") parts = authhdr.split() if self._auth[1]: return len(parts) > 1 and parts[0] == "Bearer" and \ self._auth[1] == parts[1] if authhdr: log.warn( "Authorization key provided, but none has been configured") return authhdr == "" def send_unauthorized(self): self.set_response(401, "Not authorized") if self._auth[0] == 'header': self.add_header('WWW-Authenticate', 'Bearer') self.end_headers() return [] def do_GET(self, path): # return the status on request or a list of previous requests steps = path.split('/') if steps[0] == '': try: out = json.dumps(['midas']) except Exception, ex: log.exception("Internal error: " + str(ex)) self.send_error(500, "Internal error") return ["[]"] self.set_response(200, "Supported SIP Types") self.add_header('Content-Type', 'application/json') self.end_headers() return [out] elif steps[0] == 'midas': if len(steps) > 2: path = '/'.join(steps[1:]) self.send_error(400, "Unsupported SIP identifier: " + path) return [] elif len(steps) > 1: if steps[1].startswith("_") or steps[1].startswith(".") or \ self.badidre.search(steps[1]): self.send_error(400, "Unsupported SIP identifier: " + path) return [] return self.request_status(steps[1]) else: return self.requests()
class SimHandler(object): def __init__(self, repo, basepath, prefixes, wsgienv, start_resp): self.repo = repo self.basepath = basepath self.prefs = prefixes self._env = wsgienv self._start = start_resp self._meth = wsgienv.get("REQUEST_METHOD", "GET") self._hdr = Headers([]) self._code = 0 self._msg = "unknown state" def send_error(self, code, message, errtitle=None, errdesc={}, tellexc=False): edata = None if errdesc and not errtitle: errtitle = message if errtitle: edata = {"errors": [{"title": errtitle, "status": code}]} if errdesc: edata['errors'][0].update(errdesc) if edata: edata = json.dumps(edata) self.add_header("Content-type", JSONAPI_MT) self.add_header("Content-length", len(edata)) status = "{0} {1}".format(str(code), message) excinfo = None if tellexc: excinfo = sys.exc_info() if excinfo == (None, None, None): excinfo = None self._start(status, self._hdr.items(), excinfo) if edata: return [edata.encode()] return [] def add_header(self, name, value): self._hdr.add_header(name, str(value)) def set_response(self, code, message): self._code = code self._msg = message def end_headers(self): status = "{0} {1}".format(str(self._code), self._msg) self._start(status, self._hdr.items()) def handle(self): meth_handler = 'do_' + self._meth path = self._env.get('PATH_INFO', '/') params = parse_qs(self._env.get('QUERY_STRING', '')) if path.startswith(self.basepath): path = path[len(self.basepath):] else: return self.send_error(404, "Unsupported service") if hasattr(self, meth_handler): return getattr(self, meth_handler)(path, params) else: return self.send_error( 405, self._meth + " not supported on this resource") _envelope = OrderedDict([("data", OrderedDict([("type", "dois")]))]) def _new_resp(self, id=None, attrs=None): out = deepcopy(self._envelope) if id: out['data']['id'] = id if attrs is not None: out['data']['attributes'] = attrs return out def do_GET(self, path, params=None): if path: path = path.strip('/') else: return self.send_error(200, "Ready") if 'HTTP_ACCEPT' in self._env and self._env[ 'HTTP_ACCEPT'] != JSONAPI_MT: return self.send_error(406, "Not Acceptable", "Unsupported Accept value", {"detail": self._env['HTTP_ACCEPT']}) try: out = self._new_resp(path, self.repo.describe(path)) except ValueError as ex: return self.send_error(404, str(ex), "ID not found", errdesc={"detail": "path"}) try: out = json.dumps(out) self.set_response(200, "Found") self.add_header("Content-type", JSONAPI_MT) self.add_header("Content-length", len(out)) self.end_headers() return [out.encode()] except (ValueError, TypeError) as ex: return self.send_error(500, "JSON encoding error", errdesc={"detail": str(ex)}, tellexc=True) def do_HEAD(self, path, params=None): if path: path = path.strip('/') else: return self.send_error(200, "Ready") if path in self.repo.ids: return self.send_error(200, "ID Found") else: return self.send_error(404, "ID Not Found") def do_POST(self, path, params=None): if path: path = path.strip('/') if path: return self.send_error(405, "Cannot POST to ID", errdesc={"detail": path}) if 'HTTP_ACCEPT' in self._env and self._env[ 'HTTP_ACCEPT'] != JSONAPI_MT: return self.send_error(406, "Not Acceptable", "Unsupported Accept value", {"detail": "self._env['HTTP_ACCEPT']"}) if 'CONTENT_TYPE' in self._env and self._env[ 'CONTENT_TYPE'] != JSONAPI_MT: return self.send_error(415, "Wrong Input Type", "Unsupported input content type", {"detail": self._env['CONTENT_TYPE']}) try: bodyin = self._env['wsgi.input'].read().decode('utf-8') doc = json.loads(bodyin, object_pairs_hook=OrderedDict) except (ValueError, TypeError) as ex: return self.send_error(400, "Not JSON", "Failed to parse input as JSON", {"detail": str(ex)}) doi = None event = None try: if doc['data']['type'] != "dois": return self.send_error( 400, "Wrong input data type", { "detail": doc['data']['type'], "source": { "pointer": "/data/type" } }) prefix = None if 'doi' in doc['data']['attributes']: doi = doc['data']['attributes']['doi'] parts = doi.split('/', 1) if len(parts) < 2: return self.send_error(400, "Bad doi syntax", errdesc={"detail": doi}) prefix = parts[0] elif 'prefix' in doc['data']['attributes']: prefix = doc['data']['attributes']['prefix'] if prefix and not doi: if 'suffix' in doc['data']['attributes']: suffix = doc['data']['attributes']['suffix'] else: suffix = "rand" + str(random.randrange(10000, 99999)) doi = "%s/%s" % (prefix, suffix) if not doi: return self.send_error(400, "No prefix specified") if self.prefs and prefix not in self.prefs: return self.send_error(403, "Not Authorized for Prefix") event = doc['data']['attributes'].get('event') except KeyError as ex: return self.send_error(400, "Bad Input: Missing property", {"detail": str(ex)}) state = "draft" errors = None if doi in self.repo.ids: state = self.repo.ids[doi]['state'] out = self.repo.update_id(doi, doc['data']['attributes']) resp = {"code": 200, "message": "Updated"} else: out = self.repo.add_id(doi, doc['data']['attributes']) resp = {"code": 201, "message": "Created"} if (event == "publish" and state != "findable") or \ (event == "register" and state != "registered"): missing = [] for prop in "url titles publisher publicationYear creators types".split( ): if prop not in out or not out[prop]: missing.append(prop) elif prop.endswith('s') and len(out[prop]) < 1: missing.append(prop) if missing: self.repo.ids[doi]['state'] = state out['state'] = state errors = [{ "title": "Cannot publish due to missing metadata", "detail": "Missing properties: " + str(missing) }] resp = {"code": 422, "message": "Unprocessable Entity"} try: out = self._new_resp(doi, out) if errors: out['errors'] = errors out = json.dumps(out) self.set_response(**resp) self.add_header("Content-type", JSONAPI_MT) self.add_header("Content-length", str(len(out))) self.end_headers() return [out.encode()] except (ValueError, TypeError) as ex: return self.send_error(500, "JSON encoding error", errdesc={"detail": str(ex)}, tellexc=True) def do_PUT(self, path, params=None): if path: path = path.strip('/') if not path: return self.send_error(405, "Cannot PUT without ID") if 'HTTP_ACCEPT' in self._env and self._env[ 'HTTP_ACCEPT'] != JSONAPI_MT: return self.send_error(406, "Not Acceptable", "Unsupported Accept value", {"detail": self._env['HTTP_ACCEPT']}) if 'CONTENT_TYPE' in self._env and self._env[ 'CONTENT_TYPE'] != JSONAPI_MT: return self.send_error(415, "Wrong Input Type", "Unsupported input content type", {"detail": self._env['HTTP_ACCEPT']}) parts = path.split('/', 1) if len(parts) < 2: return self.send_error(405, "Incomplete DOI", errdesc={"detail": path}) if self.prefs and parts[0] not in self.prefs: return self.send_error(401, "Not authorized for prefix", errdesc={"detail": parts[0]}) if path not in self.repo.ids: return self.send_error(404, "ID Not Found", errdesc={"detail": path}) try: bodyin = self._env['wsgi.input'].read().decode('utf-8') doc = json.loads(bodyin, object_pairs_hook=OrderedDict) except (ValueError, TypeError) as ex: return self.send_error(400, "Not JSON", "Failed to parse input as JSON", {"detail": str(ex)}) doi = path errors = None try: state = self.repo.ids[doi].get('state', 'draft') attrs = doc['data']['attributes'] event = attrs.get('event') out = self.repo.update_id(path, attrs) except KeyError as ex: return self.send_error(400, "Bad Input: missing property", {"detail": str(ex)}) except ValueError as ex: return self.send_error(404, "ID not found", errdesc={"detail": path}) resp = {"code": 201, "message": "Updated"} if event == "publish" and state != "findable": missing = [] for prop in "url titles publisher publicationYear creators types".split( ): if prop not in out or not out[prop]: missing.append(prop) elif prop.endswith('s') and len(out[prop]) < 1: missing.append(prop) if missing: self.repo.ids[doi]['state'] = state out['state'] = state errors = [{ "title": "Cannot publish due to missing metadata", "detail": "Missing properties: " + str(missing) }] resp = {"code": 422, "message": "Unprocessable Entity"} try: out = self._new_resp(doi, out) if errors: out['errors'] = errors out = json.dumps(out) self.set_response(**resp) self.add_header("Content-type", JSONAPI_MT) self.add_header("Content-length", len(out)) self.end_headers() return [out.encode()] except (ValueError, TypeError) as ex: return self.send_error(500, "JSON encoding error", tellexc=True) def do_DELETE(self, path, params=None): if path: path = path.strip('/') else: return self.send_error(405, "ID not deletable", errdesc={"detail": path}) if path not in self.repo.ids: return self.send_error(404, "ID Not Found", errdesc={"detail": path}) try: self.repo.delete(path) except ValueError as ex: return self.send_error(403, str(ex), errdesc={"detail": path}) return self.send_error(204, "Deleted")
class BaseResponse: """Base class for Response""" default_status = 200 default_content_type = 'text/plain;' def __init__(self, body=b'', status=None, headers=None): self.headers = Headers() self._body = body self._status_code = status or self.default_status self._cookies = SimpleCookie() if headers: for name, value in headers.items(): self.headers.add_header(name, value) @property def body(self): return [self._body] @property def status_code(self): """ The HTTP status code as an integer (e.g. 404).""" return self._status_code @property def status(self): """ The HTTP status line as a string (e.g. ``404 Not Found``).""" if not 100 <= self._status_code <= 999: raise ValueError('Status code out of range.') status = _HTTP_STATUS_LINES.get(self._status_code) return str(status or ('{} Unknown'.format(self._status_code))) @status.setter def status(self, status_code): if not 100 <= status_code <= 999: raise ValueError('Status code out of range.') self._status_code = status_code @property def headerlist(self): """ WSGI conform list of (header, value) tuples. """ if 'Content-Type' not in self.headers: self.headers.add_header('Content-Type', self.default_content_type) if self._cookies: for c in self._cookies.values(): self.headers.add_header('Set-Cookie', c.OutputString()) return self.headers.items() def set_cookie(self, key, value, expires=None, max_age=None, path=None, secret=None, digestmod=hashlib.sha256): if secret: if isinstance(secret, str): secret = secret.encode('utf-8') encoded = base64.b64encode(pickle.dumps((key, value), pickle.HIGHEST_PROTOCOL)) sig = base64.b64encode(hmac.new(secret, encoded, digestmod=digestmod).digest()) value_bytes = b'!' + sig + b'?' + encoded value = value_bytes.decode('utf-8') self._cookies[key] = value if len(key) + len(value) > 3800: raise ValueError('Content does not fit into a cookie.') if max_age is not None: if isinstance(max_age, int): max_age_value = max_age else: max_age_value = max_age.seconds + max_age.days * 24 * 3600 self._cookies[key]['max-age'] = max_age_value if expires is not None: if isinstance(expires, int): expires_value = expires else: expires_value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", expires.timetuple()) self._cookies[key]['expires'] = expires_value if path: self._cookies[key]['path'] = path def delete_cookie(self, key, **kwargs): kwargs['max_age'] = -1 kwargs['expires'] = 0 self.set_cookie(key, '', **kwargs)
class Shortly(object): def __init__(self): self.url_map = {} self.view_functions = {} self.headers = Headers() self.status = None def add_url_rule(self, rule, endpoint=None, view_func=None, **options): if endpoint is None: endpoint = _endpoint_from_view_func(view_func) options["endpoint"] = endpoint methods = options.pop("methods", None) # if the methods are not given and the view_func object knows its # methods we can use that instead. If neither exists, we go with # a tuple of only ``GET`` as default. if methods is None: methods = getattr(view_func, "methods", None) or ("GET", ) if isinstance(methods, str): raise TypeError("Allowed methods have to be iterables of strings, " 'for example: @app.route(..., methods=["POST"])') methods = set(item.upper() for item in methods) rule = Rule(rule, methods=methods, **options) self.url_map.add(rule) if view_func is not None: self.view_functions[endpoint] = view_func def route(self, rule, **options): def decorator(f): endpoint = options.pop("endpoint", None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator def request_context(self, environ): """ :param environ: a WSGI environment """ request = DotDict() request.scheme = util.guess_scheme(environ) request.uri = util.request_uri(environ) request.address = util.application_uri(environ) request.path = util.shift_path_info(environ) if environ.get('REQUEST_METHOD', None): request.method = environ['REQUEST_METHOD'] if environ.get('CONTENT_TYPE', None): self.headers.add_header('CONTENT_TYPE', environ['CONTENT_TYPE']) try: length = int(environ.get('CONTENT_LENGTH', '0')) request.body = environ['wsgi.input'].read(length) except ValueError: request.body = b'' return request def dispatch_request(self, request): if request.path.startswith('/static'): fn = os.path.join(path, request.path[1:]) if '.' not in fn.split(os.path.sep)[-1]: fn = os.path.join(fn, 'index.html') type = mimetypes.guess_type(fn)[0] if os.path.exists(fn): self.status = '200 OK' self.headers.add_header('Content-type', type) return util.FileWrapper(open(fn, "rb")) else: self.status = '404 Not Found' self.headers.add_header('Content-type', 'text/plain') return [b'not found'] try: self.status = '200 OK' body = json.loads(request.body.decode('utf-8')) #rule = request.url_rule #return self.view_functions[rule.endpoint](**req.view_args) return body except Exception as e: self.status = '500 server error' return str(e) def wsgi_app(self, environ, start_response): ctx = self.request_context(environ) try: try: response = self.dispatch_request(ctx) headers = [(k, v) for k, v in self.headers.items()] start_response(self.status, headers) return response except Exception as e: start_response('500 server error', [('Content-type', 'text/plain')]) return [str(e)] finally: pass def __call__(self, environ, start_response): return self.wsgi_app(environ, start_response)
class Route(): def __init__(self, path, method, callback, status=200, content_type=None): self.path = path self.method = method self.callback = callback self.__status = status if content_type is None: content_type = 'text/html; charset=UTF-8' self.__ct = content_type self.__header = Headers() @property def status_code(self): return '{} {}'.format(self.__status, responses[self.__status]) @property def headers(self): self.__header.add_header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') self.__header.add_header('Content-Security-Policy', "default-src 'self'") self.__header.add_header('X-Content-Type-Options', 'nosniff') self.__header.add_header('X-Frame-Options', 'SAMEORIGIN') self.__header.add_header('X-XSS-Protection', '1; mode=block') self.__header.add_header('Content-type', self.__ct) return self.__header.items()
class application(object): # don't serve static by default static_serve = False static_alias = { '' : 'ketcher.html' } static_root = None indigo = None indigo_inchi = None def __init__(self, environ, start_response): self.path = environ['PATH_INFO'].strip('/') self.method = environ['REQUEST_METHOD'] self.content_type = environ.get('CONTENT_TYPE', '') self.fields = FieldStorage(fp=environ['wsgi.input'], environ=environ, keep_blank_values=True) self.FileWrapper = environ.get('wsgi.file_wrapper', FileWrapper) self.headers = Headers([]) route = getattr(self, 'on_' + self.path, None) if route is None: route = self.serve_static if self.method == 'GET' and \ self.static_serve else self.notsupported status = "200 OK" try: self.response = route() except self.HttpException as e: status = e.args[0] self.response = [e.args[1]] self.headers.setdefault('Content-Type', 'text/plain') start_response(status, self.headers.items()) def __iter__(self): for chunk in self.response: yield chunk if sys.version_info[0] < 3 or \ not hasattr(chunk, 'encode') else chunk.encode() def notsupported(self): raise self.HttpException("405 Method Not Allowed", "Request not supported") def indigo_required(method): def wrapper(self, **args): if not self.indigo: raise self.HttpException("501 Not Implemented", "Indigo libraries are not found") try: return method(self, **args) except indigo.IndigoException as e: message = str(sys.exc_info()[1]) if 'indigoLoad' in message: # error on load message = "Cannot load the specified " + \ "structure: %s " % str(e) raise self.HttpException("400 Bad Request", message) return wrapper @indigo_required def on_knocknock(self): return ["You are welcome!"] @indigo_required def on_layout(self): moldata = None if self.method == 'GET' and 'smiles' in self.fields: moldata = self.fields.getfirst('smiles') elif self.is_form_request() and 'moldata' in self.fields: moldata = self.fields.getfirst('moldata') selective = 'selective' in self.fields if moldata: if '>>' in moldata or moldata.startswith('$RXN'): rxn = self.indigo.loadQueryReaction(moldata) if selective: for mol in rxn.iterateMolecules(): self.selective_layout(mol) else: rxn.layout() return ["Ok.\n", rxn.rxnfile()] elif moldata.startswith('InChI'): mol = self.indigo_inchi.loadMolecule(moldata) mol.layout() return ["Ok.\n", mol.molfile()] else: mol = self.indigo.loadQueryMolecule(moldata) if selective: for rg in mol.iterateRGroups(): for frag in rg.iterateRGroupFragments(): self.selective_layout(frag) self.selective_layout(mol) else: mol.layout() return ["Ok.\n", mol.molfile()] self.notsupported() @indigo_required def on_automap(self): moldata = None if self.method == 'GET' and 'smiles' in self.fields: moldata = self.fields.getfirst('smiles') elif self.is_form_request() and 'moldata' in self.fields: moldata = self.fields.getfirst('moldata') if moldata: mode = self.fields.getfirst('mode', 'discard') rxn = self.indigo.loadQueryReaction(moldata) if not moldata.startswith('$RXN'): rxn.layout() rxn.automap(mode) return ["Ok.\n", rxn.rxnfile()] self.notsupported() @indigo_required def on_aromatize(self): try: md, is_rxn = self.load_moldata() except: message = str(sys.exc_info()[1]) if message.startswith("\"molfile loader:") and \ message.endswith("queries\""): # hack to avoid user confusion md, is_rxn = self.load_moldata(True) else: raise md.aromatize() return ["Ok.\n", md.rxnfile() if is_rxn else md.molfile()] @indigo_required def on_getinchi(self): md, is_rxn = self.load_moldata() inchi = self.indigo_inchi.getInchi(md) return ["Ok.\n", inchi] @indigo_required def on_dearomatize(self): try: md, is_rxn = self.load_moldata() except: # TODO: test for query features presence raise self.HttpException("400 Bad Request", "Molecules and reactions " + \ "containing query features " + \ "cannot be dearomatized yet.") md.dearomatize() return ["Ok.\n", md.rxnfile() if is_rxn else md.molfile()] def on_open(self): if self.is_form_request(): self.headers.add_header('Content-Type', 'text/html') return ['<html><body onload="parent.ui.loadMoleculeFromFile()" title="', b64encode("Ok.\n"), b64encode(self.fields.getfirst('filedata')), '"></body></html>'] self.notsupported() def on_save(self): if self.is_form_request(): type, data = self.fields.getfirst('filedata').split('\n', 1) type = type.strip() if type == 'smi': self.headers.add_header('Content-Type', 'chemical/x-daylight-smiles') elif type == 'mol': if data.startswith('$RXN'): type = 'rxn' self.headers.add_header('Content-Type', 'chemical/x-mdl-%sfile' % type) self.headers.add_header('Content-Length', str(len(data))) self.headers.add_header('Content-Disposition', 'attachment', filename='ketcher.%s' % type) return [data] self.notsupported() class HttpException(Exception): pass def load_moldata(self, is_query=False): moldata = self.fields.getfirst('moldata') if moldata.startswith('$RXN'): if is_query: md = self.indigo.loadQueryReaction(moldata) else: md = self.indigo.loadReaction(moldata) is_rxn = True else: if is_query: md = self.indigo.loadQueryMolecule(moldata) else: md = self.indigo.loadMolecule(moldata) is_rxn = False return md, is_rxn def selective_layout(self, mol): dsgs = [dsg for dsg in mol.iterateDataSGroups() \ if dsg.description() == '_ketcher_selective_layout' and \ dsg.data() == '1'] atoms = sorted([atom.index() for dsg in dsgs \ for atom in dsg.iterateAtoms()]) for dsg in dsgs: dsg.remove() mol.getSubmolecule(atoms).layout() return mol def serve_static(self): root = self.static_root or getcwd() fpath = self.static_alias.get(self.path, self.path) fpath = path.abspath(path.join(root, fpath)) if not fpath.startswith(root + path.sep) or not path.isfile(fpath) \ or fpath == path.abspath(__file__): raise self.HttpException("404 Not Found", "Requested file isn't accessible") self.headers['Content-Type'] = guess_type(fpath)[0] or 'text/plain' try: fd = open(fpath, 'rb') return self.FileWrapper(fd) if self.method == 'GET' else [''] except (IOError, OSError): raise self.HttpException("402 Payment Required", # or 403, hmm.. "Must get more money for overtime") def is_form_request(self): return self.method == 'POST' and \ (self.content_type.startswith('application/x-www-form-urlencoded') or self.content_type.startswith('multipart/form-data'))
class StaticFile(object): ACCEPT_GZIP_RE = re.compile(r'\bgzip\b') BLOCK_SIZE = 16 * 4096 # All mimetypes starting 'text/' take a charset parameter, plus the # additions in this set MIMETYPES_WITH_CHARSET = {'application/javascript', 'application/xml'} CHARSET = 'utf-8' # Ten years is what nginx sets a max age if you use 'expires max;' # so we'll follow its lead FOREVER = 10*365*24*60*60 GZIP_SUFFIX = '.gz' def __init__(self, path, is_immutable, guess_type=mimetypes.guess_type, **config): self.path = path stat = os.stat(path) self.mtime_tuple = gmtime(stat.st_mtime) mimetype, encoding = guess_type(path) mimetype = mimetype or 'application/octet-stream' charset = self.get_charset(mimetype) params = {'charset': charset} if charset else {} self.headers = Headers([ ('Last-Modified', formatdate(stat.st_mtime, usegmt=True)), ('Content-Length', str(stat.st_size)), ]) self.headers.add_header('Content-Type', str(mimetype), **params) if encoding: self.headers['Content-Encoding'] = encoding max_age = self.FOREVER if is_immutable else config['max_age'] if max_age is not None: self.headers['Cache-Control'] = 'public, max-age=%s' % max_age if config['allow_all_origins']: self.headers['Access-Control-Allow-Origin'] = '*' gzip_path = path + self.GZIP_SUFFIX if os.path.isfile(gzip_path): self.gzip_path = gzip_path self.headers['Vary'] = 'Accept-Encoding' # Copy the headers and add the appropriate encoding and length self.gzip_headers = Headers(self.headers.items()) self.gzip_headers['Content-Encoding'] = 'gzip' self.gzip_headers['Content-Length'] = str(os.stat(gzip_path).st_size) else: self.gzip_path = self.gzip_headers = None def get_charset(self, mimetype): if mimetype.startswith('text/') or mimetype in self.MIMETYPES_WITH_CHARSET: return self.CHARSET def serve(self, environ, start_response): method = environ['REQUEST_METHOD'] if method != 'GET' and method != 'HEAD': start_response('405 Method Not Allowed', [('Allow', 'GET, HEAD')]) return [] if self.file_not_modified(environ): start_response('304 Not Modified', []) return [] path, headers = self.get_path_and_headers(environ) start_response('200 OK', headers.items()) if method == 'HEAD': return [] file_wrapper = environ.get('wsgi.file_wrapper', self.yield_file) fileobj = open(path, 'rb') return file_wrapper(fileobj) def file_not_modified(self, environ): try: last_requested = environ['HTTP_IF_MODIFIED_SINCE'] except KeyError: return False # Exact match, no need to parse if last_requested == self.headers['Last-Modified']: return True return parsedate(last_requested) >= self.mtime_tuple def get_path_and_headers(self, environ): if self.gzip_path: if self.ACCEPT_GZIP_RE.search(environ.get('HTTP_ACCEPT_ENCODING', '')): return self.gzip_path, self.gzip_headers return self.path, self.headers def yield_file(self, fileobj): # Only used as a fallback in case environ doesn't supply a # wsgi.file_wrapper try: while True: block = fileobj.read(self.BLOCK_SIZE) if block: yield block else: break finally: fileobj.close()