thirdparty/google_appengine/google/appengine/ext/remote_api/handler.py
changeset 828 f5fd65cc3bf3
child 1278 a7766286a7be
equal deleted inserted replaced
827:88c186556a80 828:f5fd65cc3bf3
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     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 
       
    18 """A handler that exports various App Engine services over HTTP.
       
    19 
       
    20 You can export this handler in your app by adding it directly to app.yaml's
       
    21 list of handlers:
       
    22 
       
    23   handlers:
       
    24   - url: /remote_api
       
    25     script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
       
    26     login: admin
       
    27 
       
    28 Then, you can use remote_api_stub to remotely access services exported by this
       
    29 handler. See the documentation in remote_api_stub.py for details on how to do
       
    30 this.
       
    31 
       
    32 Using this handler without specifying "login: admin" would be extremely unwise.
       
    33 So unwise that the default handler insists on checking for itself.
       
    34 """
       
    35 
       
    36 
       
    37 
       
    38 
       
    39 
       
    40 import google
       
    41 import pickle
       
    42 import sha
       
    43 import wsgiref.handlers
       
    44 from google.appengine.api import api_base_pb
       
    45 from google.appengine.api import apiproxy_stub
       
    46 from google.appengine.api import apiproxy_stub_map
       
    47 from google.appengine.api import users
       
    48 from google.appengine.datastore import datastore_pb
       
    49 from google.appengine.ext import webapp
       
    50 from google.appengine.ext.remote_api import remote_api_pb
       
    51 from google.appengine.runtime import apiproxy_errors
       
    52 
       
    53 
       
    54 class RemoteDatastoreStub(apiproxy_stub.APIProxyStub):
       
    55   """Provides a stub that permits execution of stateful datastore queries.
       
    56 
       
    57   Some operations aren't possible using the standard interface. Notably,
       
    58   datastore RunQuery operations internally store a cursor that is referenced in
       
    59   later Next calls, and cleaned up at the end of each request. Because every
       
    60   call to ApiCallHandler takes place in its own request, this isn't possible.
       
    61 
       
    62   To work around this, RemoteDatastoreStub provides its own implementation of
       
    63   RunQuery that immediately returns the query results.
       
    64   """
       
    65 
       
    66   def _Dynamic_RunQuery(self, request, response):
       
    67     """Handle a RunQuery request.
       
    68 
       
    69     We handle RunQuery by executing a Query and a Next and returning the result
       
    70     of the Next request.
       
    71     """
       
    72     runquery_response = datastore_pb.QueryResult()
       
    73     apiproxy_stub_map.MakeSyncCall('datastore_v3', 'RunQuery',
       
    74                                    request, runquery_response)
       
    75     next_request = datastore_pb.NextRequest()
       
    76     next_request.mutable_cursor().CopyFrom(runquery_response.cursor())
       
    77     next_request.set_count(request.limit())
       
    78     apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Next',
       
    79                                    next_request, response)
       
    80 
       
    81   def _Dynamic_Transaction(self, request, response):
       
    82     """Handle a Transaction request.
       
    83 
       
    84     We handle transactions by accumulating Put requests on the client end, as
       
    85     well as recording the key and hash of Get requests. When Commit is called,
       
    86     Transaction is invoked, which verifies that all the entities in the
       
    87     precondition list still exist and their hashes match, then performs a
       
    88     transaction of its own to make the updates.
       
    89     """
       
    90     tx = datastore_pb.Transaction()
       
    91     apiproxy_stub_map.MakeSyncCall('datastore_v3', 'BeginTransaction',
       
    92                                    api_base_pb.VoidProto(), tx)
       
    93 
       
    94     preconditions = request.precondition_list()
       
    95     if preconditions:
       
    96       get_request = datastore_pb.GetRequest()
       
    97       get_request.mutable_transaction().CopyFrom(tx)
       
    98       for precondition in preconditions:
       
    99         key = get_request.add_key()
       
   100         key.CopyFrom(precondition.key())
       
   101       get_response = datastore_pb.GetResponse()
       
   102       apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Get', get_request,
       
   103                                      get_response)
       
   104       entities = get_response.entity_list()
       
   105       assert len(entities) == request.precondition_size()
       
   106       for precondition, entity in zip(preconditions, entities):
       
   107         if precondition.has_hash() != entity.has_entity():
       
   108           raise apiproxy_errors.ApplicationError(
       
   109               datastore_pb.Error.CONCURRENT_TRANSACTION,
       
   110               "Transaction precondition failed.")
       
   111         elif entity.has_entity():
       
   112           entity_hash = sha.new(entity.entity().Encode()).digest()
       
   113           if precondition.hash() != entity_hash:
       
   114             raise apiproxy_errors.ApplicationError(
       
   115                 datastore_pb.Error.CONCURRENT_TRANSACTION,
       
   116                 "Transaction precondition failed.")
       
   117 
       
   118     if request.has_puts():
       
   119       put_request = request.puts()
       
   120       put_request.mutable_transaction().CopyFrom(tx)
       
   121       apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Put',
       
   122                                      put_request, response)
       
   123 
       
   124     if request.has_deletes():
       
   125       delete_request = request.deletes()
       
   126       delete_request.mutable_transaction().CopyFrom(tx)
       
   127       apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Delete',
       
   128                                      delete_request, api_base_pb.VoidProto())
       
   129 
       
   130     apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Commit', tx,
       
   131                                    api_base_pb.VoidProto())
       
   132 
       
   133   def _Dynamic_GetIDs(self, request, response):
       
   134     """Fetch unique IDs for a set of paths."""
       
   135     for entity in request.entity_list():
       
   136       assert entity.property_size() == 0
       
   137       assert entity.raw_property_size() == 0
       
   138       assert entity.entity_group().element_size() == 0
       
   139       lastpart = entity.key().path().element_list()[-1]
       
   140       assert lastpart.id() == 0 and not lastpart.has_name()
       
   141 
       
   142     tx = datastore_pb.Transaction()
       
   143     apiproxy_stub_map.MakeSyncCall('datastore_v3', 'BeginTransaction',
       
   144                                    api_base_pb.VoidProto(), tx)
       
   145 
       
   146     apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Put', request, response)
       
   147 
       
   148     apiproxy_stub_map.MakeSyncCall('datastore_v3', 'Rollback', tx,
       
   149                                    api_base_pb.VoidProto())
       
   150 
       
   151 
       
   152 SERVICE_PB_MAP = {
       
   153     'datastore_v3': {
       
   154         'Get': (datastore_pb.GetRequest, datastore_pb.GetResponse),
       
   155         'Put': (datastore_pb.PutRequest, datastore_pb.PutResponse),
       
   156         'Delete': (datastore_pb.DeleteRequest, datastore_pb.DeleteResponse),
       
   157         'Count': (datastore_pb.Query, api_base_pb.Integer64Proto),
       
   158         'GetIndices': (api_base_pb.StringProto, datastore_pb.CompositeIndices),
       
   159     },
       
   160     'remote_datastore': {
       
   161         'RunQuery': (datastore_pb.Query, datastore_pb.QueryResult),
       
   162         'Transaction': (remote_api_pb.TransactionRequest,
       
   163                              datastore_pb.PutResponse),
       
   164         'GetIDs': (remote_api_pb.PutRequest, datastore_pb.PutResponse),
       
   165     },
       
   166 }
       
   167 
       
   168 
       
   169 class ApiCallHandler(webapp.RequestHandler):
       
   170   """A webapp handler that accepts API calls over HTTP and executes them."""
       
   171 
       
   172   LOCAL_STUBS = {
       
   173       'remote_datastore': RemoteDatastoreStub('remote_datastore'),
       
   174   }
       
   175 
       
   176   def CheckIsAdmin(self):
       
   177     if not users.is_current_user_admin():
       
   178       self.response.set_status(401)
       
   179       self.response.out.write(
       
   180           "You must be logged in as an administrator to access this.")
       
   181       self.response.headers['Content-Type'] = 'text/plain'
       
   182       return False
       
   183     elif 'X-appcfg-api-version' not in self.request.headers:
       
   184       self.response.set_status(403)
       
   185       self.response.out.write("This request did not contain a necessary header")
       
   186       return False
       
   187     return True
       
   188 
       
   189 
       
   190   def get(self):
       
   191     """Handle a GET. Just show an info page."""
       
   192     if not self.CheckIsAdmin():
       
   193       return
       
   194 
       
   195     page = self.InfoPage()
       
   196     self.response.out.write(page)
       
   197 
       
   198   def post(self):
       
   199     """Handle POST requests by executing the API call."""
       
   200     if not self.CheckIsAdmin():
       
   201       return
       
   202 
       
   203     self.response.headers['Content-Type'] = 'application/octet-stream'
       
   204     response = remote_api_pb.Response()
       
   205     try:
       
   206       request = remote_api_pb.Request()
       
   207       request.ParseFromString(self.request.body)
       
   208       response_data = self.ExecuteRequest(request)
       
   209       response.mutable_response().set_contents(response_data.Encode())
       
   210       self.response.set_status(200)
       
   211     except Exception, e:
       
   212       self.response.set_status(500)
       
   213       response.mutable_exception().set_contents(pickle.dumps(e))
       
   214     self.response.out.write(response.Encode())
       
   215 
       
   216   def ExecuteRequest(self, request):
       
   217     """Executes an API invocation and returns the response object."""
       
   218     service = request.service_name()
       
   219     method = request.method()
       
   220     service_methods = SERVICE_PB_MAP.get(service, {})
       
   221     request_class, response_class = service_methods.get(method, (None, None))
       
   222     if not request_class:
       
   223       raise apiproxy_errors.CallNotFoundError()
       
   224 
       
   225     request_data = request_class()
       
   226     request_data.ParseFromString(request.request().contents())
       
   227     response_data = response_class()
       
   228 
       
   229     if service in self.LOCAL_STUBS:
       
   230       self.LOCAL_STUBS[service].MakeSyncCall(service, method, request_data,
       
   231                                              response_data)
       
   232     else:
       
   233       apiproxy_stub_map.MakeSyncCall(service, method, request_data,
       
   234                                      response_data)
       
   235 
       
   236     return response_data
       
   237 
       
   238   def InfoPage(self):
       
   239     """Renders an information page."""
       
   240     return """
       
   241 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
       
   242  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
       
   243 <html><head>
       
   244 <title>App Engine API endpoint.</title>
       
   245 </head><body>
       
   246 <h1>App Engine API endpoint.</h1>
       
   247 <p>This is an endpoint for the App Engine remote API interface.
       
   248 Point your stubs (google.appengine.ext.remote_api.remote_api_stub) here.</p>
       
   249 </body>
       
   250 </html>"""
       
   251 
       
   252 
       
   253 def main():
       
   254   application = webapp.WSGIApplication([('.*', ApiCallHandler)])
       
   255   wsgiref.handlers.CGIHandler().run(application)
       
   256 
       
   257 
       
   258 if __name__ == '__main__':
       
   259   main()