3
# Copyright 2010 Google Inc.
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
9
# http://www.apache.org/licenses/LICENSE-2.0
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.
18
"""Handlers for remote services.
20
This module contains classes that may be used to build a service
21
on top of the App Engine Webapp framework.
23
The services request handler can be configured to handle requests in a number
24
of different request formats. All different request formats must have a way
25
to map the request to the service handlers defined request message.Message
26
class. The handler can also send a response in any format that can be mapped
27
from the response message.Message class.
29
Participants in an RPC:
31
There are four classes involved with the life cycle of an RPC.
33
Service factory: A user-defined service factory that is responsible for
34
instantiating an RPC service. The methods intended for use as RPC
35
methods must be decorated by the 'remote' decorator.
37
RPCMapper: Responsible for determining whether or not a specific request
38
matches a particular RPC format and translating between the actual
39
request/response and the underlying message types. A single instance of
40
an RPCMapper sub-class is required per service configuration. Each
41
mapper must be usable across multiple requests.
43
ServiceHandler: A webapp.RequestHandler sub-class that responds to the
44
webapp framework. It mediates between the RPCMapper and service
45
implementation class during a request. As determined by the Webapp
46
framework, a new ServiceHandler instance is created to handle each
47
user request. A handler is never used to handle more than one request.
49
ServiceHandlerFactory: A class that is responsible for creating new,
50
properly configured ServiceHandler instance for each request. The
51
factory is configured by providing it with a set of RPCMapper instances.
52
When the Webapp framework invokes the service handler, the handler
53
creates a new service class instance. The service class instance is
54
provided with a reference to the handler. A single instance of an
55
RPCMapper sub-class is required to configure each service. Each mapper
56
instance must be usable across multiple requests.
60
RPC mappers translate between a single HTTP based RPC protocol and the
61
underlying service implementation. Each RPC mapper must configured
62
with the following information to determine if it is an appropriate
63
mapper for a given request:
65
http_methods: Set of HTTP methods supported by handler.
67
content_types: Set of supported content types.
69
default_content_type: Default content type for handler responses.
71
Built-in mapper implementations:
73
URLEncodedRPCMapper: Matches requests that are compatible with post
74
forms with the 'application/x-www-form-urlencoded' content-type
75
(this content type is the default if none is specified. It
76
translates post parameters into request parameters.
78
ProtobufRPCMapper: Matches requests that are compatible with post
79
forms with the 'application/x-google-protobuf' content-type. It
80
reads the contents of a binary post request.
83
Error: Base class for service handler errors.
84
ServiceConfigurationError: Raised when a service not correctly configured.
85
RequestError: Raised by RPC mappers when there is an error in its request
87
ResponseError: Raised by RPC mappers when there is an error in its response.
90
__author__ = 'rafek@google.com (Rafe Kaplan)'
104
from google.appengine.ext import webapp
105
from google.appengine.ext.webapp import util as webapp_util
106
from .. import messages
107
from .. import protobuf
108
from .. import protojson
109
from .. import protourlencode
110
from .. import registry
111
from .. import remote
119
'ServiceConfigurationError',
121
'DEFAULT_REGISTRY_PATH',
126
'ServiceHandlerFactory',
127
'URLEncodedRPCMapper',
134
class Error(Exception):
135
"""Base class for all errors in service handlers module."""
138
class ServiceConfigurationError(Error):
139
"""When service configuration is incorrect."""
142
class RequestError(Error):
143
"""Error occurred when building request."""
146
class ResponseError(Error):
147
"""Error occurred when building response."""
150
_URLENCODED_CONTENT_TYPE = protourlencode.CONTENT_TYPE
151
_PROTOBUF_CONTENT_TYPE = protobuf.CONTENT_TYPE
152
_JSON_CONTENT_TYPE = protojson.CONTENT_TYPE
154
_EXTRA_JSON_CONTENT_TYPES = ['application/x-javascript',
161
# The whole method pattern is an optional regex. It contains a single
162
# group used for mapping to the query parameter. This is passed to the
163
# parameters of 'get' and 'post' on the ServiceHandler.
164
_METHOD_PATTERN = r'(?:\.([^?]*))?'
166
DEFAULT_REGISTRY_PATH = forms.DEFAULT_REGISTRY_PATH
169
class RPCMapper(object):
170
"""Interface to mediate between request and service object.
172
Request mappers are implemented to support various types of
173
RPC protocols. It is responsible for identifying whether a
174
given request matches a particular protocol, resolve the remote
175
method to invoke and mediate between the request and appropriate
176
protocol messages for the remote method.
182
default_content_type,
188
http_methods: Set of HTTP methods supported by mapper.
189
default_content_type: Default content type supported by mapper.
190
protocol: The protocol implementation. Must implement encode_message and
192
content_types: Set of additionally supported content types.
194
self.__http_methods = frozenset(http_methods)
195
self.__default_content_type = default_content_type
196
self.__protocol = protocol
198
if content_types is None:
200
self.__content_types = frozenset([self.__default_content_type] +
204
def http_methods(self):
205
return self.__http_methods
208
def default_content_type(self):
209
return self.__default_content_type
212
def content_types(self):
213
return self.__content_types
215
def build_request(self, handler, request_type):
216
"""Build request message based on request.
218
Each request mapper implementation is responsible for converting a
219
request to an appropriate message instance.
222
handler: RequestHandler instance that is servicing request.
223
Must be initialized with request object and been previously determined
224
to matching the protocol of the RPCMapper.
225
request_type: Message type to build.
228
Instance of request_type populated by protocol buffer in request body.
231
RequestError if the mapper implementation is not able to correctly
232
convert the request to the appropriate message.
235
return self.__protocol.decode_message(request_type, handler.request.body)
236
except (messages.ValidationError, messages.DecodeError), err:
237
raise RequestError('Unable to parse request content: %s' % err)
239
def build_response(self, handler, response, pad_string=False):
240
"""Build response based on service object response message.
242
Each request mapper implementation is responsible for converting a
243
response message to an appropriate handler response.
246
handler: RequestHandler instance that is servicing request.
247
Must be initialized with request object and been previously determined
248
to matching the protocol of the RPCMapper.
249
response: Response message as returned from the service object.
252
ResponseError if the mapper implementation is not able to correctly
253
convert the message to an appropriate response.
256
encoded_message = self.__protocol.encode_message(response)
257
except messages.ValidationError, err:
258
raise ResponseError('Unable to encode message: %s' % err)
260
handler.response.headers['Content-Type'] = self.default_content_type
261
handler.response.out.write(encoded_message)
264
class ServiceHandlerFactory(object):
265
"""Factory class used for instantiating new service handlers.
267
Normally a handler class is passed directly to the webapp framework
268
so that it can be simply instantiated to handle a single request.
269
The service handler, however, must be configured with additional
270
information so that it knows how to instantiate a service object.
271
This class acts the same as a normal RequestHandler class by
272
overriding the __call__ method to correctly configures a ServiceHandler
273
instance with a new service object.
275
The factory must also provide a set of RPCMapper instances which
276
examine a request to determine what protocol is being used and mediates
277
between the request and the service object.
279
The mapping of a service handler must have a single group indicating the
280
part of the URL path that maps to the request method. This group must
281
exist but can be optional for the request (the group may be followed by
282
'?' in the regular expression matching the request).
286
stock_factory = ServiceHandlerFactory(StockService)
287
... configure stock_factory by adding RPCMapper instances ...
289
application = webapp.WSGIApplication(
290
[stock_factory.mapping('/stocks')])
294
application = webapp.WSGIApplication(
295
[ServiceHandlerFactory.default(StockService).mapping('/stocks')])
298
def __init__(self, service_factory):
302
service_factory: Service factory to instantiate and provide to
305
self.__service_factory = service_factory
306
self.__request_mappers = []
308
def all_request_mappers(self):
309
"""Get all request mappers.
312
Iterator of all request mappers used by this service factory.
314
return iter(self.__request_mappers)
316
def add_request_mapper(self, mapper):
317
"""Add request mapper to end of request mapper list."""
318
self.__request_mappers.append(mapper)
321
"""Construct a new service handler instance."""
322
return ServiceHandler(self, self.__service_factory())
325
def service_factory(self):
326
"""Service factory associated with this factory."""
327
return self.__service_factory
330
def __check_path(path):
331
"""Check a path parameter.
333
Make sure a provided path parameter is compatible with the
337
path: Path to check. This is a plain path, not a regular expression.
340
ValueError if path does not start with /, path ends with /.
342
if path.endswith('/'):
343
raise ValueError('Path %s must not end with /.' % path)
345
def mapping(self, path):
346
"""Convenience method to map service to application.
349
path: Path to map service to. It must be a simple path
350
with a leading / and no trailing /.
353
Mapping from service URL to service handler factory.
355
self.__check_path(path)
357
service_url_pattern = r'(%s)%s' % (path, _METHOD_PATTERN)
359
return service_url_pattern, self
362
def default(cls, service_factory, parameter_prefix=''):
363
"""Convenience method to map default factory configuration to application.
365
Creates a standardized default service factory configuration that pre-maps
366
the URL encoded protocol handler to the factory.
369
service_factory: Service factory to instantiate and provide to
371
method_parameter: The name of the form parameter used to determine the
372
method to invoke used by the URLEncodedRPCMapper. If None, no
373
parameter is used and the mapper will only match against the form
374
path-name. Defaults to 'method'.
375
parameter_prefix: If provided, all the parameters in the form are
376
expected to begin with that prefix by the URLEncodedRPCMapper.
379
Mapping from service URL to service handler factory.
381
factory = cls(service_factory)
383
factory.add_request_mapper(ProtobufRPCMapper())
384
factory.add_request_mapper(JSONRPCMapper())
389
class ServiceHandler(webapp.RequestHandler):
390
"""Web handler for RPC service.
393
get: All requests handled by 'handle' method. HTTP method stored in
394
attribute. Takes remote_method parameter as derived from the URL mapping.
395
post: All requests handled by 'handle' method. HTTP method stored in
396
attribute. Takes remote_method parameter as derived from the URL mapping.
397
redirect: Not implemented for this service handler.
400
handle: Handle request for both GET and POST.
402
Attributes (in addition to attributes in RequestHandler):
403
service: Service instance associated with request being handled.
404
method: Method of request. Used by RPCMapper to determine match.
405
remote_method: Sub-path as provided to the 'get' and 'post' methods.
408
def __init__(self, factory, service):
412
factory: Instance of ServiceFactory used for constructing new service
413
instances used for handling requests.
414
service: Service instance used for handling RPC.
416
self.__factory = factory
417
self.__service = service
421
return self.__service
423
def __show_info(self, service_path, remote_method):
424
self.response.headers['content-type'] = 'text/plain; charset=utf-8'
425
response_message = []
427
response_message.append('%s.%s is a ProtoRPC method.\n\n' %(
428
service_path, remote_method))
430
response_message.append('%s is a ProtoRPC service.\n\n' % service_path)
431
definition_name_function = getattr(self.__service, 'definition_name', None)
432
if definition_name_function:
433
definition_name = definition_name_function()
435
definition_name = '%s.%s' % (self.__service.__module__,
436
self.__service.__class__.__name__)
438
response_message.append('Service %s\n\n' % definition_name)
439
response_message.append('More about ProtoRPC: ')
441
response_message.append('http://code.google.com/p/google-protorpc\n')
442
self.response.out.write(util.pad_string(''.join(response_message)))
444
def get(self, service_path, remote_method):
445
"""Handler method for GET requests.
448
service_path: Service path derived from request URL.
449
remote_method: Sub-path after service path has been matched.
451
self.handle('GET', service_path, remote_method)
453
def post(self, service_path, remote_method):
454
"""Handler method for POST requests.
457
service_path: Service path derived from request URL.
458
remote_method: Sub-path after service path has been matched.
460
self.handle('POST', service_path, remote_method)
462
def redirect(self, uri, permanent=False):
463
"""Not supported for services."""
464
raise NotImplementedError('Services do not currently support redirection.')
466
def __send_error(self,
472
status = remote.RpcStatus(state=status_state,
473
error_message=error_message,
474
error_name=error_name)
475
mapper.build_response(self, status)
476
self.response.headers['content-type'] = mapper.default_content_type
478
logging.error(error_message)
479
response_content = self.response.out.getvalue()
480
padding = ' ' * max(0, 512 - len(response_content))
481
self.response.out.write(padding)
483
self.response.set_status(http_code, error_message)
485
def __send_simple_error(self, code, message, pad=True):
486
"""Send error to caller without embedded message."""
487
self.response.headers['content-type'] = 'text/plain; charset=utf-8'
488
logging.error(message)
489
self.response.set_status(code, message)
491
response_message = httplib.responses.get(code, 'Unknown Error')
493
response_message = util.pad_string(response_message)
494
self.response.out.write(response_message)
496
def __get_content_type(self):
497
content_type = self.request.headers.get('content-type', None)
499
content_type = self.request.environ.get('HTTP_CONTENT_TYPE', None)
503
# Lop off parameters from the end (for example content-encoding)
504
return content_type.split(';', 1)[0].lower()
506
def __headers(self, content_type):
507
for name in self.request.headers:
509
if name == 'content-type':
511
elif name == 'content-length':
512
value = str(len(self.request.body))
514
value = self.request.headers.get(name, '')
517
def handle(self, http_method, service_path, remote_method):
518
"""Handle a service request.
520
The handle method will handle either a GET or POST response.
521
It is up to the individual mappers from the handler factory to determine
522
which request methods they can service.
524
If the protocol is not recognized, the request does not provide a correct
525
request for that protocol or the service object does not support the
526
requested RPC method, will return error code 400 in the response.
529
http_method: HTTP method of request.
530
service_path: Service path derived from request URL.
531
remote_method: Sub-path after service path has been matched.
533
self.response.headers['x-content-type-options'] = 'nosniff'
534
if not remote_method and http_method == 'GET':
535
# Special case a normal get request, presumably via a browser.
537
self.__show_info(service_path, remote_method)
540
content_type = self.__get_content_type()
542
# Provide server state to the service. If the service object does not have
543
# an "initialize_request_state" method, will not attempt to assign state.
545
state_initializer = self.service.initialize_request_state
546
except AttributeError:
549
server_port = self.request.environ.get('SERVER_PORT', None)
551
server_port = int(server_port)
553
request_state = remote.HttpRequestState(
554
remote_host=self.request.environ.get('REMOTE_HOST', None),
555
remote_address=self.request.environ.get('REMOTE_ADDR', None),
556
server_host=self.request.environ.get('SERVER_HOST', None),
557
server_port=server_port,
558
http_method=http_method,
559
service_path=service_path,
560
headers=list(self.__headers(content_type)))
561
state_initializer(request_state)
564
self.__send_simple_error(400, 'Invalid RPC request: missing content-type')
567
# Search for mapper to mediate request.
568
for mapper in self.__factory.all_request_mappers():
569
if content_type in mapper.content_types:
572
if http_method == 'GET':
573
self.error(httplib.UNSUPPORTED_MEDIA_TYPE)
574
self.__show_info(service_path, remote_method)
576
self.__send_simple_error(httplib.UNSUPPORTED_MEDIA_TYPE,
577
'Unsupported content-type: %s' % content_type)
581
if http_method not in mapper.http_methods:
582
if http_method == 'GET':
583
self.error(httplib.METHOD_NOT_ALLOWED)
584
self.__show_info(service_path, remote_method)
586
self.__send_simple_error(httplib.METHOD_NOT_ALLOWED,
587
'Unsupported HTTP method: %s' % http_method)
592
method = getattr(self.service, remote_method)
593
method_info = method.remote
594
except AttributeError, err:
596
400, remote.RpcState.METHOD_NOT_FOUND_ERROR,
597
'Unrecognized RPC method: %s' % remote_method,
601
request = mapper.build_request(self, method_info.request_type)
602
except (RequestError, messages.DecodeError), err:
603
self.__send_error(400,
604
remote.RpcState.REQUEST_ERROR,
605
'Error parsing ProtoRPC request (%s)' % err,
610
response = method(request)
611
except remote.ApplicationError, err:
612
self.__send_error(400,
613
remote.RpcState.APPLICATION_ERROR,
619
mapper.build_response(self, response)
620
except Exception, err:
621
logging.error('An unexpected error occured when handling RPC: %s',
624
self.__send_error(500,
625
remote.RpcState.SERVER_ERROR,
626
'Internal Server Error',
631
# TODO(rafek): Support tag-id only forms.
632
class URLEncodedRPCMapper(RPCMapper):
633
"""Request mapper for application/x-www-form-urlencoded forms.
635
This mapper is useful for building forms that can invoke RPC. Many services
636
are also configured to work using URL encoded request information because
637
of its perceived ease of programming and debugging.
639
The mapper must be provided with at least method_parameter or
640
remote_method_pattern so that it is possible to determine how to determine the
641
requests RPC method. If both are provided, the service will respond to both
642
method request types, however, only one may be present in a given request.
643
If both types are detected, the request will not match.
646
def __init__(self, parameter_prefix=''):
650
parameter_prefix: If provided, all the parameters in the form are
651
expected to begin with that prefix.
653
# Private attributes:
654
# __parameter_prefix: parameter prefix as provided by constructor
656
super(URLEncodedRPCMapper, self).__init__(['POST'],
657
_URLENCODED_CONTENT_TYPE,
659
self.__parameter_prefix = parameter_prefix
661
def encode_message(self, message):
662
"""Encode a message using parameter prefix.
665
message: Message to URL Encode.
670
return protourlencode.encode_message(message,
671
prefix=self.__parameter_prefix)
674
def parameter_prefix(self):
675
"""Prefix all form parameters are expected to begin with."""
676
return self.__parameter_prefix
678
def build_request(self, handler, request_type):
679
"""Build request from URL encoded HTTP request.
681
Constructs message from names of URL encoded parameters. If this service
682
handler has a parameter prefix, parameters must begin with it or are
686
handler: RequestHandler instance that is servicing request.
687
request_type: Message type to build.
690
Instance of request_type populated by protocol buffer in request
694
RequestError if message type contains nested message field or repeated
695
message field. Will raise RequestError if there are any repeated
698
request = request_type()
699
builder = protourlencode.URLEncodedRequestBuilder(
700
request, prefix=self.__parameter_prefix)
701
for argument in sorted(handler.request.arguments()):
702
values = handler.request.get_all(argument)
704
builder.add_parameter(argument, values)
705
except messages.DecodeError, err:
706
raise RequestError(str(err))
710
class ProtobufRPCMapper(RPCMapper):
711
"""Request mapper for application/x-protobuf service requests.
713
This mapper will parse protocol buffer from a POST body and return the request
714
as a protocol buffer.
718
super(ProtobufRPCMapper, self).__init__(['POST'],
719
_PROTOBUF_CONTENT_TYPE,
723
class JSONRPCMapper(RPCMapper):
724
"""Request mapper for application/x-protobuf service requests.
726
This mapper will parse protocol buffer from a POST body and return the request
727
as a protocol buffer.
731
super(JSONRPCMapper, self).__init__(
735
content_types=_EXTRA_JSON_CONTENT_TYPES)
738
def service_mapping(services,
739
registry_path=DEFAULT_REGISTRY_PATH):
740
"""Create a services mapping for use with webapp.
742
Creates basic default configuration and registration for ProtoRPC services.
743
Each service listed in the service mapping has a standard service handler
744
factory created for it.
746
The list of mappings can either be an explicit path to service mapping or
747
just services. If mappings are just services, they will automatically
748
be mapped to their default name. For exampel:
750
package = 'my_package'
752
class MyService(remote.Service):
755
server_mapping([('/my_path', MyService), # Maps to /my_path
756
MyService, # Maps to /my_package/MyService
759
Specifying a service mapping:
761
Normally services are mapped to URL paths by specifying a tuple
763
path: The path the service resides on.
764
service: The service class or service factory for creating new instances
765
of the service. For more information about service factories, please
766
see remote.Service.new_factory.
768
If no tuple is provided, and therefore no path specified, a default path
769
is calculated by using the fully qualified service name using a URL path
770
separator for each of its components instead of a '.'.
773
services: Can be service type, service factory or string definition name of
774
service being mapped or list of tuples (path, service):
775
path: Path on server to map service to.
776
service: Service type, service factory or string definition name of
777
service being mapped.
778
Can also be a dict. If so, the keys are treated as the path and values as
780
registry_path: Path to give to registry service. Use None to disable
784
List of tuples defining a mapping of request handlers compatible with a
788
ServiceConfigurationError when duplicate paths are provided.
790
if isinstance(services, dict):
791
services = services.iteritems()
795
if registry_path is not None:
796
registry_service = registry.RegistryService.new_factory(registry_map)
797
services = list(services) + [(registry_path, registry_service)]
798
mapping.append((registry_path + r'/form(?:/)?',
799
forms.FormsHandler.new_factory(registry_path)))
800
mapping.append((registry_path + r'/form/(.+)', forms.ResourceHandler))
803
for service_item in services:
804
infer_path = not isinstance(service_item, (list, tuple))
806
service = service_item
808
service = service_item[1]
810
service_class = getattr(service, 'service_class', service)
813
path = '/' + service_class.definition_name().replace('.', '/')
815
path = service_item[0]
818
raise ServiceConfigurationError(
819
'Path %r is already defined in service mapping' % path.encode('utf-8'))
823
# Create service mapping for webapp.
824
new_mapping = ServiceHandlerFactory.default(service).mapping(path)
825
mapping.append(new_mapping)
827
# Update registry with service class.
828
registry_map[path] = service_class
833
def run_services(services,
834
registry_path=DEFAULT_REGISTRY_PATH):
835
"""Handle CGI request using service mapping.
838
Same as service_mapping.
840
mappings = service_mapping(services, registry_path=registry_path)
841
application = webapp.WSGIApplication(mappings)
842
webapp_util.run_wsgi_app(application)