This repository has been archived by the owner on Aug 26, 2020. It is now read-only.
/
host_http_server.py
180 lines (143 loc) · 5.69 KB
/
host_http_server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import json
import tornado
from tornado.ioloop import IOLoop
from tornado.routing import Rule, RuleRouter, PathMatches
from tornado.httpserver import HTTPServer
from tornado.web import Application, RequestHandler
from tornado.httpclient import HTTPClientError
from .host import HOST
class BaseHandler(RequestHandler):
"""
A base class for all request handlers.
Adds necessary headers and handles `OPTIONS` requests
needed for CORS. Handles errors.
"""
def set_default_headers(self):
self.set_header('Server', 'Bindilla / Tornado %s' % tornado.version)
self.set_header('Content-Type', 'application/json')
# Use origin of request to avoid browser errors like
# "The value of the 'Access-Control-Allow-Origin' header in the
# response must not be the wildcard '*' when the
# request's credentials mode is 'include'."
origin = self.request.headers.get('Origin', '')
self.set_header('Access-Control-Allow-Origin', origin)
self.set_header('Access-Control-Allow-Credentials', 'true')
self.set_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
self.set_header('Access-Control-Allow-Headers', 'Content-Type')
self.set_header('Access-Control-Max-Age', '86400')
def head(self, *args, **kwargs): #pylint: disable=unused-argument
self.set_status(204)
self.finish()
def options(self, *args, **kwargs): #pylint: disable=unused-argument
self.set_status(204)
self.finish()
def send(self, value):
body = json.dumps(value, indent=2)
self.write(body)
def write_error(self, status_code, **kwargs):
if 'exc_info' in kwargs:
_, value, _ = kwargs['exc_info']
if isinstance(value, ValueError):
self.set_status(400)
self.write(str(value))
return
RequestHandler.write_error(self, status_code, **kwargs)
class IndexHandler(BaseHandler):
"""
Handles requests to the index/home page.
Just redirect to the Github repo. Don't use a 301, or 302,
because that can cause load balancer helth checks to fail.
"""
def get(self, *args, **kwargs): #pylint: disable=unused-argument
self.set_header('Content-Type', 'text/html')
return self.write('''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; URL='https://github.com/stencila/bindilla#readme'" />
</head>
<body>
<script>window.location = "https://github.com/stencila/bindilla#readme";</script>
</body>
</html
''')
class ManifestHandler(BaseHandler):
"""
Handles requests for Bindilla's `Host` manifest.
"""
def get(self, environs):
self.send(HOST.manifest(environs.split(',') if environs else None))
class EnvironHandler(BaseHandler):
"""
Handles requests to launch and shutdown an environment on Binder.
"""
async def post(self, environ_id):
"""
Launch a Binder for the environment.
"""
self.send(await HOST.launch_environ(environ_id))
async def delete(self, environ_id): #pylint: disable=unused-argument
"""
Shutdown a Binder for the environment.
Currently, this is a no-op, but is provided for API compatability
(clients may request for an environ to be stopped).
"""
self.set_status(200)
self.finish()
class ProxyHandler(BaseHandler):
"""
Proxies requests through to the container running on Binder.
"""
async def proxy(self, method, binder_id, token, path, body=None): #pylint: disable=too-many-arguments
try:
response = await HOST.proxy_environ(method, binder_id, token, path, body)
except HTTPClientError as error:
self.set_status(error.code)
self.write(str(error))
else:
for header, value in response.headers.get_all():
if header not in ('Content-Length', 'Transfer-Encoding', 'Content-Encoding', 'Connection'):
self.add_header(header, value)
if response.body:
self.set_header('Content-Length', len(response.body))
self.write(response.body)
self.finish()
async def get(self, binder_id, token, path):
await self.proxy('GET', binder_id, token, path)
async def post(self, binder_id, token, path):
await self.proxy('POST', binder_id, token, path, self.request.body)
async def put(self, binder_id, token, path):
await self.proxy('PUT', binder_id, token, path, self.request.body)
def make():
"""
Make the Tornado `RuleRouter`.
"""
# API v1 endpoints
v1_app = Application([
(r'^/?(?P<environs>.*?)/v1/manifest/?', ManifestHandler),
(r'^.*?/v1/environs/(?P<environ_id>.+)', EnvironHandler),
(r'^.*?/v1/proxy/(?P<binder_id>[^@]+)\@(?P<token>[^\/]+)/(?P<path>.+)', ProxyHandler)
])
# API v0 endpoints
v0_app = Application([
(r'^/?(?P<environs>.*?)/v0/manifest/?', ManifestHandler),
(r'^.*?/v0/environ/(?P<environ_id>.+)', EnvironHandler),
(r'^.*?/v0/proxy/(?P<binder_id>[^@]+)\@(?P<token>[^\/]+)/(?P<path>.+)', ProxyHandler)
])
index_app = Application([
(r'^/', IndexHandler)
])
return RuleRouter([
Rule(PathMatches(r'^.*?/v1/.*'), v1_app),
Rule(PathMatches(r'^.*?/v0/.*'), v0_app),
Rule(PathMatches(r'^/$'), index_app)
])
def run():
"""
Run the HTTP server.
"""
router = make()
server = HTTPServer(router)
server.listen(8888)
IOLoop.current().start()