Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(356)

Side by Side Diff: appengine/ephelper/epfrontend/service.go

Issue 1750143003: Remove ephelper and other endpoints code. (Closed) Base URL: https://github.com/luci/luci-go@master
Patch Set: Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « appengine/ephelper/epfrontend/json.go ('k') | appengine/ephelper/epfrontend/service_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package epfrontend
6
7 import (
8 "bytes"
9 "encoding/json"
10 "fmt"
11 "io/ioutil"
12 "net/http"
13 "strconv"
14 "strings"
15
16 "github.com/GoogleCloudPlatform/go-endpoints/endpoints"
17 "github.com/luci/luci-go/common/logging"
18 "github.com/luci/luci-go/common/paniccatcher"
19 )
20
21 const (
22 // DefaultServerRoot is the default Server root value.
23 DefaultServerRoot = "/_ah/api/"
24 )
25
26 var (
27 errMethodNotAllowed = endpoints.NewAPIError(http.StatusText(http.StatusM ethodNotAllowed),
28 "", http.StatusMethodNotAllowed)
29 )
30
31 type handlerFuncWithError func(http.ResponseWriter, *http.Request) error
32
33 // endpoint is a resolved backend endpoint.
34 type endpoint struct {
35 // desc is the resolved service descriptor.
36 desc *endpoints.APIDescriptor
37 // method is the resolved method within the service.
38 method *endpoints.APIMethod
39 // backendPath is the backend path of this method.
40 backendPath string
41 }
42
43 type endpointNode struct {
44 // parent points to this endpointNode's parent node. If nil, this is the
45 // root endpoint node.
46 parent *endpointNode
47
48 // component is the path component's value.
49 //
50 // If this is an empty string, this is a query parameter component.
51 component string
52 // paramName is the query parameter name for this node. It is valid if
53 // component is "".
54 paramName string
55 // ep is the endpoint attached to this node. It can be nil if no endpoin t is
56 // attached.
57 ep *endpoint
58
59 children map[string]*endpointNode
60 }
61
62 func (n *endpointNode) addChild(components []string, ep *endpoint) error {
63 comp := components[0]
64 paramName := ""
65
66 if strings.HasPrefix(comp, "{") && strings.HasSuffix(comp, "}") {
67 // Replace query parameter with empty string.
68 paramName = comp[1 : len(comp)-1]
69 comp = ""
70 }
71
72 child := n.children[comp]
73 if child == nil {
74 if n.children == nil {
75 n.children = map[string]*endpointNode{}
76 }
77
78 child = &endpointNode{
79 parent: n,
80 component: comp,
81 paramName: paramName,
82 }
83 n.children[child.component] = child
84 }
85
86 if len(components) > 1 {
87 return child.addChild(components[1:], ep)
88 }
89
90 child.ep = ep
91 return nil
92 }
93
94 func (n *endpointNode) lookup(components ...string) *endpointNode {
95 if len(components) == 0 {
96 if n.ep == nil {
97 // Not a leaf node!
98 return nil
99 }
100 return n
101 }
102
103 // Is this node parent to a query parameter?
104 if child := n.children[""]; child != nil {
105 // Assume that this component is a query parameter. Can we resol ve against
106 // the remainder of the tree?
107 if cn := child.lookup(components[1:]...); cn != nil {
108 return cn
109 }
110 }
111
112 // Use this node as a direct value.
113 if child := n.children[components[0]]; child != nil {
114 return child.lookup(components[1:]...)
115 }
116 return nil
117 }
118
119 // serviceCall is a stateful structure that will be populated with the service
120 // call's parameters as they are resolved.
121 type serviceCall struct {
122 // path is the initial service call path.
123 path string
124
125 // ep is the resolved endpoint description for this call.
126 ep *endpoint
127 // params is the map of key/value query parameters populated during
128 // resolution.
129 params map[string]interface{}
130 }
131
132 func (c *serviceCall) addParam(k string, v interface{}) {
133 if c.params == nil {
134 c.params = map[string]interface{}{}
135 }
136 c.params[k] = v
137 }
138
139 // Server is an HTTP handler
140 type Server struct {
141 // Logger is a function that is called to obtain a logger for the curren t
142 // http.Request. Logger will be called with a nil http.Request when a lo gger
143 // is needed outside of the scope of request handling.
144 //
145 // If Logger is nil, or if Logger returns nil, a logging.Null logger wil l be
146 // used.
147 Logger func(*http.Request) logging.Logger
148
149 // root is the root path of this Server.
150 root string
151 // backend is the endpoints Server instance to bind to.
152 backend http.Handler
153 // logger is the resolved no-context logger.
154 logger logging.Logger
155 // endpoints is a map of method to endpoint path component tree.
156 endpoints map[string]*endpointNode
157 // services is a map of registered services.
158 services map[string]*endpoints.APIDescriptor
159 // paths is a map of direct paths and their handler functions.
160 paths map[string]handlerFuncWithError
161 }
162
163 // New instantiates a new Server instance.
164 //
165 // If root is the empty string, DefaultServerRoot will be used.
166 // If backend is nil, endpoints.DefaultServer will be used.
167 func New(root string, backend http.Handler) *Server {
168 if root == "" {
169 root = DefaultServerRoot
170 }
171 if backend == nil {
172 backend = endpoints.DefaultServer
173 }
174
175 s := &Server{
176 root: root,
177 backend: backend,
178 }
179 s.logger = s.getLogger(nil)
180
181 s.registerPath(safeURLPathJoin("discovery", "v1", "apis"), s.serveGetJSO N(s.handleDirectoryList))
182 s.registerPath(safeURLPathJoin("explorer"), func(w http.ResponseWriter, r *http.Request) error {
183 http.Redirect(w, r, fmt.Sprintf("http://apis-explorer.appspot.co m/apis-explorer/?base=%s",
184 safeURLPathJoin(getHostURL(r).String(), s.root)), http.S tatusFound)
185 return nil
186 })
187 return s
188 }
189
190 // RegisterService registers an endpoints service's descriptor with the Server.
191 func (s *Server) RegisterService(svc *endpoints.RPCService) error {
192 desc := endpoints.APIDescriptor{}
193 if err := svc.APIDescriptor(&desc, "epfrontend"); err != nil {
194 return err
195 }
196 if _, ok := s.services[desc.Name]; ok {
197 return fmt.Errorf("service [%s] already registered", desc.Name)
198 }
199 if s.services == nil {
200 s.services = map[string]*endpoints.APIDescriptor{}
201 }
202 s.services[desc.Name] = &desc
203
204 // Traverse the method map.
205 apibase := []string{desc.Name, desc.Version}
206 for _, m := range desc.Methods {
207 // Build our API path: service/version / method/path/components
208 parts := strings.Split(m.Path, "/")
209 apipath := make([]string, 0, len(apibase)+len(parts))
210 apipath = append(apipath, apibase...)
211 apipath = append(apipath, parts...)
212
213 ep := endpoint{
214 desc: &desc,
215 method: m,
216 backendPath: m.RosyMethod,
217 }
218 if err := s.registerEndpoint(m.HTTPMethod, apipath, &ep); err != nil {
219 return err
220 }
221 }
222
223 s.registerPath(safeURLPathJoin("discovery", "v1", "apis", desc.Name, des c.Version, "rest"),
224 s.serveGetJSON(func(r *http.Request) (interface{}, error) {
225 return s.handleRestDescription(r, &desc)
226 }))
227 return nil
228 }
229
230 func (s *Server) registerEndpoint(method string, apipath []string, ep *endpoint) error {
231 s.logger.Debugf("Registering %s %s => %#v", method, apipath, ep)
232 if s.endpoints == nil {
233 s.endpoints = map[string]*endpointNode{}
234 }
235
236 methodNode := s.endpoints[method]
237 if methodNode == nil {
238 methodNode = &endpointNode{}
239 s.endpoints[method] = methodNode
240 }
241
242 if n := methodNode.lookup(apipath...); n != nil {
243 return fmt.Errorf("%s method [%s] is already registered", method , apipath)
244 }
245 return methodNode.addChild(apipath, ep)
246 }
247
248 func (s *Server) registerPath(path string, handler handlerFuncWithError) {
249 if s.paths == nil {
250 s.paths = map[string]handlerFuncWithError{}
251 }
252 s.paths[path] = handler
253 }
254
255 // HandleHTTP adds Server s to specified http.ServeMux.
256 // If no mux is provided, http.DefaultServeMux will be used.
257 func (s *Server) HandleHTTP(mux *http.ServeMux) {
258 if mux == nil {
259 mux = http.DefaultServeMux
260 }
261 mux.Handle(s.root, s)
262 }
263
264 // ServeHTTP is the Server's implementation of the http.Handler interface.
265 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
266 h := requestHandler{
267 Server: s,
268 logger: s.getLogger(r),
269 }
270 h.handle(w, r)
271 }
272
273 // serveGetJSON returns an HTTP handler that serves the marshalled JSON form of
274 // the specified object.
275 func (s *Server) serveGetJSON(f func(*http.Request) (interface{}, error)) handle rFuncWithError {
276 return func(w http.ResponseWriter, req *http.Request) error {
277 if req.Method != "GET" {
278 return errMethodNotAllowed
279 }
280
281 i, err := f(req)
282 if err != nil {
283 return err
284 }
285
286 data, err := json.MarshalIndent(i, "", " ")
287 if err != nil {
288 return err
289 }
290
291 w.Header().Add("Content-Type", "application/json")
292 _, err = w.Write(data)
293 return err
294 }
295 }
296
297 func (s *Server) getLogger(r *http.Request) (l logging.Logger) {
298 if s.Logger != nil {
299 l = s.Logger(r)
300 }
301 if l == nil {
302 l = logging.Null()
303 }
304 return
305 }
306
307 type requestHandler struct {
308 *Server
309
310 // logger shadows Server.logger and is bound to an http.Request.
311 logger logging.Logger
312 }
313
314 func (h *requestHandler) handle(w http.ResponseWriter, r *http.Request) {
315 var err error
316 iw := &errorResponseWriter{ResponseWriter: w}
317 defer func() {
318 if err != nil {
319 iw.setError(err)
320 }
321 iw.forwardError()
322 }()
323
324 paniccatcher.Do(func() {
325 err = h.handleImpl(iw, r)
326 }, func(p *paniccatcher.Panic) {
327 h.logger.Errorf("Panic during endpoint handling: %s\n%s", p.Reas on, p.Stack)
328 err = endpoints.InternalServerError
329 })
330 }
331
332 func (h *requestHandler) handleImpl(w http.ResponseWriter, r *http.Request) erro r {
333 if r.Body != nil {
334 defer r.Body.Close()
335 }
336
337 // Strip our root from the request path, leaving the endpoint path.
338 if !strings.HasPrefix(r.URL.Path, h.root) {
339 h.logger.Debugf("Path (%s) does not begin with root (%s)", r.URL .Path, h.root)
340 return endpoints.NotFoundError
341 }
342 path := strings.TrimPrefix(r.URL.Path, h.root)
343 h.logger.Debugf("Received HTTP %s request [%s]: [%s]", r.Method, r.URL.P ath, path)
344
345 // If we have an explicit path handler set up for this path, use it.
346 if ph := h.paths[path]; ph != nil {
347 return ph(w, r)
348 }
349
350 // Resolve path to a service call.
351 call, err := h.resolveRequest(r.Method, path)
352 if err != nil {
353 h.logger.Warningf("Could not resolve %s request [%s] to a backen d endpoint: %v",
354 r.Method, path, err)
355 return err
356 }
357 // Add query strings to call parameters.
358 for k, vs := range r.URL.Query() {
359 // If there are multiple values, add the last.
360 if len(vs) > 0 {
361 param := vs[len(vs)-1]
362 v, err := convertQueryParam(k, param, call.ep.method)
363 if err != nil {
364 h.logger.Errorf("Could not convert query param % q (%q): %v", k, param, err)
365 return endpoints.BadRequestError
366 }
367 call.addParam(k, v)
368 }
369 }
370 h.logger.Debugf("Resolved %s request [%s] to endpoint [%s].", r.Method, path, call.ep.backendPath)
371
372 // Read the body for forwarding.
373 buf := bytes.Buffer{}
374 if _, err := buf.ReadFrom(r.Body); err != nil {
375 h.logger.Errorf("failed to read request body: %v", err)
376 return endpoints.InternalServerError
377 }
378
379 // Replace the body with a JSON object, if necessary.
380 if buf.Len() == 0 || len(call.params) > 0 {
381 data := map[string]*jsonRWRawMessage{}
382 if buf.Len() > 0 {
383 if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
384 h.logger.Errorf("Failed to unmarshal request JSO N: %v", err)
385 return endpoints.BadRequestError
386 }
387 }
388
389 for k, v := range call.params {
390 data[k] = &jsonRWRawMessage{
391 v: v,
392 }
393 }
394
395 buf.Reset()
396 m := json.NewEncoder(&buf)
397 if err := m.Encode(data); err != nil {
398 h.logger.Errorf("Failed to re-marshal request JSON: %v", err)
399 return endpoints.InternalServerError
400 }
401 }
402 r.Body = ioutil.NopCloser(&buf)
403
404 // Mutate our request to pretend it's a frontend-to-backend request, the n
405 // forward it to the backend for actual API processing!
406 //
407 // The actual Path prefix doesn't matter, as the service only looks at t he
408 // last component to derive the service call.
409 r.Method = "POST"
410 r.RequestURI = "/_ah/spi/" + call.ep.backendPath // Strips query paramet ers.
411 r.URL.Path = r.RequestURI
412 h.backend.ServeHTTP(w, r)
413 return nil
414 }
415
416 func (h *requestHandler) resolveRequest(method, path string) (*serviceCall, erro r) {
417 call := serviceCall{
418 path: path,
419 }
420
421 parts := strings.Split(path, "/")
422
423 h.logger.Debugf("Resolving %s request %s...", method, parts)
424 n := h.endpoints[method]
425 if n != nil {
426 n = n.lookup(parts...)
427 }
428 if n == nil {
429 return nil, endpoints.NotFoundError
430 }
431 ep := n.ep
432 call.ep = ep
433
434 // Build our query parameters by reverse-traversing the tree towards its root.
435 //
436 // The endpoints node path will have the same size as parts, since it ma tched.
437 for len(parts) > 0 {
438 if n.paramName != "" {
439 param := parts[len(parts)-1]
440 v, err := convertQueryParam(n.paramName, param, ep.metho d)
441 if err != nil {
442 h.logger.Errorf("Could not convert path param %q (%q): %v", n.paramName, param, err)
443 return nil, endpoints.BadRequestError
444 }
445 call.addParam(n.paramName, v)
446 }
447
448 parts = parts[:len(parts)-1]
449 n = n.parent
450 }
451
452 return &call, nil
453 }
454
455 func convertQueryParam(k, v string, m *endpoints.APIMethod) (interface{}, error) {
456 // Find the parameter for "k".
457 p := m.Request.Params[k]
458 if p == nil {
459 return v, nil
460 }
461
462 switch p.Type {
463 case "int64", "uint64":
464 // Assume all int64 are "string"-quoted integers. If there are q uotes, strip
465 // them. Return the result as a string.
466 if len(v) >= 2 && strings.HasPrefix(v, `"`) && strings.HasSuffix (v, `"`) {
467 v = v[1 : len(v)-1]
468 }
469 if _, err := strconv.ParseInt(v, 10, 64); err != nil {
470 return nil, err
471 }
472 return v, nil
473 case "int32", "uint32":
474 return strconv.ParseInt(v, 10, 32)
475 case "float", "double":
476 return strconv.ParseFloat(v, 64)
477 case "boolean":
478 switch v {
479 case "", "false":
480 return false, nil
481 default:
482 return true, nil
483 }
484 case "string", "bytes":
485 return v, nil
486
487 default:
488 return nil, fmt.Errorf("unknown type %q", p.Type)
489 }
490 }
OLDNEW
« no previous file with comments | « appengine/ephelper/epfrontend/json.go ('k') | appengine/ephelper/epfrontend/service_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698