# http://pyrocko.org - GPLv3 # # The Pyrocko Developers, 21st Century # ---|P------/S----------~Lg---------- Simple async HTTP server
Based on this recipe:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440665
which is based on this one:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/259148 """
# Preallocate the list to save memory resizing.
self.d.append(data) else: buf.append(data[i:i+BS])
# In enabling the use of buffer objects by setting use_buffer to True, # any data block sent will remain in memory until it has actually been # sent.
# set the terminator : when it is received, this means that the # http request is complete ; control will be passed to # self.found_terminator self.outgoing, -self.use_buffer or self.blocksize) # buffer the response and headers to avoid several calls to select()
self.use_buffer = True self.blocksize = 131072
"""Collect the data arriving on the connexion""" self.ac_in_buffer = "" return
"""Prepare to read the request body""" bytesToRead = int(self.headers.getheader('Content-length')) # set terminator to length (will read bytesToRead bytes) self.set_terminator(bytesToRead) self.incoming.clear() # control will be passed to a new found_terminator self.found_terminator = self.handle_post_data
"""Called when a POST request body has been read""" self.rfile = BytesIO(b''.join(popall(self.incoming))) self.rfile.seek(0) self.do_POST()
# Check for query string in URL self.body = cgi.parse_qs(self.path[qspos+1:], keep_blank_values=1) self.path = self.path[:qspos] else:
"""Begins serving a HEAD request"""
"""Begins serving a GET request"""
"""Begins serving a POST request. The request data must be readable on a file-like object called self.rfile""" ctype, pdict = cgi.parse_header(self.headers.getheader('content-type')) length = int(self.headers.getheader('content-length')) if ctype == 'multipart/form-data': self.body = cgi.parse_multipart(self.rfile, pdict) elif ctype == 'application/x-www-form-urlencoded': qs = self.rfile.read(length) self.body = cgi.parse_qs(qs, keep_blank_values=1) else: self.body = {} # self.handle_post_body() self.handle_data()
f.close()
"""Class to override"""
# do some special things with file objects so that we don't have # to read them all into memory at the same time...may leave a # file handle open for longer than is really desired, but it does # make it able to handle files of unlimited size. except (AttributeError, io.UnsupportedOperation): size = len(f.getvalue())
else: self.log_request(self.code) # signal the end of this request self.outgoing.append(None)
"""Called when the http request line and headers have been received""" # prepare attributes needed in parse_request()
# if method is GET or HEAD, call do_GET or do_HEAD and finish elif self.command == "POST": # if method is POST, call prepare_POST, don't finish yet self.prepare_POST() else: self.send_error(501, "Unsupported method (%s)" % self.command)
traceback.print_exc(sys.stderr) self.close()
# handle end of request disconnection # Some clients have issues with keep-alive connections, or # perhaps I implemented them wrong.
# If the user is running a Python version < 2.4.1, there is a # bug with SimpleHTTPServer: # http://python.org/sf/1097597 # So we should be closing anyways, even though the client will # claim a partial download, so as to prevent hung-connections. # if self.close_connection: self.close() return
# handle file objects else:
# handle string/buffer objects else: # if we get here, the outgoing deque is empty
# if we get here, 'a' is a string or buffer object of length > 0 if not num_sent: # this is probably overkill, but it can save the # allocations of buffers when they are enabled out.appendleft(a) elif self.use_buffer: out.appendleft(buffer(a, num_sent)) # noqa else: out.appendleft(a[num_sent:])
except socket.error as why: if isinstance(why, newstr): self.log_error(why) elif isinstance(why, tuple) and isinstance(why[-1], newstr): self.log_error(why[-1]) else: self.log_error(str(why)) self.handle_error()
self.log_info(message)
{ 'debug': logger.debug, 'info': logger.info, 'warning': logger.warning, 'error': logger.error }.get(type, logger.info)(str(message))
self.address_string(), self.log_date_time_string(), format % args, self.headers.get('referer', ''), self.headers.get('user-agent', '')))
"""Helper to produce a directory listing (absent index.html).
Return value is either a file object, or None (indicating an error). In either case, the headers are sent, making the interface the same as for send_head().
""" except os.error: self.send_error(404, "No permission to list directory") return None
% displaypath)) enc("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath)) # Append / for directories or @ for symbolic links displayname = name + "@" # Note: a link to a directory displays with @ and links with / (urllib.parse.quote(linkname), html.escape(displayname))))
self.send_response(301) self.send_header("Location", path) self.end_headers()
"""Common code for GET and HEAD commands.
This sends the response code and MIME headers.
Return value is either a file object (which has to be copied to the outputfile by the caller unless the command was HEAD, and must be closed by the caller under all circumstances), or None, in which case the caller has nothing further to do.
""" self.send_error(404, "File not found") return None
# redirect browser - doing basically what apache does return self.redirect(self.path + '/') else:
# Always read in binary mode. Opening files in text mode may cause # newline translations, making the actual size of the content # transmitted *less* than the content-length! except IOError: self.send_error(404, "File not found") return None
return self.redirect(x)
elif re.match(r'^' + S + '$', self.path): return self.list_stores()
elif re.match(r'^' + A + '$', self.path): return self.list_stores_json()
elif re.match(r'^' + A + store_id_pattern + '$', self.path): return self.get_store_config()
elif re.match(r'^' + A + store_id_pattern + '/profile$', self.path): return self.get_store_velocity_profile()
elif re.match(r'^' + P + '$', self.path): return self.process()
else: self.send_error(404, "File not found") self.end_headers() return None
return None else: else: return None
continue
return list(self.server.engine.get_store_ids()) else:
'''Create listing of stores.''' from jinja2 import Template
engine = self.server.engine
store_ids = list(engine.get_store_ids()) store_ids.sort(key=lambda x: x.lower())
stores = [engine.get_store(store_id) for store_id in store_ids]
templates = { 'html': Template(''' <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <html> <title>{{ title }}</title> <body> <h2>{{ title }}</h2> <hr> <table> <tr> <th style="text-align:left">Store ID</th> <th style="text-align:center">Type</th> <th style="text-align:center">Extent</th> <th style="text-align:center">Sample-rate</th> <th style="text-align:center">Size (index + traces)</th> </tr> {% for store in stores %} <tr> <td><a href="{{ store.config.id }}/">{{ store.config.id|e }}/</a></td> <td style="text-align:center">{{ store.config.short_type }}</td> <td style="text-align:right">{{ store.config.short_extent }} km</td> <td style="text-align:right">{{ store.config.sample_rate }} Hz</td> <td style="text-align:right">{{ store.size_index_and_data_human }}</td> </tr> {% endfor %} </table> </hr> </body> </html> '''.lstrip()), 'text': Template(''' {% for store in stores %}{# #}{{ store.config.id.ljust(25) }} {# #}{{ store.config.short_type.center(5) }} {# #}{{ store.config.short_extent.rjust(30) }} km {# #}{{ "%10.2g"|format(store.config.sample_rate) }} Hz {# #}{{ store.size_index_and_data_human.rjust(8) }} {% endfor %}'''.lstrip())}
format = self.body.get('format', ['html'])[0] if format not in ('html', 'text'): format = 'html'
title = "Green's function stores listing" s = templates[format].render(stores=stores, title=title).encode('utf8') length = len(s) f = BytesIO(s) self.send_response(200, 'OK') self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f
engine = self.server.engine
store_ids = list(engine.get_store_ids()) store_ids.sort(key=lambda x: x.lower())
def get_store_dict(store): store.ensure_reference()
return { 'id': store.config.id, 'short_type': store.config.short_type, 'modelling_code_id': store.config.modelling_code_id, 'source_depth_min': store.config.source_depth_min, 'source_depth_max': store.config.source_depth_max, 'source_depth_delta': store.config.source_depth_delta, 'distance_min': store.config.distance_min, 'distance_max': store.config.distance_max, 'distance_delta': store.config.distance_delta, 'sample_rate': store.config.sample_rate, 'size': store.size_index_and_data, 'uuid': store.config.uuid, 'reference': store.config.reference }
stores = { 'stores': [get_store_dict(engine.get_store(store_id)) for store_id in store_ids] }
s = json.dumps(stores) length = len(s) f = BytesIO(s.encode('ascii')) self.send_response(200, 'OK') self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(length)) self.send_header("Access-Control-Allow-Origin", '*') self.end_headers()
return f
engine = self.server.engine
store_ids = list(engine.get_store_ids()) store_ids.sort(key=lambda x: x.lower())
for match in re.finditer(r'/gfws/api/(' + store_id_pattern + ')', self.path): store_id = match.groups()[0]
try: store = engine.get_store(store_id) except Exception: self.send_error(404) self.end_headers() return
data = {} data['id'] = store_id data['config'] = str(store.config)
s = json.dumps(data) length = len(s) f = BytesIO(s.encode('ascii')) self.send_response(200, 'OK') self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(length)) self.send_header("Access-Control-Allow-Origin", '*') self.end_headers()
return f
engine = self.server.engine
fig = plt.figure() axes = fig.gca()
store_ids = list(engine.get_store_ids()) store_ids.sort(key=lambda x: x.lower())
for match in re.finditer( r'/gfws/api/(' + store_id_pattern + ')/profile', self.path): store_id = match.groups()[0]
try: store = engine.get_store(store_id) except Exception: self.send_error(404) self.end_headers() return
cake_plot.my_model_plot(store.config.earthmodel_1d, axes=axes)
f = BytesIO() fig.savefig(f, format='png')
length = f.tell() self.send_response(200, 'OK') self.send_header("Content-Type", "image/png;") self.send_header("Content-Length", str(length)) self.send_header("Access-Control-Allow-Origin", '*') self.end_headers()
f.seek(0) return f.read()
request = gf.load(string=self.body['request'][0]) try: resp = self.server.engine.process(request=request) except (gf.BadRequest, gf.StoreError) as e: self.send_error(400, str(e)) return
f = BytesIO() resp.dump(stream=f) length = f.tell()
f.seek(0)
self.send_response(200, 'OK') self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(length)) self.end_headers() return f
return 'text/plain'
else: or 'application/x-octet'
# Quoting the socket module documentation... # listen(backlog) # Listen for connections made to the socket. The backlog argument # specifies the maximum number of queued connections and should # be at least 1; the maximum value is system-dependent (usually # 5).
def ensure_uuids(engine):
except socket.error: self.log_info('warning: server accept() threw an exception', 'warning') return except TypeError: self.log_info('warning: server accept() threw EWOULDBLOCK', 'warning') return
self.log_info(message)
self.close()
{ 'debug': logger.debug, 'info': logger.info, 'warning': logger.warning, 'error': logger.error }.get(type, 'info')(str(message))
s = Server(ip, port, SeismosizerHandler, engine) asyncore.loop() del s
if __name__ == '__main__': util.setup_logging('pyrocko.gf.server', 'info') port = 8085 engine = gf.LocalEngine(store_superdirs=sys.argv[1:]) run('127.0.0.1', port, engine) |