blob: e660b05a4b13ea0c7dd392b71ad3911c601118e3 [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
34
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070035
36log = get_logger()
37
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070038
39class WebServer(object):
40
41 app = Klein()
42
ubuntu37975582017-07-01 17:53:19 -070043 def __init__(self, port, work_dir, swagger_url, grpc_client, key=None, cert=None):
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070044 self.port = port
45 self.site = None
46 self.work_dir = work_dir
Matteo Scandolo54356572017-04-24 18:52:28 -070047 self.swagger_url = swagger_url
ubuntu37975582017-07-01 17:53:19 -070048 self.grpc_client = grpc_client
49 self.key = key
50 self.cert = cert
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070051
52 self.swagger_ui_root_dir = os.path.abspath(
53 os.path.join(os.path.dirname(__file__), '../swagger_ui'))
54
55 self.tcp_port = None
Zsolt Harasztie7b60762016-10-05 17:49:27 -070056 self.shutting_down = False
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070057
Matteo Scandolo54356572017-04-24 18:52:28 -070058 self.add_swagger_routes(self.app, swagger_url)
59
60 def add_swagger_routes(self, app, swagger_url):
61 log.info('Publishing swagger docs at %s' % swagger_url)
62
63 @app.route(swagger_url + '/', branch=True)
64 def static(self, request):
65 try:
66 log.debug(request=request)
67 return File(self.swagger_ui_root_dir)
68 except Exception, e:
69 log.exception('file-not-found', request=request)
70
71 @app.route(swagger_url + '/v1/swagger.json')
72 def swagger_json(self, request):
73 try:
74 return File(os.path.join(self.work_dir, 'swagger.json'))
75 except Exception, e:
76 log.exception('file-not-found', request=request)
77
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070078 @inlineCallbacks
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070079 def start(self):
80 log.debug('starting')
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070081 yield self._open_endpoint()
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070082 log.info('started')
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070083 returnValue(self)
84
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070085 @inlineCallbacks
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070086 def stop(self):
87 log.debug('stopping')
88 self.shutting_down = True
89 if self.tcp_port is not None:
90 assert isinstance(self.tcp_port, Port)
91 yield self.tcp_port.socket.close()
92 log.info('stopped')
93
94 @inlineCallbacks
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070095 def _open_endpoint(self):
ubuntu37975582017-07-01 17:53:19 -070096 try:
97 if self.key == None or self.cert == None:
98 endpoint = endpoints.TCP4ServerEndpoint(reactor, self.port)
99 else:
100 # Enforce TLSv1_2_METHOD
101 ctx = DefaultOpenSSLContextFactory(self.key, self.cert, TLSv1_2_METHOD)
102 endpoint = SSL4ServerEndpoint(reactor, self.port, ctx)
103
104 self.site = Site(self.app.resource())
105 self.tcp_port = yield endpoint.listen(self.site)
106 log.info('web-server-started', port=self.port)
107 self.endpoint = endpoint
108 except Exception, e:
109 self.log.exception('web-server-failed-to-start', e=e)
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700110
Zsolt Harasztie7b60762016-10-05 17:49:27 -0700111 def reload_generated_routes(self):
112 for fname in os.listdir(self.work_dir):
113 if fname.endswith('_gw.py'):
114 module_name = fname.replace('.py', '')
115 m = __import__(module_name)
116 assert hasattr(m, 'add_routes')
117 m.add_routes(self.app, self.grpc_client)
118 log.info('routes-loaded', module=module_name)
119
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -0800120 @app.handle_errors(grpc._channel._Rendezvous)
121 def grpc_exception(self, request, failure):
122 code = failure.value.code()
123 if code == StatusCode.NOT_FOUND:
124 request.setResponseCode(404)
125 return failure.value.details()
126 elif code == StatusCode.INVALID_ARGUMENT:
127 request.setResponseCode(400)
128 return failure.value.details()
129 elif code == StatusCode.ALREADY_EXISTS:
130 request.setResponseCode(409)
131 return failure.value.details()
Matteo Scandolo92f7f1f2017-04-28 15:56:29 -0700132 elif code == StatusCode.UNAUTHENTICATED:
133 request.setResponseCode(401)
134 return failure.value.details()
135 elif code == StatusCode.PERMISSION_DENIED:
136 request.setResponseCode(403)
137 return failure.value.details()
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -0800138 else:
139 raise
140