|
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() |