blob: f817045541e51a8562f9a3cbc71a275ad185a2b2 [file] [log] [blame]
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -07001#!/usr/bin/env python
2#
Zsolt Harasztiaccad4a2017-01-03 21:56:48 -08003# Copyright 2017 the original author or authors.
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -07004#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18import os
19
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080020import grpc
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070021from klein import Klein
22from simplejson import dumps, load
23from structlog import get_logger
24from twisted.internet import reactor, endpoints
25from twisted.internet.defer import inlineCallbacks, returnValue
26from twisted.internet.tcp import Port
ubuntu37975582017-07-01 17:53:19 -070027from twisted.internet.endpoints import SSL4ServerEndpoint
28from twisted.internet.ssl import DefaultOpenSSLContextFactory
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070029from twisted.web.server import Site
30from twisted.web.static import File
ubuntu37975582017-07-01 17:53:19 -070031from OpenSSL.SSL import TLSv1_2_METHOD
Zsolt Harasztie7b60762016-10-05 17:49:27 -070032from werkzeug.exceptions import BadRequest
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080033from grpc import StatusCode
Matteo Scandolocaecf422017-09-11 16:16:07 -070034import json
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080035
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070036
37log = get_logger()
38
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070039
40class WebServer(object):
41
42 app = Klein()
43
ubuntu37975582017-07-01 17:53:19 -070044 def __init__(self, port, work_dir, swagger_url, grpc_client, key=None, cert=None):
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070045 self.port = port
46 self.site = None
47 self.work_dir = work_dir
Matteo Scandolo54356572017-04-24 18:52:28 -070048 self.swagger_url = swagger_url
ubuntu37975582017-07-01 17:53:19 -070049 self.grpc_client = grpc_client
50 self.key = key
51 self.cert = cert
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070052
53 self.swagger_ui_root_dir = os.path.abspath(
54 os.path.join(os.path.dirname(__file__), '../swagger_ui'))
55
56 self.tcp_port = None
Zsolt Harasztie7b60762016-10-05 17:49:27 -070057 self.shutting_down = False
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070058
Matteo Scandolo54356572017-04-24 18:52:28 -070059 self.add_swagger_routes(self.app, swagger_url)
60
61 def add_swagger_routes(self, app, swagger_url):
62 log.info('Publishing swagger docs at %s' % swagger_url)
63
64 @app.route(swagger_url + '/', branch=True)
65 def static(self, request):
66 try:
67 log.debug(request=request)
68 return File(self.swagger_ui_root_dir)
69 except Exception, e:
70 log.exception('file-not-found', request=request)
71
72 @app.route(swagger_url + '/v1/swagger.json')
73 def swagger_json(self, request):
74 try:
75 return File(os.path.join(self.work_dir, 'swagger.json'))
76 except Exception, e:
77 log.exception('file-not-found', request=request)
78
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070079 @inlineCallbacks
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070080 def start(self):
81 log.debug('starting')
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070082 yield self._open_endpoint()
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070083 log.info('started')
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070084 returnValue(self)
85
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070086 @inlineCallbacks
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070087 def stop(self):
88 log.debug('stopping')
89 self.shutting_down = True
90 if self.tcp_port is not None:
91 assert isinstance(self.tcp_port, Port)
92 yield self.tcp_port.socket.close()
93 log.info('stopped')
94
95 @inlineCallbacks
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070096 def _open_endpoint(self):
ubuntu37975582017-07-01 17:53:19 -070097 try:
98 if self.key == None or self.cert == None:
99 endpoint = endpoints.TCP4ServerEndpoint(reactor, self.port)
100 else:
101 # Enforce TLSv1_2_METHOD
102 ctx = DefaultOpenSSLContextFactory(self.key, self.cert, TLSv1_2_METHOD)
103 endpoint = SSL4ServerEndpoint(reactor, self.port, ctx)
104
105 self.site = Site(self.app.resource())
106 self.tcp_port = yield endpoint.listen(self.site)
107 log.info('web-server-started', port=self.port)
108 self.endpoint = endpoint
109 except Exception, e:
110 self.log.exception('web-server-failed-to-start', e=e)
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700111
Zsolt Harasztie7b60762016-10-05 17:49:27 -0700112 def reload_generated_routes(self):
113 for fname in os.listdir(self.work_dir):
114 if fname.endswith('_gw.py'):
115 module_name = fname.replace('.py', '')
116 m = __import__(module_name)
117 assert hasattr(m, 'add_routes')
118 m.add_routes(self.app, self.grpc_client)
119 log.info('routes-loaded', module=module_name)
120
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -0800121 @app.handle_errors(grpc._channel._Rendezvous)
122 def grpc_exception(self, request, failure):
123 code = failure.value.code()
124 if code == StatusCode.NOT_FOUND:
125 request.setResponseCode(404)
126 return failure.value.details()
127 elif code == StatusCode.INVALID_ARGUMENT:
128 request.setResponseCode(400)
129 return failure.value.details()
130 elif code == StatusCode.ALREADY_EXISTS:
131 request.setResponseCode(409)
132 return failure.value.details()
Matteo Scandolo92f7f1f2017-04-28 15:56:29 -0700133 elif code == StatusCode.UNAUTHENTICATED:
134 request.setResponseCode(401)
135 return failure.value.details()
136 elif code == StatusCode.PERMISSION_DENIED:
137 request.setResponseCode(403)
138 return failure.value.details()
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -0800139 else:
Matteo Scandolocaecf422017-09-11 16:16:07 -0700140 request.setResponseCode(500)
141 return json.dumps({'error': 'Internal Server Error', 'specific_error': failure.value.details()})
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -0800142