Index: appengine/ephelper/epfrontend/service.go |
diff --git a/appengine/ephelper/epfrontend/service.go b/appengine/ephelper/epfrontend/service.go |
deleted file mode 100644 |
index dc42dcd308bb3bd89deab261dde56354a6a15c65..0000000000000000000000000000000000000000 |
--- a/appengine/ephelper/epfrontend/service.go |
+++ /dev/null |
@@ -1,490 +0,0 @@ |
-// Copyright 2015 The Chromium Authors. All rights reserved. |
-// Use of this source code is governed by a BSD-style license that can be |
-// found in the LICENSE file. |
- |
-package epfrontend |
- |
-import ( |
- "bytes" |
- "encoding/json" |
- "fmt" |
- "io/ioutil" |
- "net/http" |
- "strconv" |
- "strings" |
- |
- "github.com/GoogleCloudPlatform/go-endpoints/endpoints" |
- "github.com/luci/luci-go/common/logging" |
- "github.com/luci/luci-go/common/paniccatcher" |
-) |
- |
-const ( |
- // DefaultServerRoot is the default Server root value. |
- DefaultServerRoot = "/_ah/api/" |
-) |
- |
-var ( |
- errMethodNotAllowed = endpoints.NewAPIError(http.StatusText(http.StatusMethodNotAllowed), |
- "", http.StatusMethodNotAllowed) |
-) |
- |
-type handlerFuncWithError func(http.ResponseWriter, *http.Request) error |
- |
-// endpoint is a resolved backend endpoint. |
-type endpoint struct { |
- // desc is the resolved service descriptor. |
- desc *endpoints.APIDescriptor |
- // method is the resolved method within the service. |
- method *endpoints.APIMethod |
- // backendPath is the backend path of this method. |
- backendPath string |
-} |
- |
-type endpointNode struct { |
- // parent points to this endpointNode's parent node. If nil, this is the |
- // root endpoint node. |
- parent *endpointNode |
- |
- // component is the path component's value. |
- // |
- // If this is an empty string, this is a query parameter component. |
- component string |
- // paramName is the query parameter name for this node. It is valid if |
- // component is "". |
- paramName string |
- // ep is the endpoint attached to this node. It can be nil if no endpoint is |
- // attached. |
- ep *endpoint |
- |
- children map[string]*endpointNode |
-} |
- |
-func (n *endpointNode) addChild(components []string, ep *endpoint) error { |
- comp := components[0] |
- paramName := "" |
- |
- if strings.HasPrefix(comp, "{") && strings.HasSuffix(comp, "}") { |
- // Replace query parameter with empty string. |
- paramName = comp[1 : len(comp)-1] |
- comp = "" |
- } |
- |
- child := n.children[comp] |
- if child == nil { |
- if n.children == nil { |
- n.children = map[string]*endpointNode{} |
- } |
- |
- child = &endpointNode{ |
- parent: n, |
- component: comp, |
- paramName: paramName, |
- } |
- n.children[child.component] = child |
- } |
- |
- if len(components) > 1 { |
- return child.addChild(components[1:], ep) |
- } |
- |
- child.ep = ep |
- return nil |
-} |
- |
-func (n *endpointNode) lookup(components ...string) *endpointNode { |
- if len(components) == 0 { |
- if n.ep == nil { |
- // Not a leaf node! |
- return nil |
- } |
- return n |
- } |
- |
- // Is this node parent to a query parameter? |
- if child := n.children[""]; child != nil { |
- // Assume that this component is a query parameter. Can we resolve against |
- // the remainder of the tree? |
- if cn := child.lookup(components[1:]...); cn != nil { |
- return cn |
- } |
- } |
- |
- // Use this node as a direct value. |
- if child := n.children[components[0]]; child != nil { |
- return child.lookup(components[1:]...) |
- } |
- return nil |
-} |
- |
-// serviceCall is a stateful structure that will be populated with the service |
-// call's parameters as they are resolved. |
-type serviceCall struct { |
- // path is the initial service call path. |
- path string |
- |
- // ep is the resolved endpoint description for this call. |
- ep *endpoint |
- // params is the map of key/value query parameters populated during |
- // resolution. |
- params map[string]interface{} |
-} |
- |
-func (c *serviceCall) addParam(k string, v interface{}) { |
- if c.params == nil { |
- c.params = map[string]interface{}{} |
- } |
- c.params[k] = v |
-} |
- |
-// Server is an HTTP handler |
-type Server struct { |
- // Logger is a function that is called to obtain a logger for the current |
- // http.Request. Logger will be called with a nil http.Request when a logger |
- // is needed outside of the scope of request handling. |
- // |
- // If Logger is nil, or if Logger returns nil, a logging.Null logger will be |
- // used. |
- Logger func(*http.Request) logging.Logger |
- |
- // root is the root path of this Server. |
- root string |
- // backend is the endpoints Server instance to bind to. |
- backend http.Handler |
- // logger is the resolved no-context logger. |
- logger logging.Logger |
- // endpoints is a map of method to endpoint path component tree. |
- endpoints map[string]*endpointNode |
- // services is a map of registered services. |
- services map[string]*endpoints.APIDescriptor |
- // paths is a map of direct paths and their handler functions. |
- paths map[string]handlerFuncWithError |
-} |
- |
-// New instantiates a new Server instance. |
-// |
-// If root is the empty string, DefaultServerRoot will be used. |
-// If backend is nil, endpoints.DefaultServer will be used. |
-func New(root string, backend http.Handler) *Server { |
- if root == "" { |
- root = DefaultServerRoot |
- } |
- if backend == nil { |
- backend = endpoints.DefaultServer |
- } |
- |
- s := &Server{ |
- root: root, |
- backend: backend, |
- } |
- s.logger = s.getLogger(nil) |
- |
- s.registerPath(safeURLPathJoin("discovery", "v1", "apis"), s.serveGetJSON(s.handleDirectoryList)) |
- s.registerPath(safeURLPathJoin("explorer"), func(w http.ResponseWriter, r *http.Request) error { |
- http.Redirect(w, r, fmt.Sprintf("http://apis-explorer.appspot.com/apis-explorer/?base=%s", |
- safeURLPathJoin(getHostURL(r).String(), s.root)), http.StatusFound) |
- return nil |
- }) |
- return s |
-} |
- |
-// RegisterService registers an endpoints service's descriptor with the Server. |
-func (s *Server) RegisterService(svc *endpoints.RPCService) error { |
- desc := endpoints.APIDescriptor{} |
- if err := svc.APIDescriptor(&desc, "epfrontend"); err != nil { |
- return err |
- } |
- if _, ok := s.services[desc.Name]; ok { |
- return fmt.Errorf("service [%s] already registered", desc.Name) |
- } |
- if s.services == nil { |
- s.services = map[string]*endpoints.APIDescriptor{} |
- } |
- s.services[desc.Name] = &desc |
- |
- // Traverse the method map. |
- apibase := []string{desc.Name, desc.Version} |
- for _, m := range desc.Methods { |
- // Build our API path: service/version / method/path/components |
- parts := strings.Split(m.Path, "/") |
- apipath := make([]string, 0, len(apibase)+len(parts)) |
- apipath = append(apipath, apibase...) |
- apipath = append(apipath, parts...) |
- |
- ep := endpoint{ |
- desc: &desc, |
- method: m, |
- backendPath: m.RosyMethod, |
- } |
- if err := s.registerEndpoint(m.HTTPMethod, apipath, &ep); err != nil { |
- return err |
- } |
- } |
- |
- s.registerPath(safeURLPathJoin("discovery", "v1", "apis", desc.Name, desc.Version, "rest"), |
- s.serveGetJSON(func(r *http.Request) (interface{}, error) { |
- return s.handleRestDescription(r, &desc) |
- })) |
- return nil |
-} |
- |
-func (s *Server) registerEndpoint(method string, apipath []string, ep *endpoint) error { |
- s.logger.Debugf("Registering %s %s => %#v", method, apipath, ep) |
- if s.endpoints == nil { |
- s.endpoints = map[string]*endpointNode{} |
- } |
- |
- methodNode := s.endpoints[method] |
- if methodNode == nil { |
- methodNode = &endpointNode{} |
- s.endpoints[method] = methodNode |
- } |
- |
- if n := methodNode.lookup(apipath...); n != nil { |
- return fmt.Errorf("%s method [%s] is already registered", method, apipath) |
- } |
- return methodNode.addChild(apipath, ep) |
-} |
- |
-func (s *Server) registerPath(path string, handler handlerFuncWithError) { |
- if s.paths == nil { |
- s.paths = map[string]handlerFuncWithError{} |
- } |
- s.paths[path] = handler |
-} |
- |
-// HandleHTTP adds Server s to specified http.ServeMux. |
-// If no mux is provided, http.DefaultServeMux will be used. |
-func (s *Server) HandleHTTP(mux *http.ServeMux) { |
- if mux == nil { |
- mux = http.DefaultServeMux |
- } |
- mux.Handle(s.root, s) |
-} |
- |
-// ServeHTTP is the Server's implementation of the http.Handler interface. |
-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
- h := requestHandler{ |
- Server: s, |
- logger: s.getLogger(r), |
- } |
- h.handle(w, r) |
-} |
- |
-// serveGetJSON returns an HTTP handler that serves the marshalled JSON form of |
-// the specified object. |
-func (s *Server) serveGetJSON(f func(*http.Request) (interface{}, error)) handlerFuncWithError { |
- return func(w http.ResponseWriter, req *http.Request) error { |
- if req.Method != "GET" { |
- return errMethodNotAllowed |
- } |
- |
- i, err := f(req) |
- if err != nil { |
- return err |
- } |
- |
- data, err := json.MarshalIndent(i, "", " ") |
- if err != nil { |
- return err |
- } |
- |
- w.Header().Add("Content-Type", "application/json") |
- _, err = w.Write(data) |
- return err |
- } |
-} |
- |
-func (s *Server) getLogger(r *http.Request) (l logging.Logger) { |
- if s.Logger != nil { |
- l = s.Logger(r) |
- } |
- if l == nil { |
- l = logging.Null() |
- } |
- return |
-} |
- |
-type requestHandler struct { |
- *Server |
- |
- // logger shadows Server.logger and is bound to an http.Request. |
- logger logging.Logger |
-} |
- |
-func (h *requestHandler) handle(w http.ResponseWriter, r *http.Request) { |
- var err error |
- iw := &errorResponseWriter{ResponseWriter: w} |
- defer func() { |
- if err != nil { |
- iw.setError(err) |
- } |
- iw.forwardError() |
- }() |
- |
- paniccatcher.Do(func() { |
- err = h.handleImpl(iw, r) |
- }, func(p *paniccatcher.Panic) { |
- h.logger.Errorf("Panic during endpoint handling: %s\n%s", p.Reason, p.Stack) |
- err = endpoints.InternalServerError |
- }) |
-} |
- |
-func (h *requestHandler) handleImpl(w http.ResponseWriter, r *http.Request) error { |
- if r.Body != nil { |
- defer r.Body.Close() |
- } |
- |
- // Strip our root from the request path, leaving the endpoint path. |
- if !strings.HasPrefix(r.URL.Path, h.root) { |
- h.logger.Debugf("Path (%s) does not begin with root (%s)", r.URL.Path, h.root) |
- return endpoints.NotFoundError |
- } |
- path := strings.TrimPrefix(r.URL.Path, h.root) |
- h.logger.Debugf("Received HTTP %s request [%s]: [%s]", r.Method, r.URL.Path, path) |
- |
- // If we have an explicit path handler set up for this path, use it. |
- if ph := h.paths[path]; ph != nil { |
- return ph(w, r) |
- } |
- |
- // Resolve path to a service call. |
- call, err := h.resolveRequest(r.Method, path) |
- if err != nil { |
- h.logger.Warningf("Could not resolve %s request [%s] to a backend endpoint: %v", |
- r.Method, path, err) |
- return err |
- } |
- // Add query strings to call parameters. |
- for k, vs := range r.URL.Query() { |
- // If there are multiple values, add the last. |
- if len(vs) > 0 { |
- param := vs[len(vs)-1] |
- v, err := convertQueryParam(k, param, call.ep.method) |
- if err != nil { |
- h.logger.Errorf("Could not convert query param %q (%q): %v", k, param, err) |
- return endpoints.BadRequestError |
- } |
- call.addParam(k, v) |
- } |
- } |
- h.logger.Debugf("Resolved %s request [%s] to endpoint [%s].", r.Method, path, call.ep.backendPath) |
- |
- // Read the body for forwarding. |
- buf := bytes.Buffer{} |
- if _, err := buf.ReadFrom(r.Body); err != nil { |
- h.logger.Errorf("failed to read request body: %v", err) |
- return endpoints.InternalServerError |
- } |
- |
- // Replace the body with a JSON object, if necessary. |
- if buf.Len() == 0 || len(call.params) > 0 { |
- data := map[string]*jsonRWRawMessage{} |
- if buf.Len() > 0 { |
- if err := json.Unmarshal(buf.Bytes(), &data); err != nil { |
- h.logger.Errorf("Failed to unmarshal request JSON: %v", err) |
- return endpoints.BadRequestError |
- } |
- } |
- |
- for k, v := range call.params { |
- data[k] = &jsonRWRawMessage{ |
- v: v, |
- } |
- } |
- |
- buf.Reset() |
- m := json.NewEncoder(&buf) |
- if err := m.Encode(data); err != nil { |
- h.logger.Errorf("Failed to re-marshal request JSON: %v", err) |
- return endpoints.InternalServerError |
- } |
- } |
- r.Body = ioutil.NopCloser(&buf) |
- |
- // Mutate our request to pretend it's a frontend-to-backend request, then |
- // forward it to the backend for actual API processing! |
- // |
- // The actual Path prefix doesn't matter, as the service only looks at the |
- // last component to derive the service call. |
- r.Method = "POST" |
- r.RequestURI = "/_ah/spi/" + call.ep.backendPath // Strips query parameters. |
- r.URL.Path = r.RequestURI |
- h.backend.ServeHTTP(w, r) |
- return nil |
-} |
- |
-func (h *requestHandler) resolveRequest(method, path string) (*serviceCall, error) { |
- call := serviceCall{ |
- path: path, |
- } |
- |
- parts := strings.Split(path, "/") |
- |
- h.logger.Debugf("Resolving %s request %s...", method, parts) |
- n := h.endpoints[method] |
- if n != nil { |
- n = n.lookup(parts...) |
- } |
- if n == nil { |
- return nil, endpoints.NotFoundError |
- } |
- ep := n.ep |
- call.ep = ep |
- |
- // Build our query parameters by reverse-traversing the tree towards its root. |
- // |
- // The endpoints node path will have the same size as parts, since it matched. |
- for len(parts) > 0 { |
- if n.paramName != "" { |
- param := parts[len(parts)-1] |
- v, err := convertQueryParam(n.paramName, param, ep.method) |
- if err != nil { |
- h.logger.Errorf("Could not convert path param %q (%q): %v", n.paramName, param, err) |
- return nil, endpoints.BadRequestError |
- } |
- call.addParam(n.paramName, v) |
- } |
- |
- parts = parts[:len(parts)-1] |
- n = n.parent |
- } |
- |
- return &call, nil |
-} |
- |
-func convertQueryParam(k, v string, m *endpoints.APIMethod) (interface{}, error) { |
- // Find the parameter for "k". |
- p := m.Request.Params[k] |
- if p == nil { |
- return v, nil |
- } |
- |
- switch p.Type { |
- case "int64", "uint64": |
- // Assume all int64 are "string"-quoted integers. If there are quotes, strip |
- // them. Return the result as a string. |
- if len(v) >= 2 && strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`) { |
- v = v[1 : len(v)-1] |
- } |
- if _, err := strconv.ParseInt(v, 10, 64); err != nil { |
- return nil, err |
- } |
- return v, nil |
- case "int32", "uint32": |
- return strconv.ParseInt(v, 10, 32) |
- case "float", "double": |
- return strconv.ParseFloat(v, 64) |
- case "boolean": |
- switch v { |
- case "", "false": |
- return false, nil |
- default: |
- return true, nil |
- } |
- case "string", "bytes": |
- return v, nil |
- |
- default: |
- return nil, fmt.Errorf("unknown type %q", p.Type) |
- } |
-} |