├── .gitignore ├── .travis.yml ├── endpoints ├── urlfetch.go ├── errors_test.go ├── backend_test.go ├── utils_test.go ├── auth_prod.go ├── auth_dev.go ├── backend.go ├── auth_dev_test.go ├── errors.go ├── server.go ├── jwt_test.go ├── service.go ├── doc.go ├── auth_test.go ├── server_test.go ├── auth.go ├── apiconfig_test.go └── apiconfig.go ├── samples └── helloworld │ ├── app.yaml │ ├── static │ ├── index.html │ └── greetings.js │ └── helloworld.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | install: 3 | - curl -sSo gae_sdk.zip https://storage.googleapis.com/appengine-sdks/featured/go_appengine_sdk_linux_amd64-1.9.24.zip 4 | - unzip -q gae_sdk.zip 5 | - ./go_appengine/goapp get ./endpoints 6 | script: 7 | - ./go_appengine/goapp test -v ./endpoints 8 | -------------------------------------------------------------------------------- /endpoints/urlfetch.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "net/http" 5 | 6 | "golang.org/x/net/context" 7 | "google.golang.org/appengine/urlfetch" 8 | ) 9 | 10 | // httpTransportFactory creates a new HTTP transport suitable for App Engine. 11 | // This is made a variable on purpose, to be stubbed during testing. 12 | var httpTransportFactory = func(c context.Context) http.RoundTripper { 13 | return &urlfetch.Transport{Context: c} 14 | } 15 | 16 | // newHTTPClient returns a new HTTP client using httpTransportFactory 17 | func newHTTPClient(c context.Context) *http.Client { 18 | return &http.Client{Transport: httpTransportFactory(c)} 19 | } 20 | -------------------------------------------------------------------------------- /samples/helloworld/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All rights reserved. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http:#www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to writing, software distributed 8 | # under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | # CONDITIONS OF ANY KIND, either express or implied. 10 | # 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | application: hello-world 15 | version: 1 16 | runtime: go 17 | api_version: go1 18 | 19 | handlers: 20 | - url: /_ah/spi/.* 21 | script: _go_app 22 | secure: always 23 | 24 | - url: / 25 | static_files: static/index.html 26 | upload: static/index.html 27 | secure: always 28 | 29 | - url: / 30 | static_dir: static 31 | secure: always 32 | -------------------------------------------------------------------------------- /endpoints/errors_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestCustomErrorResponse(t *testing.T) { 11 | tts := []*struct { 12 | errMsg string 13 | want *errorResponse 14 | }{ 15 | {"Not Found: test msg", &errorResponse{ 16 | State: "APPLICATION_ERROR", 17 | Name: "Not Found", 18 | Msg: "test msg", 19 | Code: http.StatusNotFound, 20 | }}, 21 | {"Random error", &errorResponse{ 22 | State: "APPLICATION_ERROR", 23 | Name: "Internal Server Error", 24 | Msg: "Random error", 25 | Code: http.StatusBadRequest, 26 | }}, 27 | } 28 | 29 | for i, tt := range tts { 30 | err := errors.New(tt.errMsg) 31 | res := newErrorResponse(err) 32 | if !reflect.DeepEqual(res, tt.want) { 33 | t.Errorf("%d: newErrorResponse(%q) = %#v; want %#v", 34 | i, tt.errMsg, res, tt.want) 35 | } 36 | } 37 | } 38 | 39 | func TestAPIErrorResponse(t *testing.T) { 40 | res := newErrorResponse(BadRequestError) 41 | want := &errorResponse{ 42 | State: "APPLICATION_ERROR", 43 | Name: res.Name, 44 | Msg: res.Msg, 45 | Code: res.Code, 46 | } 47 | if !reflect.DeepEqual(res, want) { 48 | t.Errorf("newErrorResponse(%#v) = %#v; want %#v", 49 | BadRequestError, res, want) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /samples/helloworld/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | Greetings 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |

Gretings

29 | 30 |

You can explore the endpoints API on the 31 | API explorer 32 |

33 | 34 | 39 | 40 |
41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /endpoints/backend_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "testing" 9 | 10 | "appengine/aetest" 11 | ) 12 | 13 | func TestInvalidAppRevision(t *testing.T) { 14 | inst, err := aetest.NewInstance(nil) 15 | if err != nil { 16 | t.Fatalf("Failed to create instance: %v", err) 17 | } 18 | defer inst.Close() 19 | 20 | backend := &BackendService{} 21 | r := newBackendHTTPRequest(inst, "GetApiConfigs", nil) 22 | req := &GetAPIConfigsRequest{AppRevision: "invalid"} 23 | 24 | if err := backend.GetApiConfigs(r, req, nil); err == nil { 25 | t.Errorf("GetApiConfigs(%#v) = nil; want error", req) 26 | } 27 | } 28 | 29 | func TestEmptyAPIConfigsList(t *testing.T) { 30 | inst, err := aetest.NewInstance(nil) 31 | if err != nil { 32 | t.Fatalf("failed to create instance: %v", err) 33 | } 34 | defer inst.Close() 35 | 36 | server := &Server{root: "/_ah/spi", services: new(serviceMap)} 37 | backend := newBackendService(server) 38 | r := newBackendHTTPRequest(inst, "GetApiConfigs", nil) 39 | req := &GetAPIConfigsRequest{} 40 | resp := &APIConfigsList{} 41 | 42 | if err := backend.GetApiConfigs(r, req, resp); err != nil { 43 | t.Errorf("GetApiConfigs() = %v", err) 44 | } 45 | if resp.Items == nil { 46 | t.Errorf("resp.Items = nil; want initialized") 47 | } 48 | if l := len(resp.Items); l != 0 { 49 | t.Errorf("len(resp.Item) = %d (%+v); want 0", l, resp.Items) 50 | } 51 | } 52 | 53 | func newBackendHTTPRequest(inst aetest.Instance, method string, body []byte) *http.Request { 54 | if body == nil { 55 | body = []byte{} 56 | } 57 | buf := bytes.NewBuffer(body) 58 | url := fmt.Sprintf("/_ah/spi/BackendService.%s", method) 59 | req, err := inst.NewRequest("POST", url, buf) 60 | if err != nil { 61 | log.Fatalf("failed to create req: %v", err) 62 | } 63 | return req 64 | } 65 | -------------------------------------------------------------------------------- /samples/helloworld/static/greetings.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | function GreetingsCtrl($scope, $http) { 16 | $scope.greetings = []; 17 | $scope.running = true; 18 | var api; 19 | 20 | loadAPI(function() { 21 | api = gapi.client.greetings; 22 | // load the messages now and refresh every second after. 23 | $scope.refresh(); 24 | window.setInterval($scope.refresh, 1000); 25 | }); 26 | 27 | $scope.refresh = function() { 28 | api.list({'limit':100}).execute(function(res) { 29 | $scope.running = false; 30 | $scope.greetings = res.result.items || []; 31 | $scope.$apply(); 32 | }); 33 | }; 34 | 35 | // Load the greetings API 36 | $scope.add = function() { 37 | $scope.running = true; 38 | api.add($scope.msg).execute(function(res) { 39 | $scope.greetings.unshift($scope.msg); 40 | $scope.msg = {'author': $scope.msg.author}; 41 | $scope.running = false; 42 | $scope.$apply(); 43 | }); 44 | }; 45 | } 46 | 47 | function loadAPI(then) { 48 | var script = document.createElement('script'); 49 | script.type = 'text/javascript'; 50 | 51 | window.onAPILoaded = function() { 52 | var rootpath = "//" + window.location.host + "/_ah/api"; 53 | gapi.client.load('greetings', 'v1', then, rootpath); 54 | window.onAPILoaded = undefined; 55 | } 56 | script.src = 'https://apis.google.com/js/client.js?onload=onAPILoaded'; 57 | 58 | var head = document.getElementsByTagName('head')[0]; 59 | head.appendChild(script); 60 | } 61 | -------------------------------------------------------------------------------- /endpoints/utils_test.go: -------------------------------------------------------------------------------- 1 | // Testing utils 2 | 3 | package endpoints 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "net/http" 9 | "reflect" 10 | "testing" 11 | 12 | "appengine/aetest" 13 | ) 14 | 15 | // verifyPairs loops over ab slice and calls reflect.DeepEqual() on each pair. 16 | // Expects even number of ab args. 17 | // When the order matters, "a" (first element of ab pair) is normally "actual" 18 | // and "b" (second element) is expected. 19 | func verifyPairs(t *testing.T, ab ...interface{}) { 20 | lenAb := len(ab) 21 | if lenAb%2 != 0 { 22 | t.Fatalf("verifyPairs: odd number of ab args (%d)", lenAb) 23 | return 24 | } 25 | for i := 0; i < lenAb; i += 2 { 26 | if !reflect.DeepEqual(ab[i], ab[i+1]) { 27 | t.Errorf("verifyPairs(%d): ab[%d] != ab[%d] (%#v != %#v)", 28 | i/2, i, i+1, ab[i], ab[i+1]) 29 | } 30 | } 31 | } 32 | 33 | // newTestRequest creates a new request using aetest package. 34 | // last return value is a closer function. 35 | func newTestRequest(t *testing.T, method, path string, body io.Reader) ( 36 | *http.Request, 37 | aetest.Instance, 38 | func(), 39 | ) { 40 | inst, err := aetest.NewInstance(nil) 41 | if err != nil { 42 | t.Fatalf("Failed to create instance: %v", err) 43 | } 44 | 45 | req, err := inst.NewRequest(method, path, body) 46 | if err != nil { 47 | t.Fatalf("Failed to create req: %v", err) 48 | } 49 | 50 | return req, inst, func() { inst.Close() } 51 | } 52 | 53 | func newTestRoundTripper(resp ...*http.Response) *TestRoundTripper { 54 | rt := &TestRoundTripper{} 55 | rt.Add(resp...) 56 | return rt 57 | } 58 | 59 | type TestRoundTripper struct { 60 | reqs []*http.Request 61 | responses []*http.Response 62 | next int 63 | } 64 | 65 | func (rt *TestRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 66 | rt.reqs = append(rt.reqs, r) 67 | if rt.next+1 > len(rt.responses) { 68 | return nil, errors.New("TestRoundTripper ran out of responses") 69 | } 70 | resp := rt.responses[rt.next] 71 | rt.next++ 72 | return resp, nil 73 | } 74 | 75 | // Add appends another response(s) 76 | func (rt *TestRoundTripper) Add(resp ...*http.Response) { 77 | rt.responses = append(rt.responses, resp...) 78 | } 79 | 80 | // Count returns the number of responses which have been served so far. 81 | func (rt *TestRoundTripper) Count() int { 82 | return rt.next 83 | } 84 | 85 | func (rt *TestRoundTripper) Requests() []*http.Request { 86 | return rt.reqs 87 | } 88 | 89 | func (rt *TestRoundTripper) Responses() []*http.Response { 90 | return rt.responses 91 | } 92 | -------------------------------------------------------------------------------- /endpoints/auth_prod.go: -------------------------------------------------------------------------------- 1 | // Default implementation of Authenticator interface. 2 | // You can swap this with a stub implementation in tests like so: 3 | // 4 | // func stubAuthenticatorFactory() endpoints.Authenticator { 5 | // // Create a stub which implements (or probably fakes) 6 | // // endpoints.Authenticator 7 | // } 8 | // 9 | // func TestSomething(t *testing.T) { 10 | // origFactory = endpoints.AuthenticatorFactory 11 | // endpoints.AuthenticatorFactory = stubAuthenticatorFactory 12 | // defer func() { 13 | // endpoints.AuthenticatorFactory = origFactory 14 | // } 15 | // // Do some testing here. 16 | // // Any call in the code that (indirectly) does 17 | // // "endpoints.NewContext(r)" will actually invoke 18 | // // stubAuthenticatorFactory() now. 19 | // } 20 | 21 | package endpoints 22 | 23 | import ( 24 | "sync" 25 | 26 | "golang.org/x/net/context" 27 | "google.golang.org/appengine/user" 28 | ) 29 | 30 | type cachingAuthenticator struct { 31 | // map keys are scopes 32 | oauthResponseCache map[string]*user.User 33 | // mutex for oauthResponseCache 34 | sync.Mutex 35 | } 36 | 37 | // populateOAuthResponse updates (overwrites) OAuth user data associated 38 | // with this request and the given scope. It should only be called 39 | // while the mutex is held. 40 | func (ca *cachingAuthenticator) populateOAuthResponse(c context.Context, scope string) error { 41 | // Only one scope should be cached at once, so we just destroy the cache 42 | ca.oauthResponseCache = map[string]*user.User{} 43 | 44 | u, err := user.CurrentOAuth(c, scope) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | ca.oauthResponseCache[scope] = u 50 | return nil 51 | } 52 | 53 | func (ca *cachingAuthenticator) oauthResponse(c context.Context, scope string) (*user.User, error) { 54 | ca.Lock() 55 | defer ca.Unlock() 56 | 57 | res, ok := ca.oauthResponseCache[scope] 58 | if !ok { 59 | if err := ca.populateOAuthResponse(c, scope); err != nil { 60 | return nil, err 61 | } 62 | res = ca.oauthResponseCache[scope] 63 | } 64 | return res, nil 65 | } 66 | 67 | // CurrentOAuthClientID returns a clientID associated with the scope. 68 | func (ca *cachingAuthenticator) CurrentOAuthClientID(c context.Context, scope string) (string, error) { 69 | u, err := ca.oauthResponse(c, scope) 70 | if err != nil { 71 | return "", err 72 | } 73 | return u.ClientID, nil 74 | } 75 | 76 | // CurrentOAuthUser returns a user of this request for the given scope. 77 | // It caches OAuth info at the first call for future invocations. 78 | // 79 | // Returns an error if data for this scope is not available. 80 | func (ca *cachingAuthenticator) CurrentOAuthUser(c context.Context, scope string) (*user.User, error) { 81 | u, err := ca.oauthResponse(c, scope) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return u, nil 86 | } 87 | 88 | // Default implentation of endpoints.AuthenticatorFactory. 89 | func cachingAuthenticatorFactory() Authenticator { 90 | // TODO(dhermes): Check whether the prod behaviour is identical to dev. 91 | // On dev appengine.NewContext() panics on error so, if it is identical 92 | // then there's nothing else to do here. 93 | // (was: Fail if ctx is nil.) 94 | return &cachingAuthenticator{ 95 | oauthResponseCache: make(map[string]*user.User), 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /samples/helloworld/helloworld.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package helloworld 16 | 17 | import ( 18 | "log" 19 | "time" 20 | 21 | "github.com/GoogleCloudPlatform/go-endpoints/endpoints" 22 | "golang.org/x/net/context" 23 | "google.golang.org/appengine/datastore" 24 | ) 25 | 26 | // Greeting is a datastore entity that represents a single greeting. 27 | // It also serves as (a part of) a response of GreetingService. 28 | type Greeting struct { 29 | Key *datastore.Key `json:"id" datastore:"-"` 30 | Author string `json:"author"` 31 | Content string `json:"content" datastore:",noindex"` 32 | Date time.Time `json:"date"` 33 | } 34 | 35 | // GretingAddReq is the request type for GreetingService.Add. 36 | type GreetingAddReq struct { 37 | Author string `json:"author"` 38 | Content string `json:"content" endpoints:"req"` 39 | } 40 | 41 | // GreetingListReq is the request type for GreetingService.List. 42 | type GreetingsListReq struct { 43 | Limit int `json:"limit" endpoints:"d=10"` 44 | } 45 | 46 | // GreetingsList is the response type for GreetingService.List. 47 | type GreetingsList struct { 48 | Items []*Greeting `json:"items"` 49 | } 50 | 51 | // GreetingService offers operations to add and list greetings. 52 | type GreetingService struct{} 53 | 54 | // List responds with a list of all greetings ordered by Date field. 55 | // Most recent greets come first. 56 | func (gs *GreetingService) List(c context.Context, r *GreetingsListReq) (*GreetingsList, error) { 57 | q := datastore.NewQuery("Greeting").Order("-Date").Limit(r.Limit) 58 | greets := make([]*Greeting, 0, r.Limit) 59 | keys, err := q.GetAll(c, &greets) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | for i, k := range keys { 65 | greets[i].Key = k 66 | } 67 | return &GreetingsList{greets}, nil 68 | } 69 | 70 | // Add adds a greeting. 71 | func (gs *GreetingService) Add(c context.Context, r *GreetingAddReq) error { 72 | k := datastore.NewIncompleteKey(c, "Greeting", nil) 73 | g := &Greeting{ 74 | Author: r.Author, 75 | Content: r.Content, 76 | Date: time.Now(), 77 | } 78 | _, err := datastore.Put(c, k, g) 79 | return err 80 | } 81 | 82 | func init() { 83 | api, err := endpoints.RegisterService(&GreetingService{}, 84 | "greetings", "v1", "Greetings API", true) 85 | if err != nil { 86 | log.Fatalf("Register service: %v", err) 87 | } 88 | 89 | list := api.MethodByName("List").Info() 90 | list.HTTPMethod = "GET" 91 | list.Path = "greetings" 92 | list.Name = "list" 93 | list.Desc = "List most recent greetings." 94 | 95 | add := api.MethodByName("Add").Info() 96 | add.HTTPMethod = "PUT" 97 | add.Path = "greetings" 98 | add.Name = "add" 99 | add.Desc = "Add a greeting." 100 | 101 | endpoints.HandleHTTP() 102 | } 103 | -------------------------------------------------------------------------------- /endpoints/auth_dev.go: -------------------------------------------------------------------------------- 1 | // This implementation of Authenticator uses tokeninfo API to validate 2 | // bearer token. 3 | // 4 | // It is intended to be used only on dev server. 5 | 6 | package endpoints 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "net/http" 13 | "strings" 14 | 15 | "golang.org/x/net/context" 16 | "google.golang.org/appengine/log" 17 | "google.golang.org/appengine/user" 18 | ) 19 | 20 | const tokeninfoEndpointURL = "https://www.googleapis.com/oauth2/v2/tokeninfo" 21 | 22 | type tokeninfo struct { 23 | IssuedTo string `json:"issued_to"` 24 | Audience string `json:"audience"` 25 | UserID string `json:"user_id"` 26 | Scope string `json:"scope"` 27 | ExpiresIn int `json:"expires_in"` 28 | Email string `json:"email"` 29 | VerifiedEmail bool `json:"verified_email"` 30 | AccessType string `json:"access_type"` 31 | // ErrorDescription is populated when an error occurs. Usually, the response 32 | // either contains only ErrorDescription or the fields above 33 | ErrorDescription string `json:"error_description"` 34 | } 35 | 36 | // fetchTokeninfo retrieves token info from tokeninfoEndpointURL (tokeninfo API) 37 | func fetchTokeninfo(c context.Context, token string) (*tokeninfo, error) { 38 | url := tokeninfoEndpointURL + "?access_token=" + token 39 | log.Debugf(c, "Fetching token info from %q", url) 40 | resp, err := newHTTPClient(c).Get(url) 41 | if err != nil { 42 | return nil, err 43 | } 44 | defer resp.Body.Close() 45 | log.Debugf(c, "Tokeninfo replied with %s", resp.Status) 46 | 47 | ti := &tokeninfo{} 48 | if err = json.NewDecoder(resp.Body).Decode(ti); err != nil { 49 | return nil, err 50 | } 51 | if resp.StatusCode != http.StatusOK { 52 | errMsg := fmt.Sprintf("Error fetching tokeninfo (status %d)", resp.StatusCode) 53 | if ti.ErrorDescription != "" { 54 | errMsg += ": " + ti.ErrorDescription 55 | } 56 | return nil, errors.New(errMsg) 57 | } 58 | 59 | switch { 60 | case ti.ExpiresIn <= 0: 61 | return nil, errors.New("Token is expired") 62 | case !ti.VerifiedEmail: 63 | return nil, fmt.Errorf("Unverified email %q", ti.Email) 64 | case ti.Email == "": 65 | return nil, fmt.Errorf("Invalid email address") 66 | } 67 | 68 | return ti, err 69 | } 70 | 71 | // scopedTokeninfo validates fetched token by matching tokeninfo.Scope 72 | // with scope arg. 73 | func scopedTokeninfo(c context.Context, scope string) (*tokeninfo, error) { 74 | token := parseToken(HTTPRequest(c)) 75 | if token == "" { 76 | return nil, errors.New("No token found") 77 | } 78 | ti, err := fetchTokeninfo(c, token) 79 | if err != nil { 80 | return nil, err 81 | } 82 | for _, s := range strings.Split(ti.Scope, " ") { 83 | if s == scope { 84 | return ti, nil 85 | } 86 | } 87 | return nil, fmt.Errorf("No scope matches: expected one of %q, got %q", 88 | ti.Scope, scope) 89 | } 90 | 91 | // tokeninfoAuthenticator is an Authenticator that uses tokeninfo API 92 | // to validate bearer token. 93 | type tokeninfoAuthenticator struct{} 94 | 95 | // CurrentOAuthClientID returns a clientID associated with the scope. 96 | func (tokeninfoAuthenticator) CurrentOAuthClientID(c context.Context, scope string) (string, error) { 97 | ti, err := scopedTokeninfo(c, scope) 98 | if err != nil { 99 | return "", err 100 | } 101 | return ti.IssuedTo, nil 102 | } 103 | 104 | // CurrentOAuthUser returns a user associated with the request in context. 105 | func (tokeninfoAuthenticator) CurrentOAuthUser(c context.Context, scope string) (*user.User, error) { 106 | ti, err := scopedTokeninfo(c, scope) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return &user.User{ 111 | ID: ti.UserID, 112 | Email: ti.Email, 113 | ClientID: ti.IssuedTo, 114 | }, nil 115 | } 116 | 117 | // tokeninfoAuthenticatorFactory creates a new tokeninfoAuthenticator from r. 118 | // To be used as auth.go/AuthenticatorFactory. 119 | func tokeninfoAuthenticatorFactory() Authenticator { 120 | return tokeninfoAuthenticator{} 121 | } 122 | -------------------------------------------------------------------------------- /endpoints/backend.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "golang.org/x/net/context" 11 | "google.golang.org/appengine" 12 | "google.golang.org/appengine/log" 13 | ) 14 | 15 | // Levels that can be specified for a LogMessage. 16 | type logLevel string 17 | 18 | const ( 19 | levelDebug logLevel = "debug" 20 | levelInfo logLevel = "info" 21 | levelWarning logLevel = "warning" 22 | levelError logLevel = "error" 23 | levelCritical logLevel = "critical" 24 | ) 25 | 26 | // GetAPIConfigsRequest is the request scheme for fetching API configs. 27 | type GetAPIConfigsRequest struct { 28 | AppRevision string `json:"appRevision"` 29 | } 30 | 31 | // APIConfigsList is the response scheme for BackendService.getApiConfigs method. 32 | type APIConfigsList struct { 33 | Items []string `json:"items"` 34 | } 35 | 36 | // LogMessagesRequest is the request body for log messages sent by Swarm FE. 37 | type LogMessagesRequest struct { 38 | Messages []*LogMessage `json:"messages"` 39 | } 40 | 41 | // LogMessage is a single log message within a LogMessagesRequest. 42 | type LogMessage struct { 43 | Level logLevel `json:"level"` 44 | Message string `json:"message" endpoints:"required"` 45 | } 46 | 47 | // BackendService is an API config enumeration service used by Google API Server. 48 | // 49 | // This is a simple API providing a list of APIs served by this App Engine 50 | // instance. It is called by the Google API Server during app deployment 51 | // to get an updated interface for each of the supported APIs. 52 | type BackendService struct { 53 | server *Server // of which server 54 | } 55 | 56 | // GetApiConfigs creates APIDescriptor for every registered RPCService and 57 | // responds with a config suitable for generating Discovery doc. 58 | // 59 | // Responds with a list of active APIs and their configuration files. 60 | func (s *BackendService) GetApiConfigs( 61 | r *http.Request, req *GetAPIConfigsRequest, resp *APIConfigsList) error { 62 | c := appengine.NewContext(r) 63 | if req.AppRevision != "" { 64 | revision := strings.Split(appengine.VersionID(c), ".")[1] 65 | if req.AppRevision != revision { 66 | err := fmt.Errorf( 67 | "API backend app revision %s not the same as expected %s", 68 | revision, req.AppRevision) 69 | log.Errorf(c, "%s", err) 70 | return err 71 | } 72 | } 73 | 74 | resp.Items = make([]string, 0) 75 | for _, service := range s.server.services.services { 76 | if service.internal { 77 | continue 78 | } 79 | d := &APIDescriptor{} 80 | if err := service.APIDescriptor(d, r.Host); err != nil { 81 | log.Errorf(c, "%s", err) 82 | return err 83 | } 84 | bytes, err := json.Marshal(d) 85 | if err != nil { 86 | log.Errorf(c, "%s", err) 87 | return err 88 | } 89 | resp.Items = append(resp.Items, string(bytes)) 90 | } 91 | return nil 92 | } 93 | 94 | // LogMessages writes a log message from the Swarm FE to the log. 95 | func (s *BackendService) LogMessages( 96 | r *http.Request, req *LogMessagesRequest, _ *VoidMessage) error { 97 | 98 | c := appengine.NewContext(r) 99 | for _, msg := range req.Messages { 100 | writeLogMessage(c, msg.Level, msg.Message) 101 | } 102 | return nil 103 | } 104 | 105 | // GetFirstConfig is a test method and will be removed sooner or later. 106 | func (s *BackendService) GetFirstConfig( 107 | r *http.Request, _ *VoidMessage, resp *APIDescriptor) error { 108 | 109 | for _, service := range s.server.services.services { 110 | if !service.internal { 111 | return service.APIDescriptor(resp, r.Host) 112 | } 113 | } 114 | return errors.New("Not Found: No public API found") 115 | } 116 | 117 | func writeLogMessage(c context.Context, level logLevel, msg string) { 118 | const fmt = "%s" 119 | switch level { 120 | case levelDebug: 121 | log.Debugf(c, fmt, msg) 122 | case levelWarning: 123 | log.Warningf(c, fmt, msg) 124 | case levelError: 125 | log.Errorf(c, fmt, msg) 126 | case levelCritical: 127 | log.Criticalf(c, fmt, msg) 128 | default: 129 | log.Infof(c, fmt, msg) 130 | } 131 | } 132 | 133 | func newBackendService(server *Server) *BackendService { 134 | return &BackendService{server: server} 135 | } 136 | -------------------------------------------------------------------------------- /endpoints/auth_dev_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "golang.org/x/net/context" 11 | 12 | "appengine/aetest" 13 | ) 14 | 15 | const ( 16 | tokeinfoUserID = "12345" 17 | tokeninfoEmail = "dude@gmail.com" 18 | ) 19 | 20 | var ( 21 | tokeninfoValid = `{ 22 | "issued_to": "my-client-id", 23 | "audience": "my-client-id", 24 | "user_id": "` + tokeinfoUserID + `", 25 | "scope": "scope.one scope.two", 26 | "expires_in": 3600, 27 | "email": "` + tokeninfoEmail + `", 28 | "verified_email": true, 29 | "access_type": "online" 30 | }` 31 | tokeninfoUnverified = `{ 32 | "expires_in": 3600, 33 | "verified_email": false, 34 | "email": "user@example.org" 35 | }` 36 | // is this even possible for email to be "" and verified == true? 37 | tokeninfoInvalidEmail = `{ 38 | "expires_in": 3600, 39 | "verified_email": true, 40 | "email": "" 41 | }` 42 | tokeninfoError = `{ 43 | "error_description": "Invalid value" 44 | }` 45 | ) 46 | 47 | func TestTokeninfoContextCurrentOAuthClientID(t *testing.T) { 48 | rt := newTestRoundTripper() 49 | origTransport := httpTransportFactory 50 | defer func() { httpTransportFactory = origTransport }() 51 | httpTransportFactory = func(c context.Context) http.RoundTripper { 52 | return rt 53 | } 54 | 55 | inst, err := aetest.NewInstance(nil) 56 | if err != nil { 57 | t.Fatalf("failed to create instance: %v", err) 58 | } 59 | defer inst.Close() 60 | 61 | tts := []*struct { 62 | token, scope, clientID string 63 | httpStatus int 64 | content string 65 | }{ 66 | // token, scope, clientID, httpStatus, content 67 | {"some_token0", "scope.one", "my-client-id", 200, tokeninfoValid}, 68 | {"some_token1", "scope.two", "my-client-id", 200, tokeninfoValid}, 69 | {"some_token2", "scope.one", "", 200, tokeninfoUnverified}, 70 | {"some_token3", "scope.one", "", 200, tokeninfoInvalidEmail}, 71 | {"some_token4", "scope.one", "", 401, tokeninfoError}, 72 | {"some_token5", "invalid.scope", "", 200, tokeninfoValid}, 73 | {"some_token6", "scope.one", "", 400, "{}"}, 74 | {"some_token7", "scope.one", "", 200, ""}, 75 | {"", "scope.one", "", 200, tokeninfoValid}, 76 | {"some_token9", "scope.one", "", -1, ""}, 77 | } 78 | 79 | for i, tt := range tts { 80 | r, err := inst.NewRequest("GET", "/", nil) 81 | if err != nil { 82 | t.Fatalf("Error creating a req: %v", err) 83 | } 84 | r.Header.Set("authorization", "bearer "+tt.token) 85 | if tt.token != "" && tt.httpStatus > 0 { 86 | rt.Add(&http.Response{ 87 | Status: fmt.Sprintf("%d", tt.httpStatus), 88 | StatusCode: tt.httpStatus, 89 | Body: ioutil.NopCloser(strings.NewReader(tt.content)), 90 | }) 91 | } 92 | c := newContext(r, tokeninfoAuthenticatorFactory) 93 | auth := authenticator(c) 94 | if auth == nil { 95 | t.Errorf("%d: context authenticator missing", i) 96 | continue 97 | } 98 | id, err := auth.CurrentOAuthClientID(c, tt.scope) 99 | switch { 100 | case err != nil && tt.clientID != "": 101 | t.Errorf("%d: CurrentOAuthClientID(%v) = %v; want %q", 102 | i, tt.scope, err, tt.clientID) 103 | case err == nil && tt.clientID == "": 104 | t.Errorf("%d: CurrentOAuthClientID(%v) = %v; want error", 105 | i, tt.scope, id) 106 | case err == nil && id != tt.clientID: 107 | t.Errorf("%d: CurrentOAuthClientID(%v) = %v; want %q", 108 | i, tt.scope, id, tt.clientID) 109 | } 110 | } 111 | } 112 | 113 | func TestTokeninfoCurrentOAuthUser(t *testing.T) { 114 | origTransport := httpTransportFactory 115 | defer func() { 116 | httpTransportFactory = origTransport 117 | }() 118 | httpTransportFactory = func(c context.Context) http.RoundTripper { 119 | return newTestRoundTripper(&http.Response{ 120 | Status: "200 OK", 121 | StatusCode: 200, 122 | Body: ioutil.NopCloser(strings.NewReader(tokeninfoValid)), 123 | }) 124 | } 125 | 126 | r, _, closer := newTestRequest(t, "GET", "/", nil) 127 | defer closer() 128 | r.Header.Set("authorization", "bearer some_token") 129 | 130 | const scope = "scope.one" 131 | c := newContext(r, tokeninfoAuthenticatorFactory) 132 | auth := authenticator(c) 133 | if auth == nil { 134 | t.Fatal("context authenticator missing") 135 | } 136 | user, err := auth.CurrentOAuthUser(c, scope) 137 | 138 | if err != nil { 139 | t.Fatalf("CurrentOAuthUser(%q) = %v", scope, err) 140 | } 141 | if user.Email != tokeninfoEmail { 142 | t.Errorf("CurrentOAuthUser(%q) = %#v; want email = %q", scope, user, tokeninfoEmail) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /endpoints/errors.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // Pre-defined API errors. 12 | // Use NewAPIError() method to create your own. 13 | 14 | // InternalServerError is default error with http.StatusInternalServerError (500) 15 | InternalServerError = NewInternalServerError("") 16 | // BadRequestError is default error with http.StatusBadRequest (400) 17 | BadRequestError = NewBadRequestError("") 18 | // UnauthorizedError is default error with http.StatusUnauthorized (401) 19 | UnauthorizedError = NewUnauthorizedError("") 20 | // ForbiddenError is default error with http.StatusForbidden (403) 21 | ForbiddenError = NewForbiddenError("") 22 | // NotFoundError is default error with http.StatusNotFound (404) 23 | NotFoundError = NewNotFoundError("") 24 | // ConflictError is default error with http.StatusConflict (409) 25 | ConflictError = NewConflictError("") 26 | 27 | // knownErrors is a list of all known errors. 28 | knownErrors = [...]int{ 29 | http.StatusInternalServerError, 30 | http.StatusBadRequest, 31 | http.StatusUnauthorized, 32 | http.StatusForbidden, 33 | http.StatusNotFound, 34 | http.StatusConflict, 35 | } 36 | ) 37 | 38 | // APIError is a user custom API's error 39 | type APIError struct { 40 | Name string 41 | Msg string 42 | Code int 43 | } 44 | 45 | // APIError is an error 46 | func (a *APIError) Error() string { 47 | return a.Msg 48 | } 49 | 50 | // NewAPIError Create a new APIError for custom error 51 | func NewAPIError(name string, msg string, code int) error { 52 | return &APIError{name, msg, code} 53 | } 54 | 55 | // errorf creates a new APIError given its status code, a format string and its arguments. 56 | func errorf(code int, format string, args ...interface{}) error { 57 | return &APIError{http.StatusText(code), fmt.Sprintf(format, args...), code} 58 | } 59 | 60 | // NewInternalServerError creates a new APIError with Internal Server Error status (500) 61 | func NewInternalServerError(format string, args ...interface{}) error { 62 | return errorf(http.StatusInternalServerError, format, args...) 63 | } 64 | 65 | // NewBadRequestError creates a new APIError with Bad Request status (400) 66 | func NewBadRequestError(format string, args ...interface{}) error { 67 | return errorf(http.StatusBadRequest, format, args...) 68 | } 69 | 70 | // NewUnauthorizedError creates a new APIError with Unauthorized status (401) 71 | func NewUnauthorizedError(format string, args ...interface{}) error { 72 | return errorf(http.StatusUnauthorized, format, args...) 73 | } 74 | 75 | // NewNotFoundError creates a new APIError with Not Found status (404) 76 | func NewNotFoundError(format string, args ...interface{}) error { 77 | return errorf(http.StatusNotFound, format, args...) 78 | } 79 | 80 | // NewForbiddenError creates a new APIError with Forbidden status (403) 81 | func NewForbiddenError(format string, args ...interface{}) error { 82 | return errorf(http.StatusForbidden, format, args...) 83 | } 84 | 85 | // NewConflictError creates a new APIError with Conflict status (409) 86 | func NewConflictError(format string, args ...interface{}) error { 87 | return errorf(http.StatusConflict, format, args...) 88 | } 89 | 90 | // errorResponse is SPI-compatible error response 91 | type errorResponse struct { 92 | // Currently always "APPLICATION_ERROR" 93 | State string `json:"state"` 94 | Name string `json:"error_name"` 95 | Msg string `json:"error_message,omitempty"` 96 | Code int `json:"-"` 97 | } 98 | 99 | // Creates and initializes a new errorResponse. 100 | // If msg contains any of knownErrors then errorResponse.Name will be set 101 | // to that name and the rest of the msg becomes errorResponse.Msg. 102 | // Otherwise, a default error name is used and msg argument 103 | // is errorResponse.Msg. 104 | func newErrorResponse(err error) *errorResponse { 105 | if e, ok := err.(*APIError); ok { 106 | return &errorResponse{"APPLICATION_ERROR", e.Name, e.Msg, e.Code} 107 | } 108 | msg := err.Error() 109 | for _, code := range knownErrors { 110 | if name := http.StatusText(code); strings.HasPrefix(msg, name) { 111 | return &errorResponse{"APPLICATION_ERROR", name, strings.Trim(msg[len(name):], " :"), code} 112 | } 113 | } 114 | //for compatibility, Before behavior, always return 400 HTTP Status Code. 115 | // TODO(alex): where is 400 coming from? 116 | return &errorResponse{"APPLICATION_ERROR", http.StatusText(http.StatusInternalServerError), msg, http.StatusBadRequest} 117 | } 118 | 119 | // writeError writes SPI-compatible error response. 120 | func writeError(w http.ResponseWriter, err error) { 121 | errResp := newErrorResponse(err) 122 | w.WriteHeader(errResp.Code) 123 | json.NewEncoder(w).Encode(errResp) 124 | } 125 | -------------------------------------------------------------------------------- /endpoints/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Copyright 2012 The Gorilla Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package endpoints 7 | 8 | import ( 9 | "bytes" 10 | "encoding/json" 11 | "fmt" 12 | "net/http" 13 | "reflect" 14 | "strings" 15 | 16 | "golang.org/x/net/context" 17 | // Mainly for debug logging 18 | "io/ioutil" 19 | 20 | "google.golang.org/appengine/log" 21 | ) 22 | 23 | // Server serves registered RPC services using registered codecs. 24 | type Server struct { 25 | root string 26 | services *serviceMap 27 | 28 | // ContextDecorator will be called as the last step of the creation of a new context. 29 | // If nil the context will not be decorated. 30 | ContextDecorator func(context.Context) (context.Context, error) 31 | } 32 | 33 | // NewServer returns a new RPC server. 34 | func NewServer(root string) *Server { 35 | if root == "" { 36 | root = "/_ah/spi/" 37 | } else if root[len(root)-1] != '/' { 38 | root += "/" 39 | } 40 | 41 | server := &Server{root: root, services: new(serviceMap)} 42 | backend := newBackendService(server) 43 | server.services.register(backend, "BackendService", "", "", true, true) 44 | return server 45 | } 46 | 47 | // RegisterService adds a new service to the server. 48 | // 49 | // The name parameter is optional: if empty it will be inferred from 50 | // the receiver type name. 51 | // 52 | // Methods from the receiver will be extracted if these rules are satisfied: 53 | // 54 | // - The receiver is exported (begins with an upper case letter) or local 55 | // (defined in the package registering the service). 56 | // - The method name is exported. 57 | // - The method has either 2 arguments and 2 return values: 58 | // *http.Request|Context, *arg => *reply, error 59 | // or 3 arguments and 1 return value: 60 | // *http.Request|Context, *arg, *reply => error 61 | // - The first argument is either *http.Request or Context. 62 | // - Second argument (*arg) and *reply are exported or local. 63 | // - First argument, *arg and *reply are all pointers. 64 | // - First (or second, if method has 2 arguments) return value is of type error. 65 | // 66 | // All other methods are ignored. 67 | func (s *Server) RegisterService(srv interface{}, name, ver, desc string, isDefault bool) (*RPCService, error) { 68 | return s.services.register(srv, name, ver, desc, isDefault, false) 69 | } 70 | 71 | // RegisterServiceWithDefaults will register provided service and will try to 72 | // infer Endpoints config params from its method names and types. 73 | // See RegisterService for details. 74 | func (s *Server) RegisterServiceWithDefaults(srv interface{}) (*RPCService, error) { 75 | return s.RegisterService(srv, "", "", "", true) 76 | } 77 | 78 | // Must is a helper that wraps a call to a function returning (*Template, error) and 79 | // panics if the error is non-nil. It is intended for use in variable initializations 80 | // such as: 81 | // var s = endpoints.Must(endpoints.RegisterService(s, "Service", "v1", "some service", true)) 82 | // 83 | func Must(s *RPCService, err error) *RPCService { 84 | if err != nil { 85 | panic(err) 86 | } 87 | return s 88 | } 89 | 90 | // ServiceByName returns a registered service or nil if there's no service 91 | // registered by that name. 92 | func (s *Server) ServiceByName(serviceName string) *RPCService { 93 | return s.services.serviceByName(serviceName) 94 | } 95 | 96 | // HandleHTTP adds Server s to specified http.ServeMux. 97 | // If no mux is provided http.DefaultServeMux will be used. 98 | func (s *Server) HandleHTTP(mux *http.ServeMux) { 99 | if mux == nil { 100 | mux = http.DefaultServeMux 101 | } 102 | mux.Handle(s.root, s) 103 | } 104 | 105 | // ServeHTTP is Server's implementation of http.Handler interface. 106 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 107 | // Always respond with JSON, even when an error occurs. 108 | // Note: API server doesn't expect an encoding in Content-Type header. 109 | w.Header().Set("Content-Type", "application/json") 110 | 111 | c := NewContext(r) 112 | if s.ContextDecorator != nil { 113 | ctx, err := s.ContextDecorator(c) 114 | if err != nil { 115 | writeError(w, err) 116 | return 117 | } 118 | c = ctx 119 | } 120 | 121 | if r.Method != "POST" { 122 | err := fmt.Errorf("rpc: POST method required, got %q", r.Method) 123 | writeError(w, err) 124 | return 125 | } 126 | 127 | // methodName has "ServiceName.MethodName" format. 128 | var methodName string 129 | idx := strings.LastIndex(r.URL.Path, "/") 130 | if idx < 0 { 131 | writeError(w, fmt.Errorf("rpc: no method in path %q", r.URL.Path)) 132 | return 133 | } 134 | methodName = r.URL.Path[idx+1:] 135 | 136 | // Get service method specs 137 | serviceSpec, methodSpec, err := s.services.get(methodName) 138 | if err != nil { 139 | writeError(w, err) 140 | return 141 | } 142 | 143 | // Initialize RPC method request 144 | reqValue := reflect.New(methodSpec.ReqType) 145 | 146 | body, err := ioutil.ReadAll(r.Body) 147 | r.Body.Close() 148 | if err != nil { 149 | writeError(w, err) 150 | return 151 | } 152 | log.Debugf(c, "SPI request body: %s", body) 153 | 154 | // if err := json.NewDecoder(r.Body).Decode(req.Interface()); err != nil { 155 | // writeError(w, fmt.Errorf("Error while decoding JSON: %q", err)) 156 | // return 157 | // } 158 | if err := json.Unmarshal(body, reqValue.Interface()); err != nil { 159 | writeError(w, err) 160 | return 161 | } 162 | 163 | if err := validateRequest(reqValue.Interface()); err != nil { 164 | writeError(w, err) 165 | return 166 | } 167 | 168 | // Restore the body in the original request. 169 | r.Body = ioutil.NopCloser(bytes.NewReader(body)) 170 | 171 | numIn, numOut := methodSpec.method.Type.NumIn(), methodSpec.method.Type.NumOut() 172 | // Construct arguments for the method call 173 | var httpReqOrCtx interface{} = r 174 | if methodSpec.wantsContext { 175 | httpReqOrCtx = c 176 | } 177 | args := []reflect.Value{serviceSpec.rcvr, reflect.ValueOf(httpReqOrCtx)} 178 | if numIn > 2 { 179 | args = append(args, reqValue) 180 | } 181 | 182 | var respValue reflect.Value 183 | if numIn > 3 { 184 | respValue = reflect.New(methodSpec.RespType) 185 | args = append(args, respValue) 186 | } 187 | 188 | // Invoke the service method 189 | var errValue reflect.Value 190 | res := methodSpec.method.Func.Call(args) 191 | if numOut == 2 { 192 | respValue = res[0] 193 | errValue = res[1] 194 | } else { 195 | errValue = res[0] 196 | } 197 | 198 | // Check if method returned an error 199 | if err := errValue.Interface(); err != nil { 200 | writeError(w, err.(error)) 201 | return 202 | } 203 | 204 | // Encode non-error response 205 | if numIn == 4 || numOut == 2 { 206 | if err := json.NewEncoder(w).Encode(respValue.Interface()); err != nil { 207 | writeError(w, err) 208 | } 209 | } 210 | } 211 | 212 | // DefaultServer is the default RPC server, so you don't have to explicitly 213 | // create one. 214 | var DefaultServer *Server 215 | 216 | // RegisterService registers a service using DefaultServer. 217 | // See Server.RegisterService for details. 218 | func RegisterService(srv interface{}, name, ver, desc string, isDefault bool) ( 219 | *RPCService, error) { 220 | 221 | return DefaultServer.RegisterService(srv, name, ver, desc, isDefault) 222 | } 223 | 224 | // RegisterServiceWithDefaults registers a service using DefaultServer. 225 | // See Server.RegisterServiceWithDefaults for details. 226 | func RegisterServiceWithDefaults(srv interface{}) (*RPCService, error) { 227 | return DefaultServer.RegisterServiceWithDefaults(srv) 228 | } 229 | 230 | // HandleHTTP calls DefaultServer's HandleHTTP method using default serve mux. 231 | func HandleHTTP() { 232 | DefaultServer.HandleHTTP(nil) 233 | } 234 | 235 | // TODO: var DefaultServer = NewServer("") won't work so it's in the init() 236 | // function for now. 237 | func init() { 238 | DefaultServer = NewServer("") 239 | } 240 | -------------------------------------------------------------------------------- /endpoints/jwt_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "golang.org/x/net/context" 10 | "google.golang.org/appengine" 11 | "google.golang.org/appengine/memcache" 12 | ) 13 | 14 | var jwtValidTokenObject = signedJWT{ 15 | Audience: "my-client-id", 16 | ClientID: "hello-android", 17 | Email: "dude@gmail.com", 18 | Expires: 1370352252, 19 | IssuedAt: 1370348652, 20 | Issuer: "accounts.google.com", 21 | } 22 | 23 | // jwtValidTokenTime is a "timestamp" at which jwtValidTokenObject is valid 24 | // (e.g. not expired or something) 25 | var jwtValidTokenTime = time.Date(2013, 6, 4, 13, 24, 15, 0, time.UTC) 26 | 27 | // header: {"alg": "RS256", "typ": "JWT"} 28 | // payload: 29 | // { 30 | // "aud": "my-client-id", 31 | // "azp": "hello-android", 32 | // "email": "dude@gmail.com", 33 | // "exp": 1370352252, 34 | // "iat": 1370348652, 35 | // "iss": "accounts.google.com" 36 | // } 37 | // issued at 2013-06-04 14:24:12 UTC 38 | // expires at 2013-06-04 15:24:12 UTC 39 | const jwtValidTokenString = ("eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9." + 40 | "eyJhdWQiOiAibXktY2xpZW50LWlkIiwgImlzcyI6ICJhY2NvdW50cy5nb29nbGUuY29tIiwg" + 41 | "ImV4cCI6IDEzNzAzNTIyNTIsICJhenAiOiAiaGVsbG8tYW5kcm9pZCIsICJpYXQiOiAxMzcw" + 42 | "MzQ4NjUyLCAiZW1haWwiOiAiZHVkZUBnbWFpbC5jb20ifQ." + 43 | "sv7l0v_u6DmVe7s-hg8Q5LOYXNCdUBR7efnvQ4ns6IfBFZ71yPvWfwOYqZuYGQ0a9V5CfR0r" + 44 | "TfNlXVEpW5NE9rZy8hFiZkHBE30yPDti6PUUtT1bZST1VPFnIvSHobcUj-QPBTRC1Df86Vv0" + 45 | "Jmx4yowL1z3Yhe0Zh1WcvUUPG9sKJt8_-qKAv9QeeCMveBYpRSh6JvoU_qUKxPTjOOLvQiqV" + 46 | "4NiNjJ3sDN0P4BHJc3VcqB-SFd7kMRgQy1Fq-NHKN5-T2x4gxPwUy9GvHOftxY47B1NtJ9Q5" + 47 | "KtSui9uXdyBNJnt0xcIT5CcQYkVLoeCldxpSfwfA2kyfuJKBeiQnSA") 48 | 49 | // same header and payload, only encoded with a random private key 50 | // (generate one with "openssl genrsa 2048 > dummy.key") 51 | const jwtInvalidKeyToken = ("eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9." + 52 | "eyJhdWQiOiAibXktY2xpZW50LWlkIiwgImlzcyI6ICJhY2NvdW50cy5nb29nbGUuY29tIiwg" + 53 | "ImV4cCI6IDEzNzAzNTIyNTIsICJhenAiOiAiaGVsbG8tYW5kcm9pZCIsICJpYXQiOiAxMzcw" + 54 | "MzQ4NjUyLCAiZW1haWwiOiAiZHVkZUBnbWFpbC5jb20ifQ." + 55 | "PatagaopzqOe_LqM4rddJHKaZ-l2bacN3Lsj2t15c_iZRzjgFXlC6CsR64SaHSdC-wxde3wu" + 56 | "OKKRWZPZA2Zr03TRUMB_iLJDs2Gg4dUsEsVrbZkTzrGcmejrHIIA1wP0hIM1COBIo6bYr9Vz" + 57 | "UBDBR4tlq8kRgNdCmHXRrR1u4ITSFin3skRE6xkIFXmswI4lzpfWGD2f4jsnH8HDu5K-9X3Q" + 58 | "OhUgKUL9Hlz5z2PtLMGl0xXzFNSPoWAPPNZkNJlPUfLKL7QnJdWu_ieQ1L0xUWHcGvb76lgD" + 59 | "AbihLmbYrX0DeIMl6a_n4wKLRzoVv7qz9KlH-RbMjebudwPRnU-yeg") 60 | 61 | // header: {"alg": "HS256", "typ": "JWT"} 62 | // same payload. 63 | const jwtInvalidAlgToken = ("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9." + 64 | "Intcblx0XCJhdWRcIjogXCJteS1jbGllbnQtaWRcIlxuXHRcImF6cFwiOiBcImhlbGxvLWFuZ" + 65 | "HJvaWRcIlxuXHRcImVtYWlsXCI6IFwiZHVkZUBnbWFpbC5jb21cIlxuXHRcImV4cFwiOiAxMz" + 66 | "cwMzUyMjUyXG5cdFwiaWF0XCI6IDEzNzAzNDg2NTJcblx0XCJpc3NcIjogXCJhY2NvdW50cy5" + 67 | "nb29nbGUuY29tXCJcbn0i." + 68 | "ylh77ZOrr_Bkd3iFZRrQNcf_GCjCpUtcdhWz3AOLWUA") 69 | 70 | // Private key for testing. 71 | // Used to create exponent, modulus and sign JWT token. 72 | const privateKeyPem = `-----BEGIN RSA PRIVATE KEY----- 73 | MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj 74 | 7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/ 75 | xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs 76 | SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18 77 | pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk 78 | SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk 79 | nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq 80 | HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y 81 | nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9 82 | IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2 83 | YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU 84 | Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ 85 | vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP 86 | B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl 87 | aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2 88 | eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI 89 | aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk 90 | klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ 91 | CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu 92 | UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg 93 | soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28 94 | bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH 95 | 504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL 96 | YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx 97 | BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg== 98 | -----END RSA PRIVATE KEY-----` 99 | 100 | // openssl rsa -in private.pem -noout -text 101 | const googCerts = `{ 102 | "keyvalues": [{ 103 | "algorithm": "RSA", 104 | "modulus": "` + 105 | "AOHo9Ke20Oy/6+K1RlM/UTeFUFqHo9QYNZmFiA2LJbyq5NYKY+8GYJHZjO6FStjn4" + 106 | "K0o/xQk5CJyj6mgkdIA+yvNKvpiwazdz1JA6yUGab0WSH39/8ZlVNVnq7kCxSY5xW" + 107 | "9nartLPxWP9pFoibQQmpF71xDm+832yDbmsi/jErAhknVWLEpYkuakKYMg2qfvG+X" + 108 | "6V2VSZKvdoa3DbW5IO4QmFCXM7fJ/YXAkyQn1KQKb2hUNfKXvs6cpePls5D+uiNvI" + 109 | "Z4jEMlJQNYQYrJE+6oqLqtJvCDqHYb9n2oEmSozk01NsZEgXP/38t2abC+1qoLGUu" + 110 | "0PtFUSvICn6pMYOTmE=" + `", 111 | "exponent": "AQAB", 112 | "keyid": "goog-123" 113 | }] 114 | }` 115 | 116 | func TestVerifySignedJWT(t *testing.T) { 117 | r, _, closer := newTestRequest(t, "GET", "/", nil) 118 | defer closer() 119 | nc, err := appengine.Namespace(appengine.NewContext(r), certNamespace) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | item := &memcache.Item{Key: DefaultCertURI, Value: []byte(googCerts)} 125 | if err := memcache.Set(nc, item); err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | tts := []struct { 130 | token string 131 | now time.Time 132 | want *signedJWT 133 | }{ 134 | {jwtValidTokenString, jwtValidTokenTime, &jwtValidTokenObject}, 135 | {jwtValidTokenString, jwtValidTokenTime.Add(time.Hour * 24), nil}, 136 | {jwtValidTokenString, jwtValidTokenTime.Add(-time.Hour * 24), nil}, 137 | {jwtInvalidKeyToken, jwtValidTokenTime, nil}, 138 | {jwtInvalidAlgToken, jwtValidTokenTime, nil}, 139 | {"invalid.token", jwtValidTokenTime, nil}, 140 | {"another.invalid.token", jwtValidTokenTime, nil}, 141 | } 142 | 143 | ec := NewContext(r) 144 | 145 | for i, tt := range tts { 146 | jwt, err := verifySignedJWT(ec, tt.token, tt.now.Unix()) 147 | switch { 148 | case err != nil && tt.want != nil: 149 | t.Errorf("%d: verifySignedJWT(%q, %d) = %v; want %#v", 150 | i, tt.token, tt.now.Unix(), err, tt.want) 151 | case err == nil && tt.want == nil: 152 | t.Errorf("%d: verifySignedJWT(%q, %d) = %#v; want error", 153 | i, tt.token, tt.now.Unix(), jwt) 154 | case err == nil && tt.want != nil: 155 | if !reflect.DeepEqual(jwt, tt.want) { 156 | t.Errorf("%d: verifySignedJWT(%q, %d) = %v; want %#v", 157 | i, tt.token, tt.now.Unix(), jwt, tt.want) 158 | } 159 | } 160 | } 161 | } 162 | 163 | func TestVerifyParsedToken(t *testing.T) { 164 | const ( 165 | goog = "accounts.google.com" 166 | clientID = "my-client-id" 167 | email = "dude@gmail.com" 168 | ) 169 | audiences := []string{clientID, "hello-android"} 170 | clientIDs := []string{clientID} 171 | 172 | tts := []struct { 173 | issuer, audience, clientID, email string 174 | valid bool 175 | }{ 176 | {goog, clientID, clientID, email, true}, 177 | {goog, "hello-android", clientID, email, true}, 178 | {goog, "invalid", clientID, email, false}, 179 | {goog, clientID, "invalid", email, false}, 180 | {goog, clientID, clientID, "", false}, 181 | {"", clientID, clientID, email, false}, 182 | } 183 | 184 | r, _, closer := newTestRequest(t, "GET", "/", nil) 185 | defer closer() 186 | c := NewContext(r) 187 | 188 | for i, tt := range tts { 189 | jwt := signedJWT{ 190 | Issuer: tt.issuer, 191 | Audience: tt.audience, 192 | ClientID: tt.clientID, 193 | Email: tt.email, 194 | } 195 | res := verifyParsedToken(c, jwt, audiences, clientIDs) 196 | if res != tt.valid { 197 | t.Errorf("%d: verifyParsedToken(%#v, %v, %v) = %v; want %v", 198 | i, jwt, audiences, clientIDs, res, tt.valid) 199 | } 200 | } 201 | } 202 | 203 | func TestCurrentIDTokenUser(t *testing.T) { 204 | jwtOrigParser := jwtParser 205 | defer func() { 206 | jwtParser = jwtOrigParser 207 | }() 208 | 209 | r, _, closer := newTestRequest(t, "GET", "/", nil) 210 | defer closer() 211 | c := NewContext(r) 212 | 213 | aud := []string{jwtValidTokenObject.Audience, jwtValidTokenObject.ClientID} 214 | azp := []string{jwtValidTokenObject.ClientID} 215 | 216 | jwtUnacceptedToken := signedJWT{ 217 | Audience: "my-other-client-id", 218 | ClientID: "my-other-client-id", 219 | Email: "me@gmail.com", 220 | Expires: 1370352252, 221 | IssuedAt: 1370348652, 222 | Issuer: "accounts.google.com", 223 | } 224 | 225 | tts := []struct { 226 | token *signedJWT 227 | wantEmail string 228 | }{ 229 | {&jwtValidTokenObject, jwtValidTokenObject.Email}, 230 | {&jwtUnacceptedToken, ""}, 231 | {nil, ""}, 232 | } 233 | 234 | var currToken *signedJWT 235 | 236 | jwtParser = func(context.Context, string, int64) (*signedJWT, error) { 237 | if currToken == nil { 238 | return nil, errors.New("Fake verification failed") 239 | } 240 | return currToken, nil 241 | } 242 | 243 | for i, tt := range tts { 244 | currToken = tt.token 245 | user, err := currentIDTokenUser(c, 246 | jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix()) 247 | switch { 248 | case tt.wantEmail != "" && err != nil: 249 | t.Errorf("%d: currentIDTokenUser(%q, %v, %v, %d) = %v; want email = %q", 250 | i, jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix(), err, tt.wantEmail) 251 | case tt.wantEmail == "" && err == nil: 252 | t.Errorf("%d: currentIDTokenUser(%q, %v, %v, %d) = %#v; want error", 253 | i, jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix(), user) 254 | case err == nil && tt.wantEmail != user.Email: 255 | t.Errorf("%d: currentIDTokenUser(%q, %v, %v, %d) = %#v; want email = %q", 256 | i, jwtValidTokenString, aud, azp, jwtValidTokenTime.Unix(), user, tt.wantEmail) 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /endpoints/service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Copyright 2012 The Gorilla Authors. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package endpoints 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "reflect" 13 | "strings" 14 | "sync" 15 | "unicode" 16 | "unicode/utf8" 17 | 18 | "golang.org/x/net/context" 19 | ) 20 | 21 | var ( 22 | // Precompute the reflect type for error. 23 | typeOfOsError = reflect.TypeOf((*error)(nil)).Elem() 24 | // Same as above, this time for http.Request. 25 | typeOfRequest = reflect.TypeOf((*http.Request)(nil)).Elem() 26 | // Precompute the reflect type for context.Context. 27 | typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem() 28 | // Precompute the reflect type for *VoidMessage. 29 | typeOfVoidMessage = reflect.TypeOf(new(VoidMessage)) 30 | ) 31 | 32 | // ---------------------------------------------------------------------------- 33 | // service 34 | // ---------------------------------------------------------------------------- 35 | 36 | // RPCService represents a service registered with a specific Server. 37 | type RPCService struct { 38 | name string // name of service 39 | rcvr reflect.Value // receiver of methods for the service 40 | rcvrType reflect.Type // type of the receiver 41 | methods map[string]*ServiceMethod // registered methods 42 | 43 | internal bool 44 | info *ServiceInfo 45 | } 46 | 47 | // Name returns service method name 48 | // TODO: remove or use info.Name here? 49 | func (s *RPCService) Name() string { 50 | return s.name 51 | } 52 | 53 | // Info returns a ServiceInfo which is used to construct Endpoints API config 54 | func (s *RPCService) Info() *ServiceInfo { 55 | return s.info 56 | } 57 | 58 | // Methods returns a slice of all service's registered methods 59 | func (s *RPCService) Methods() []*ServiceMethod { 60 | items := make([]*ServiceMethod, 0, len(s.methods)) 61 | for _, m := range s.methods { 62 | items = append(items, m) 63 | } 64 | return items 65 | } 66 | 67 | // MethodByName returns a ServiceMethod of a registered service's method or nil. 68 | func (s *RPCService) MethodByName(name string) *ServiceMethod { 69 | return s.methods[name] 70 | } 71 | 72 | // ServiceInfo is used to construct Endpoints API config 73 | type ServiceInfo struct { 74 | Name string 75 | Version string 76 | Default bool 77 | Description string 78 | } 79 | 80 | // ServiceMethod is what represents a method of a registered service 81 | type ServiceMethod struct { 82 | // Type of the request data structure 83 | ReqType reflect.Type 84 | // Type of the response data structure 85 | RespType reflect.Type 86 | // method's receiver 87 | method *reflect.Method 88 | // first argument of the method is Context 89 | wantsContext bool 90 | // info used to construct Endpoints API config 91 | info *MethodInfo 92 | } 93 | 94 | // Info returns a MethodInfo struct of a registered service's method 95 | func (m *ServiceMethod) Info() *MethodInfo { 96 | return m.info 97 | } 98 | 99 | // MethodInfo is what's used to construct Endpoints API config 100 | type MethodInfo struct { 101 | // name can also contain resource, e.g. "greets.list" 102 | Name string 103 | Path string 104 | HTTPMethod string 105 | Scopes []string 106 | Audiences []string 107 | ClientIds []string 108 | Desc string 109 | } 110 | 111 | // ---------------------------------------------------------------------------- 112 | // serviceMap 113 | // ---------------------------------------------------------------------------- 114 | 115 | // serviceMap is a registry for services. 116 | type serviceMap struct { 117 | mutex sync.Mutex 118 | services map[string]*RPCService 119 | } 120 | 121 | // register adds a new service using reflection to extract its methods. 122 | // 123 | // internal == true indicase that this is an internal service, 124 | // e.g. BackendService 125 | func (m *serviceMap) register(srv interface{}, name, ver, desc string, isDefault, internal bool) ( 126 | *RPCService, error) { 127 | 128 | // Setup service. 129 | s := &RPCService{ 130 | rcvr: reflect.ValueOf(srv), 131 | rcvrType: reflect.TypeOf(srv), 132 | methods: make(map[string]*ServiceMethod), 133 | internal: internal, 134 | } 135 | s.name = reflect.Indirect(s.rcvr).Type().Name() 136 | if !isExported(s.name) { 137 | return nil, fmt.Errorf("endpoints: no service name for type %q", 138 | s.rcvrType.String()) 139 | } 140 | 141 | if !internal { 142 | s.info = &ServiceInfo{ 143 | Name: name, 144 | Version: ver, 145 | Default: isDefault, 146 | Description: desc, 147 | } 148 | if s.info.Name == "" { 149 | s.info.Name = s.name 150 | } 151 | s.info.Name = strings.ToLower(s.info.Name) 152 | if s.info.Version == "" { 153 | s.info.Version = "v1" 154 | } 155 | } 156 | 157 | // Setup methods. 158 | for i := 0; i < s.rcvrType.NumMethod(); i++ { 159 | method := s.rcvrType.Method(i) 160 | srvMethod := newServiceMethod(&method, internal) 161 | if srvMethod != nil { 162 | s.methods[method.Name] = srvMethod 163 | } 164 | } 165 | if len(s.methods) == 0 { 166 | return nil, fmt.Errorf( 167 | "endpoints: %q has no exported methods of suitable type", s.name) 168 | } 169 | 170 | // Add to the map. 171 | m.mutex.Lock() 172 | defer m.mutex.Unlock() 173 | if m.services == nil { 174 | m.services = make(map[string]*RPCService) 175 | } else if _, ok := m.services[s.name]; ok { 176 | return nil, fmt.Errorf("endpoints: service already defined: %q", s.name) 177 | } 178 | m.services[s.name] = s 179 | return s, nil 180 | } 181 | 182 | // newServiceMethod creates a new ServiceMethod from provided Go's Method. 183 | // 184 | // It doesn't create ServiceMethod.info if internal == true 185 | func newServiceMethod(m *reflect.Method, internal bool) *ServiceMethod { 186 | // Method must be exported. 187 | if m.PkgPath != "" { 188 | log.Printf("method %#v is not exported", m) 189 | return nil 190 | } 191 | 192 | mtype := m.Type 193 | numIn, numOut := mtype.NumIn(), mtype.NumOut() 194 | 195 | // Endpoint methods have at least a receiver plus one to three arguments and 196 | // return either one or two values. 197 | if !(2 <= numIn && numIn <= 4 && 1 <= numOut && numOut <= 2) { 198 | return nil 199 | } 200 | // The response message is either an input or and output, not both. 201 | if numIn == 4 && numOut == 2 { 202 | return nil 203 | } 204 | 205 | // Endpoint methods have an http request or context as first argument. 206 | httpReqType := mtype.In(1) 207 | // If there's a request type it's the second argument. 208 | reqType := typeOfVoidMessage 209 | if numIn > 2 { 210 | reqType = mtype.In(2) 211 | } 212 | // The response type can be either as the third argument or the first 213 | // returned value followed by an error. 214 | respType := typeOfVoidMessage 215 | if numIn > 3 { 216 | respType = mtype.In(3) 217 | } else if numOut == 2 { 218 | respType = mtype.Out(0) 219 | } 220 | // The last returned value is an error. 221 | errType := mtype.Out(mtype.NumOut() - 1) 222 | 223 | // First argument must be a pointer and must be http.Request or Context. 224 | if !isRequestOrContext(httpReqType) { 225 | return nil 226 | } 227 | // Second argument must be a pointer and must be exported. 228 | if reqType.Kind() != reflect.Ptr || !isExportedOrBuiltin(reqType) { 229 | return nil 230 | } 231 | // Return value must be a pointer and must be exported. 232 | if respType.Kind() != reflect.Ptr || !isExportedOrBuiltin(respType) { 233 | return nil 234 | } 235 | // Last return value must be of error type 236 | if errType != typeOfOsError { 237 | return nil 238 | } 239 | 240 | method := &ServiceMethod{ 241 | ReqType: reqType.Elem(), 242 | RespType: respType.Elem(), 243 | method: m, 244 | wantsContext: httpReqType.Implements(typeOfContext), 245 | } 246 | if !internal { 247 | mname := strings.ToLower(m.Name) 248 | method.info = &MethodInfo{Name: mname} 249 | 250 | params := requiredParamNames(method.ReqType) 251 | numParam := len(params) 252 | if method.ReqType.Kind() == reflect.Struct { 253 | switch { 254 | default: 255 | method.info.HTTPMethod = "POST" 256 | case numParam == method.ReqType.NumField(): 257 | method.info.HTTPMethod = "GET" 258 | } 259 | } 260 | if numParam == 0 { 261 | method.info.Path = mname 262 | } else { 263 | method.info.Path = mname + "/{" + strings.Join(params, "}/{") + "}" 264 | } 265 | } 266 | return method 267 | } 268 | 269 | // Used to infer method's info.Path. 270 | // TODO: refactor this and move to apiconfig.go? 271 | func requiredParamNames(t reflect.Type) []string { 272 | if t.Kind() == reflect.Struct { 273 | params := make([]string, 0, t.NumField()) 274 | for i := 0; i < t.NumField(); i++ { 275 | field := t.Field(i) 276 | // consider only exported fields 277 | if field.PkgPath == "" { 278 | parts := strings.Split(field.Tag.Get("endpoints"), ",") 279 | for _, p := range parts { 280 | if p == "required" { 281 | params = append(params, field.Name) 282 | break 283 | } 284 | } 285 | } 286 | } 287 | return params 288 | } 289 | return []string{} 290 | } 291 | 292 | // get returns a registered service given a method name. 293 | // 294 | // The method name uses a dotted notation as in "Service.Method". 295 | func (m *serviceMap) get(method string) (*RPCService, *ServiceMethod, error) { 296 | parts := strings.Split(method, ".") 297 | if len(parts) != 2 { 298 | err := fmt.Errorf("endpoints: service/method request ill-formed: %q", method) 299 | return nil, nil, err 300 | } 301 | parts[1] = strings.Title(parts[1]) 302 | 303 | m.mutex.Lock() 304 | service := m.services[parts[0]] 305 | m.mutex.Unlock() 306 | if service == nil { 307 | err := fmt.Errorf("endpoints: can't find service %q", parts[0]) 308 | return nil, nil, err 309 | } 310 | ServiceMethod := service.methods[parts[1]] 311 | if ServiceMethod == nil { 312 | err := fmt.Errorf( 313 | "endpoints: can't find method %q of service %q", parts[1], parts[0]) 314 | return nil, nil, err 315 | } 316 | return service, ServiceMethod, nil 317 | } 318 | 319 | // serviceByName returns a registered service or nil if there's no service 320 | // registered by that name. 321 | func (m *serviceMap) serviceByName(serviceName string) *RPCService { 322 | m.mutex.Lock() 323 | defer m.mutex.Unlock() 324 | return m.services[serviceName] 325 | } 326 | 327 | // isExported returns true of a string is an exported (upper case) name. 328 | func isExported(name string) bool { 329 | rune, _ := utf8.DecodeRuneInString(name) 330 | return unicode.IsUpper(rune) 331 | } 332 | 333 | // isExportedOrBuiltin returns true if a type is exported or a builtin. 334 | func isExportedOrBuiltin(t reflect.Type) bool { 335 | for t.Kind() == reflect.Ptr { 336 | t = t.Elem() 337 | } 338 | // PkgPath will be non-empty even for an exported type, 339 | // so we need to check the type name as well. 340 | return isExported(t.Name()) || t.PkgPath() == "" 341 | } 342 | 343 | // isRequestOrContext returns true if type t is either *http.Request or Context 344 | func isRequestOrContext(t reflect.Type) bool { 345 | if t.Implements(typeOfContext) { 346 | return true 347 | } 348 | return t.Kind() == reflect.Ptr && t.Elem() == typeOfRequest 349 | } 350 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | 3 | This package provides Go support for Cloud Endpoints Frameworks v1, which is **DEPRECATED** 4 | and will be shut down on August 2, 2018. 5 | 6 | This package is unmaintained and comes with no support or guarantee. 7 | 8 | If used this product and were primarily interested in code generation and API specifications, you should probably 9 | investigate the [OpenAPI specification](https://www.google.com/search?q=golang+openapi) and ecosystem. 10 | 11 | If you want API authentication and monitoring, you should check out the newer 12 | Google Cloud Endpoints product (using [OpenAPI](https://cloud.google.com/endpoints/docs/openapi/) or [gRPC](https://cloud.google.com/endpoints/docs/grpc/)). 13 | Unfortunately, App Engine standard is not supported, so you will need to move your API to App Engine flexible, Compute Engine, or Kubernetes. 14 | 15 | --------------------------------------- 16 | --------------------------------------- 17 | --------------------------------------- 18 | --------------------------------------- 19 | --------------------------------------- 20 | --------------------------------------- 21 | --------------------------------------- 22 | --------------------------------------- 23 | --------------------------------------- 24 | --------------------------------------- 25 | --------------------------------------- 26 | 27 | 28 | # Cloud Endpoints for Go 29 | 30 | This package will let you write Cloud Endpoints backends in Go. 31 | 32 | If you're not familiar with Cloud Endpoints, see Google App Engine official 33 | documentation for [Python][1] or [Java][2]. 34 | 35 | ## Install 36 | 37 | Use [goapp tool][goapp] from the Google App Engine SDK for Go to get the package: 38 | 39 | ``` 40 | GO_APPENGINE/goapp get github.com/GoogleCloudPlatform/go-endpoints/endpoints 41 | ``` 42 | 43 | If you'll ever need to pull updates from the upstream, execute `git pull` 44 | from the root of this repo. 45 | 46 | Alternatively, if you don't have `goapp` for some reason, do the standard 47 | 48 | ``` 49 | go get github.com/GoogleCloudPlatform/go-endpoints/endpoints 50 | ``` 51 | 52 | If this is not the first time you're "getting" the package, 53 | add `-u` param to get an updated version, i.e. `go get -u ...`. 54 | 55 | Now, you'll see a couple errors: 56 | 57 | ``` 58 | package appengine: unrecognized import path "appengine" 59 | package appengine/user: unrecognized import path "appengine/user" 60 | package appengine_internal/user: unrecognized import path "appengine_internal/user" 61 | ``` 62 | 63 | which is OK, don't worry! The issue here is Go looks at all imports in 64 | `endpoints` package and cannot find "appengine/*" packages nowhere in your 65 | `$GOPATH`. That's because they're not there, indeed. Appengine packages are 66 | normally available only when running an app with dev appserver, and since that's 67 | precisely what we want to do, "unrecognized import path" errors can be safely 68 | ignored. 69 | 70 | 71 | ## Usage 72 | 73 | Declare structs which describe your data. For instance: 74 | 75 | ```go 76 | // Greeting is a datastore entity that represents a single greeting. 77 | // It also serves as (a part of) a response of GreetingService. 78 | type Greeting struct { 79 | Key *datastore.Key `json:"id" datastore:"-"` 80 | Author string `json:"author"` 81 | Content string `json:"content" datastore:",noindex" endpoints:"req"` 82 | Date time.Time `json:"date"` 83 | } 84 | 85 | // GreetingsList is a response type of GreetingService.List method 86 | type GreetingsList struct { 87 | Items []*Greeting `json:"items"` 88 | } 89 | 90 | // Request type for GreetingService.List 91 | type GreetingsListReq struct { 92 | Limit int `json:"limit" endpoints:"d=10"` 93 | } 94 | ``` 95 | 96 | Then, a service: 97 | 98 | ```go 99 | // GreetingService can sign the guesbook, list all greetings and delete 100 | // a greeting from the guestbook. 101 | type GreetingService struct { 102 | } 103 | 104 | // List responds with a list of all greetings ordered by Date field. 105 | // Most recent greets come first. 106 | func (gs *GreetingService) List(c context.Context, r *GreetingsListReq) (*GreetingsList, error) { 107 | if r.Limit <= 0 { 108 | r.Limit = 10 109 | } 110 | 111 | q := datastore.NewQuery("Greeting").Order("-Date").Limit(r.Limit) 112 | greets := make([]*Greeting, 0, r.Limit) 113 | keys, err := q.GetAll(c, &greets) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | for i, k := range keys { 119 | greets[i].Key = k 120 | } 121 | return &GreetingsList{greets}, nil 122 | } 123 | ``` 124 | 125 | We can also define methods that don't require a response or a request. 126 | ```go 127 | // Add adds a greeting. 128 | func (gs *GreetingService) Add(c context.Context, g *Greeting) error { 129 | k := datastore.NewIncompleteKey(c, "Greeting", nil) 130 | _, err := datastore.Put(c, k, g) 131 | return err 132 | } 133 | 134 | type Count struct { 135 | N int `json:"count"` 136 | } 137 | 138 | // Count returns the number of greetings. 139 | func (gs *GreetingService) Count(c context.Context) (*Count, error) { 140 | n, err := datastore.NewQuery("Greeting").Count(c) 141 | if err != nil { 142 | return nil, err 143 | } 144 | return &Count{n}, nil 145 | } 146 | ``` 147 | 148 | Last step is to make the above available as a **discoverable API** 149 | and leverage all the juicy stuff Cloud Endpoints are great at. 150 | 151 | ```go 152 | import "github.com/GoogleCloudPlatform/go-endpoints/endpoints" 153 | 154 | func init() { 155 | greetService := &GreetingService{} 156 | api, err := endpoints.RegisterService(greetService, 157 | "greeting", "v1", "Greetings API", true) 158 | if err != nil { 159 | log.Fatalf("Register service: %v", err) 160 | } 161 | 162 | register := func(orig, name, method, path, desc string) { 163 | m := api.MethodByName(orig) 164 | if m == nil { 165 | log.Fatalf("Missing method %s", orig) 166 | } 167 | i := m.Info() 168 | i.Name, i.HTTPMethod, i.Path, i.Desc = name, method, path, desc 169 | } 170 | 171 | register("List", "greets.list", "GET", "greetings", "List most recent greetings.") 172 | register("Add", "greets.add", "PUT", "greetings", "Add a greeting.") 173 | register("Count", "greets.count", "GET", "greetings/count", "Count all greetings.") 174 | endpoints.HandleHTTP() 175 | } 176 | ``` 177 | 178 | Don't forget to add URL matching in app.yaml: 179 | 180 | ```yaml 181 | application: my-app-id 182 | version: v1 183 | threadsafe: true 184 | 185 | runtime: go 186 | api_version: go1 187 | 188 | handlers: 189 | - url: /.* 190 | script: _go_app 191 | 192 | # Important! Even though there's a catch all routing above, 193 | # without these two lines it's not going to work. 194 | # Make sure you have this: 195 | - url: /_ah/spi/.* 196 | script: _go_app 197 | ``` 198 | 199 | That's it. It is time to start dev server and enjoy the discovery doc at 200 | [localhost:8080/_ah/api/discovery/v1/apis/greeting/v1/rest][5] 201 | 202 | Naturally, API Explorer works too: 203 | [localhost:8080/_ah/api/explorer][6] 204 | 205 | Time to deploy the app on [appengine.appspot.com][7]! 206 | 207 | **N.B.** At present, you can't map your endpoint URL to a custom domain. Bossylobster 208 | [wrote](http://stackoverflow.com/a/16124815/1745000): "It's a non-trivial networking problem 209 | and something Google certainly plan on supporting in the future. Keep in mind, Cloud Endpoints 210 | is a combination or App Engine and Google's API Infrastructure." 211 | 212 | ## Generate client libs 213 | 214 | Now that we have the discovery doc, let's generate some client libraries. 215 | 216 | ### Android 217 | 218 | ``` 219 | $ URL='https://my-app-id.appspot.com/_ah/api/discovery/v1/apis/greeting/v1/rest' 220 | $ curl -s $URL > greetings.rest.discovery 221 | 222 | # Optionally check the discovery doc 223 | $ less greetings.rest.discovery 224 | 225 | $ GO_SDK/endpointscfg.py gen_client_lib java greetings.rest.discovery 226 | ``` 227 | 228 | You should be able to find `./greetings.rest.zip` file with Java client source 229 | code and its dependencies. 230 | 231 | Once you have that, follow the official guide: 232 | [Using Endpoints in an Android Client][8]. 233 | 234 | ### iOS 235 | 236 | ``` 237 | # Note the rpc suffix in the URL: 238 | $ URL='https://my-app-id.appspot.com/_ah/api/discovery/v1/apis/greeting/v1/rpc' 239 | $ curl -s $URL > greetings.rpc.discovery 240 | 241 | # Optionally check the discovery doc 242 | $ less greetings.rpc.discovery 243 | ``` 244 | 245 | Then, feed `greetings.rpc.discovery` file to the library generator on OS X as 246 | described in the official guide [Using Endpoints in an iOS Client][9]. 247 | 248 | ### JavaScript 249 | 250 | There's really nothing to generate for JavaScript, you just use it! 251 | 252 | Here's the official guide: [Using Endpoints in a JavaScript client][10]. 253 | 254 | ### Dart 255 | 256 | 257 | ``` 258 | # Clone or fork discovery_api_dart_client_generator 259 | git clone https://github.com/dart-gde/discovery_api_dart_client_generator 260 | cd discovery_api_dart_client_generator 261 | pub install 262 | 263 | # Generate your client library: 264 | mkdir input 265 | URL='https://my-app-id.appspot.com/_ah/api/discovery/v1/apis/greeting/v1/rest' 266 | curl -s -o input/greeting.json $URL 267 | bin/generate.dart package -i input -o ../dart_my-app-id_v1_api_client --package-name my-app-id_v1_api 268 | ``` 269 | 270 | Now you just have to add your endpoints client library to your dart application (assuming it is in the parent directory.) 271 | 272 | ``` 273 | cd ../my-app_dart/ 274 | cat >>pubspec.yaml < /dev/null 311 | ``` 312 | 313 | [Learn more about goapp tool][goapp]. 314 | 315 | 316 | 317 | [1]: https://cloud.google.com/appengine/docs/python/endpoints/ 318 | [2]: https://cloud.google.com/appengine/docs/java/endpoints/ 319 | [3]: https://github.com/crhym3/go-tictactoe 320 | [5]: http://localhost:8080/_ah/api/discovery/v1/apis/greeting/v1/rest 321 | [6]: http://localhost:8080/_ah/api/explorer 322 | [7]: http://appengine.appspot.com 323 | [8]: https://developers.google.com/appengine/docs/python/endpoints/consume_android 324 | [9]: https://developers.google.com/appengine/docs/python/endpoints/consume_ios 325 | [10]: https://developers.google.com/appengine/docs/python/endpoints/consume_js 326 | [11]: http://godoc.org/github.com/GoogleCloudPlatform/go-endpoints/endpoints 327 | [12]: https://github.com/GoogleCloudPlatform/go-endpoints/wiki 328 | [13]: https://go-endpoints.appspot.com/tictactoe 329 | [goapp]: http://blog.golang.org/appengine-dec2013 330 | -------------------------------------------------------------------------------- /endpoints/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package endpoints will let you write Cloud Endpoints backend in Go. 3 | 4 | Usage 5 | 6 | Declare structs which describe your data. For instance: 7 | 8 | // Greeting is a datastore entity that represents a single greeting. 9 | // It also serves as (a part of) a response of GreetingService. 10 | type Greeting struct { 11 | Key *datastore.Key `json:"id" datastore:"-"` 12 | Author string `json:"author"` 13 | Content string `json:"content" datastore:",noindex" endpoints:"req"` 14 | Date time.Time `json:"date"` 15 | } 16 | 17 | // GreetingsList is a response type of GreetingService.List method 18 | type GreetingsList struct { 19 | Items []*Greeting `json:"items"` 20 | } 21 | 22 | // Request type for GreetingService.List 23 | type GreetingsListReq struct { 24 | Limit int `json:"limit" endpoints:"d=10"` 25 | } 26 | 27 | 28 | Then, a service: 29 | 30 | // GreetingService can sign the guesbook, list all greetings and delete 31 | // a greeting from the guestbook. 32 | type GreetingService struct { 33 | } 34 | 35 | // List responds with a list of all greetings ordered by Date field. 36 | // Most recent greets come first. 37 | func (gs *GreetingService) List(c context.Context, r *GreetingsListReq) (*GreetingsList, error) { 38 | if r.Limit <= 0 { 39 | r.Limit = 10 40 | } 41 | 42 | q := datastore.NewQuery("Greeting").Order("-Date").Limit(r.Limit) 43 | greets := make([]*Greeting, 0, r.Limit) 44 | keys, err := q.GetAll(c, &greets) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | for i, k := range keys { 50 | greets[i].Key = k 51 | } 52 | return &GreetingsList{greets}, nil 53 | } 54 | 55 | 56 | Last step is to make the above available as a discoverable API 57 | and leverage all the juicy stuff Cloud Endpoints are great at. 58 | 59 | import "github.com/GoogleCloudPlatform/go-endpoints/endpoints" 60 | 61 | func init() { 62 | greetService := &GreetingService{} 63 | api, err := endpoints.RegisterService(greetService, 64 | "greeting", "v1", "Greetings API", true) 65 | if err != nil { 66 | panic(err.Error()) 67 | } 68 | 69 | info := api.MethodByName("List").Info() 70 | info.Name, info.HTTPMethod, info.Path, info.Desc = 71 | "greets.list", "GET", "greetings", "List most recent greetings." 72 | 73 | endpoints.HandleHTTP() 74 | } 75 | 76 | 77 | Don't forget to add URL matching in app.yaml: 78 | 79 | application: my-app-id 80 | version: v1 81 | threadsafe: true 82 | 83 | runtime: go 84 | api_version: go1 85 | 86 | handlers: 87 | - url: /.* 88 | script: _go_app 89 | 90 | # Important! Even though there's a catch all routing above, 91 | # without these two lines it's not going to work. 92 | # Make sure you have this: 93 | - url: /_ah/spi/.* 94 | script: _go_app 95 | 96 | That's it. It is time to start dev server and enjoy the discovery doc: 97 | http://localhost:8080/_ah/api/explorer 98 | 99 | 100 | Custom types 101 | 102 | You can define your own types and use them directly as a field type in a 103 | service method request/response as long as they implement json.Marshaler and 104 | json.Unmarshaler interfaces. 105 | 106 | Let's say we have this method: 107 | 108 | func (s *MyService) ListItems(c context.Context, r *ListReq) (*ItemsList, error) { 109 | // fetch a list of items 110 | } 111 | 112 | where ListReq and ItemsList are defined as follows: 113 | 114 | type ListReq struct { 115 | Limit int `json:"limit,string" endpoints:"d=10,max=100"` 116 | Page *QueryMarker `json:"cursor"` 117 | } 118 | 119 | type ItemsList struct { 120 | Items []*Item `json:"items"` 121 | Next *QueryMarker `json:"next,omitempty"` 122 | } 123 | 124 | What's interesting here is ListReq.Page and ItemsList.Next fields which are 125 | of type QueryMarker: 126 | 127 | import "appengine/datastore" 128 | 129 | type QueryMarker struct { 130 | datastore.Cursor 131 | } 132 | 133 | func (qm *QueryMarker) MarshalJSON() ([]byte, error) { 134 | return []byte(`"` + qm.String() + `"`), nil 135 | } 136 | 137 | func (qm *QueryMarker) UnmarshalJSON(buf []byte) error { 138 | if len(buf) < 2 || buf[0] != '"' || buf[len(buf)-1] != '"' { 139 | return errors.New("QueryMarker: bad cursor value") 140 | } 141 | cursor, err := datastore.DecodeCursor(string(buf[1 : len(buf)-1])) 142 | if err != nil { 143 | return err 144 | } 145 | *qm = QueryMarker{cursor} 146 | return nil 147 | } 148 | 149 | Now that our QueryMarker implements required interfaces we can use ListReq.Page 150 | field as if it were a `datastore.Cursor` in our service method, for instance: 151 | 152 | func (s *MyService) ListItems(c context.Context, r *ListReq) (*ItemsList, error) { 153 | list := &ItemsList{Items: make([]*Item, 0, r.Limit)} 154 | 155 | q := datastore.NewQuery("Item").Limit(r.Limit) 156 | if r.Page != nil { 157 | q = q.Start(r.Page.Cursor) 158 | } 159 | 160 | var iter *datastore.Iterator 161 | for iter := q.Run(c); ; { 162 | var item Item 163 | key, err := iter.Next(&item) 164 | if err == datastore.Done { 165 | break 166 | } 167 | if err != nil { 168 | return nil, err 169 | } 170 | item.Key = key 171 | list.Items = append(list.Items, &item) 172 | } 173 | 174 | cur, err := iter.Cursor() 175 | if err != nil { 176 | return nil, err 177 | } 178 | list.Next = &QueryMarker{cur} 179 | return list, nil 180 | } 181 | 182 | A serialized ItemsList would then look something like this: 183 | 184 | { 185 | "items": [ 186 | { 187 | "id": "5629499534213120", 188 | "name": "A TV set", 189 | "price": 123.45 190 | } 191 | ], 192 | "next": "E-ABAIICImoNZGV2fmdvcGhtYXJrc3IRCxIEVXNlchiAgICAgICACgwU" 193 | } 194 | 195 | Another nice thing about this is, some types in appengine/datastore package 196 | already implement json.Marshal and json.Unmarshal. 197 | 198 | Take, for instance, datastore.Key. I could use it as an ID in my JSON response 199 | out of the box, if I wanted to: 200 | 201 | type User struct { 202 | Key *datastore.Key `json:"id" datastore:"-"` 203 | Name string `json:"name" datastore:"name"` 204 | Role string `json:"role" datastore:"role"` 205 | Email string `json:"email" datastore:"email"` 206 | } 207 | 208 | type GetUserReq struct { 209 | Key *datastore.Key `json:"id"` 210 | } 211 | 212 | // defined with "users/{id}" path template 213 | func (s *MyService) GetUser(c context.Context, r *GetUserReq) (*User, error) { 214 | user := &User{} 215 | if err := datastore.Get(c, r.Key, user); err != nil { 216 | return nil, err 217 | } 218 | user.Key = r.Key 219 | return user, nil 220 | } 221 | 222 | JSON would then look something like this: 223 | 224 | GET /_ah/api/myapi/v1/users/ag1kZXZ-Z29waG1hcmtzchELEgRVc2VyGICAgICAgIAKDA 225 | 226 | { 227 | "id": "ag1kZXZ-Z29waG1hcmtzchELEgRVc2VyGICAgICAgIAKDA", 228 | "name": "John Doe", 229 | "role": "member", 230 | "email": "user@example.org" 231 | } 232 | 233 | 234 | Field tags 235 | 236 | Go Endpoints has its own field tag "endpoints" which you can use to let your 237 | clients know what a service method data constraints are (on input): 238 | 239 | - req, means "required". 240 | - d, default value, cannot be used together with req. 241 | - min and max constraints. Can be used only on int and uint (8/16/32/64 bits). 242 | - desc, a field description. Cannot contain a "," (comma) for now. 243 | 244 | Let's see an example: 245 | 246 | type TaggedStruct struct { 247 | A int `endpoints:"req,min=0,max=100,desc=An int field"` 248 | B int `endpoints:"d=10,min=1,max=200"` 249 | C string `endpoints:"req,d=Hello gopher,desc=A string field"` 250 | } 251 | 252 | - A field is required and has min & max constrains, is described as "An int field" 253 | - B field is not required, defaults to 10 and has min & max constrains 254 | - C field is required, defaults to "Hello gopher", is described as "A string field" 255 | 256 | JSON tag and path templates 257 | 258 | You can use JSON tags to shape your service method's response (the output). 259 | 260 | Endpoints will honor Go's encoding/json marshaling rules 261 | (http://golang.org/pkg/encoding/json/#Marshal), which means having this struct: 262 | 263 | type TaggedStruct struct { 264 | A int 265 | B int `json:"myB"` 266 | C string `json:"c"` 267 | Skipped int `json:"-"` 268 | } 269 | 270 | a service method path template could then look like: 271 | 272 | some/path/{A}/other/{c}/{myB} 273 | 274 | Notice, the names are case-sensitive. 275 | 276 | Naturally, you can combine json and endpoints tags to use a struct for both 277 | input and output: 278 | 279 | type TaggedStruct struct { 280 | A int `endpoints:"req,min=0,max=100,desc=An int field"` 281 | B int `json:"myB" endpoints:"d=10,min=1,max=200"` 282 | C string `json:"c" endpoints:"req,d=Hello gopher,desc=A string field"` 283 | Skipped int `json:"-"` 284 | } 285 | 286 | Long integers (int64, uint64) 287 | 288 | As per Type and Format Summary (https://developers.google.com/discovery/v1/type-format): 289 | 290 | a 64-bit integer cannot be represented in JSON (since JavaScript and JSON 291 | support integers up to 2^53). Therefore, a 64-bit integer must be 292 | represented as a string in JSON requests/responses 293 | 294 | In this case, it is sufficient to append ",string" to the json tag: 295 | 296 | type Int64Struct struct { 297 | Id int64 `json:",string"` 298 | } 299 | 300 | 301 | Generate client libraries 302 | 303 | Once an app is deployed on appspot.com, we can use the discovery doc to generate 304 | libraries for different clients. 305 | 306 | Android 307 | 308 | $ URL='https://my-app-id.appspot.com/_ah/api/discovery/v1/apis/greeting/v1/rest' 309 | $ curl -s $URL > greetings.rest.discovery 310 | 311 | # Optionally check the discovery doc 312 | $ less greetings.rest.discovery 313 | 314 | $ GO_SDK/endpointscfg.py gen_client_lib java greetings.rest.discovery 315 | 316 | You should be able to find ./greetings.rest.zip file with Java client source 317 | code and its dependencies. 318 | 319 | Once you have that, follow the official guide 320 | https://developers.google.com/appengine/docs/python/endpoints/consume_android. 321 | 322 | iOS 323 | 324 | # Note the rpc suffix in the URL: 325 | $ URL='https://my-app-id.appspot.com/_ah/api/discovery/v1/apis/greeting/v1/rpc' 326 | $ curl -s $URL > greetings.rpc.discovery 327 | 328 | # optionally check the discovery doc 329 | $ less greetings.rpc.discovery 330 | 331 | Then, feed greetings.rpc.discovery file to the library generator on OS X 332 | as described in the official guide: 333 | https://developers.google.com/appengine/docs/python/endpoints/consume_ios 334 | 335 | JavaScript 336 | 337 | There's really nothing to generate for JavaScript, you just use it! 338 | 339 | Here's the official guide: 340 | https://developers.google.com/appengine/docs/python/endpoints/consume_js 341 | 342 | 343 | Other docs 344 | 345 | Wiki pages on the github repo: 346 | https://github.com/crhym3/go-endpoints/wiki 347 | 348 | 349 | Samples 350 | 351 | Check out TicTacToe sample: 352 | https://github.com/crhym3/go-tictactoe 353 | 354 | Or play it on the live demo app at https://go-endpoints.appspot.com/tictactoe 355 | 356 | 357 | Running tests 358 | 359 | We currently use aet tool (https://github.com/crhym3/aegot) to simplify running 360 | tests on files that have "appengine" or "appengine_internal" imports. 361 | 362 | Check out the readme of that tool but, assuming you cloned this repo 363 | (so you can reach ./endpoints dir), the initial setup process is pretty simple: 364 | 365 | - go get github.com/crhym3/aegot/aet 366 | - aet init ./endpoints 367 | 368 | That's it. You should be able to run tests with "aet test ./endpoints" now. 369 | */ 370 | package endpoints 371 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /endpoints/auth_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "math/big" 8 | "net/http" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "golang.org/x/net/context" 15 | "google.golang.org/appengine" 16 | "google.golang.org/appengine/memcache" 17 | 18 | "appengine/aetest" 19 | ) 20 | 21 | func TestParseToken(t *testing.T) { 22 | tts := []struct { 23 | header, value, want string 24 | }{ 25 | {"Authorization", "Bearer token", "token"}, 26 | {"Authorization", "bearer foo", "foo"}, 27 | {"authorization", "bearer bar", "bar"}, 28 | {"Authorization", "OAuth baz", "baz"}, 29 | {"authorization", "oauth xxx", "xxx"}, 30 | {"Authorization", "Bearer", ""}, 31 | {"authorization", "Bearer ", ""}, 32 | {"Authorization", "", ""}, 33 | {"X-Other-Header", "Bearer token", ""}, 34 | {"x-header", "Bearer token", ""}, 35 | {"", "", ""}, 36 | } 37 | for i, tt := range tts { 38 | h := make(http.Header) 39 | if tt.header != "" { 40 | h.Set(tt.header, tt.value) 41 | } 42 | r := &http.Request{Header: h} 43 | 44 | out := parseToken(r) 45 | if out != tt.want { 46 | t.Errorf("%d: parseToken(%v) = %q; want %q", i, h, out, tt.want) 47 | } 48 | } 49 | } 50 | 51 | func TestMaxAge(t *testing.T) { 52 | verifyPairs(t, 53 | maxAge("max-age=86400"), 86400, 54 | maxAge("max-age = 7200, must-revalidate"), 7200, 55 | maxAge("public, max-age= 3600"), 3600, 56 | maxAge("max-age=-100"), 0, 57 | maxAge("max-age = 0a1b, must-revalidate"), 0, 58 | maxAge("public, max-age= short"), 0, 59 | maxAge("s-maxage=86400"), 0, 60 | maxAge("max=86400"), 0, 61 | maxAge("public"), 0, 62 | maxAge(""), 0, 63 | ) 64 | } 65 | 66 | func TestCertExpirationTime(t *testing.T) { 67 | tts := []struct { 68 | cacheControl, age string 69 | want time.Duration 70 | }{ 71 | {"max-age=3600", "600", 3000 * time.Second}, 72 | {"max-age=600", "", 0}, 73 | {"max-age=300", "301", 0}, 74 | {"max-age=0", "0", 0}, 75 | {"", "600", 0}, 76 | {"", "", 0}, 77 | } 78 | 79 | for i, tt := range tts { 80 | h := make(http.Header) 81 | h.Set("cache-control", tt.cacheControl) 82 | h.Set("age", tt.age) 83 | 84 | if out := certExpirationTime(h); out != tt.want { 85 | t.Errorf("%d: certExpirationTime(%v) = %d; want %d", i, h, out, tt.want) 86 | } 87 | } 88 | } 89 | 90 | func TestAddBase64Pad(t *testing.T) { 91 | verifyPairs(t, 92 | addBase64Pad("12"), "12==", 93 | addBase64Pad("123"), "123=", 94 | addBase64Pad("1234"), "1234", 95 | addBase64Pad("12345"), "12345", 96 | addBase64Pad(""), "") 97 | } 98 | 99 | func TestBase64ToBig(t *testing.T) { 100 | tts := []struct { 101 | in string 102 | want *big.Int 103 | error bool 104 | }{ 105 | {"MTI=", new(big.Int).SetBytes([]byte("12")), false}, 106 | {"MTI", new(big.Int).SetBytes([]byte("12")), false}, 107 | {"", big.NewInt(0), false}, 108 | {" ", nil, true}, 109 | } 110 | 111 | for i, tt := range tts { 112 | out, err := base64ToBig(tt.in) 113 | switch { 114 | case err == nil && !tt.error && tt.want.Cmp(out) != 0: 115 | t.Errorf("%d: base64ToBig(%q) = %v; want %v", i, tt.in, out, tt.want) 116 | case err != nil && !tt.error: 117 | t.Errorf("%d: base64ToBig(%q) = %v; want %v", i, tt.in, err, tt.want) 118 | case err == nil && tt.error: 119 | t.Errorf("%d: base64ToBig(%q) = %v; want error", i, tt.in, out) 120 | } 121 | } 122 | } 123 | 124 | func TestZeroPad(t *testing.T) { 125 | in := []byte{1, 2, 3} 126 | padded := zeroPad(in, 5) 127 | want := []byte{0, 0, 1, 2, 3} 128 | if !bytes.Equal(padded, want) { 129 | t.Errorf("zeroPad(%#v, 5) = %#v; want %#v", in, padded, want) 130 | } 131 | } 132 | 133 | func TestContains(t *testing.T) { 134 | tts := []struct { 135 | list []string 136 | val string 137 | want bool 138 | }{ 139 | {[]string{"test"}, "test", true}, 140 | {[]string{"one", "test", "two"}, "test", true}, 141 | {[]string{"test"}, "xxx", false}, 142 | {[]string{"xxx"}, "test", false}, 143 | {[]string{}, "", false}, 144 | } 145 | 146 | for i, tt := range tts { 147 | res := contains(tt.list, tt.val) 148 | if res != tt.want { 149 | t.Errorf("%d: contains(%#v, %q) = %v; want %v", 150 | i, tt.list, tt.val, res, tt.want) 151 | } 152 | } 153 | } 154 | 155 | func TestCachedCertsCacheHit(t *testing.T) { 156 | origTransport := httpTransportFactory 157 | defer func() { httpTransportFactory = origTransport }() 158 | httpTransportFactory = func(c context.Context) http.RoundTripper { 159 | return newTestRoundTripper() 160 | } 161 | 162 | req, _, closer := newTestRequest(t, "GET", "/", nil) 163 | defer closer() 164 | nc, err := appengine.Namespace(appengine.NewContext(req), certNamespace) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | tts := []struct { 170 | cacheValue string 171 | want *certsList 172 | }{ 173 | {"", nil}, 174 | {"{}", &certsList{}}, 175 | {`{"keyvalues": [{}]}`, &certsList{[]*certInfo{{}}}}, 176 | {`{"keyvalues": [ 177 | {"algorithm": "RS256", 178 | "exponent": "123", 179 | "keyid": "some-id", 180 | "modulus": "123"} ]}`, 181 | &certsList{[]*certInfo{{"RS256", "123", "some-id", "123"}}}}, 182 | } 183 | ec := NewContext(req) 184 | for i, tt := range tts { 185 | item := &memcache.Item{Key: DefaultCertURI, Value: []byte(tt.cacheValue)} 186 | if err := memcache.Set(nc, item); err != nil { 187 | t.Fatal(err) 188 | } 189 | out, err := cachedCerts(ec) 190 | switch { 191 | case err != nil && tt.want != nil: 192 | t.Errorf("%d: cachedCerts() error %v", i, err) 193 | case err == nil && tt.want == nil: 194 | t.Errorf("%d: cachedCerts() = %#v; want error", i, out) 195 | case err == nil && tt.want != nil && !reflect.DeepEqual(out, tt.want): 196 | t.Errorf("cachedCerts() = %#+v (%T); want %#+v (%T)", 197 | out, out, tt.want, tt.want) 198 | } 199 | } 200 | } 201 | 202 | func TestCachedCertsCacheMiss(t *testing.T) { 203 | rt := newTestRoundTripper() 204 | origTransport := httpTransportFactory 205 | defer func() { httpTransportFactory = origTransport }() 206 | httpTransportFactory = func(c context.Context) http.RoundTripper { 207 | return rt 208 | } 209 | 210 | req, _, closer := newTestRequest(t, "GET", "/", nil) 211 | defer closer() 212 | nc, err := appengine.Namespace(appengine.NewContext(req), certNamespace) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | ec := NewContext(req) 217 | 218 | tts := []*struct { 219 | respStatus int 220 | respContent, cacheControl, age string 221 | want *certsList 222 | shouldCache bool 223 | }{ 224 | {200, `{"keyvalues":null}`, "max-age=3600", "600", &certsList{}, true}, 225 | {-1, "", "", "", nil, false}, 226 | {400, "", "", "", nil, false}, 227 | {200, `{"keyvalues":null}`, "", "", &certsList{}, false}, 228 | } 229 | 230 | for i, tt := range tts { 231 | if tt.respStatus > 0 { 232 | resp := &http.Response{ 233 | Status: fmt.Sprintf("%d", tt.respStatus), 234 | StatusCode: tt.respStatus, 235 | Body: ioutil.NopCloser(strings.NewReader(tt.respContent)), 236 | Header: make(http.Header), 237 | } 238 | resp.Header.Set("cache-control", tt.cacheControl) 239 | resp.Header.Set("age", tt.age) 240 | rt.Add(resp) 241 | } 242 | memcache.Delete(nc, DefaultCertURI) 243 | 244 | out, err := cachedCerts(ec) 245 | switch { 246 | case err != nil && tt.want != nil: 247 | t.Errorf("%d: cachedCerts() = %v", i, err) 248 | case err == nil && tt.want == nil: 249 | t.Errorf("%d: cachedCerts() = %#v; want error", i, out) 250 | default: 251 | if !reflect.DeepEqual(out, tt.want) { 252 | t.Errorf("%d: cachedCerts() = %#v; want %#v", i, out, tt.want) 253 | } 254 | if !tt.shouldCache { 255 | continue 256 | } 257 | item, err := memcache.Get(nc, DefaultCertURI) 258 | if err != nil { 259 | t.Errorf("%d: memcache.Get(%q) = %v", i, DefaultCertURI, err) 260 | continue 261 | } 262 | cert := string(item.Value) 263 | if tt.respContent != cert { 264 | t.Errorf("%d: memcache.Get(%q) = %q; want %q", 265 | i, DefaultCertURI, cert, tt.respContent) 266 | } 267 | } 268 | } 269 | } 270 | 271 | func TestCurrentBearerTokenUser(t *testing.T) { 272 | var empty = []string{} 273 | const ( 274 | // Default values from user_service_stub.py of dev_appserver2. 275 | validScope = "valid.scope" 276 | validClientID = "123456789.apps.googleusercontent.com" 277 | email = "example@example.com" 278 | userID = "0" 279 | authDomain = "gmail.com" 280 | isAdmin = false 281 | ) 282 | 283 | inst, err := aetest.NewInstance(nil) 284 | if err != nil { 285 | t.Fatalf("failed to create instance: %v", err) 286 | } 287 | defer inst.Close() 288 | 289 | tts := []*struct { 290 | scopes []string 291 | clientIDs []string 292 | success bool 293 | }{ 294 | {empty, empty, false}, 295 | {empty, []string{validClientID}, false}, 296 | {[]string{validScope}, empty, false}, 297 | {[]string{validScope}, []string{validClientID}, true}, 298 | {[]string{"a", validScope, "b"}, []string{"c", validClientID, "d"}, true}, 299 | } 300 | for i, tt := range tts { 301 | r, err := inst.NewRequest("GET", "/", nil) 302 | if err != nil { 303 | t.Fatalf("failed to create req: %v", err) 304 | } 305 | c := newContext(r, cachingAuthenticatorFactory) 306 | user, err := CurrentBearerTokenUser(c, tt.scopes, tt.clientIDs) 307 | switch { 308 | case tt.success && (err != nil || user == nil): 309 | t.Errorf("%d: CurrentBearerTokenUser(%v, %v): err=%v, user=%+v; want ok", 310 | i, tt.scopes, tt.clientIDs, err, user) 311 | case !tt.success && err == nil: 312 | t.Errorf("%d: CurrentBearerTokenUser(%v, %v) = %+v; want error", 313 | i, tt.scopes, tt.clientIDs, user) 314 | } 315 | } 316 | 317 | r, err := inst.NewRequest("GET", "/", nil) 318 | if err != nil { 319 | t.Fatalf("failed to create req: %v", err) 320 | } 321 | c := newContext(r, cachingAuthenticatorFactory) 322 | 323 | scopes := []string{validScope} 324 | clientIDs := []string{validClientID} 325 | user, err := CurrentBearerTokenUser(c, scopes, clientIDs) 326 | if err != nil { 327 | t.Fatalf("CurrentBearerTokenUser(%v, %v) = %v", scopes, clientIDs, err) 328 | } 329 | 330 | if user.ID != userID { 331 | t.Fatalf("CurrentBearerTokenUser(%v, %v) = %v; want ID=%v", 332 | scopes, clientIDs, user, userID) 333 | } 334 | if user.Email != email { 335 | t.Fatalf("CurrentBearerTokenUser(%v, %v) = %v; want email=%v", 336 | scopes, clientIDs, user, email) 337 | } 338 | if user.AuthDomain != authDomain { 339 | t.Fatalf("CurrentBearerTokenUser(%v, %v) = %v; want authDomain=%v", 340 | scopes, clientIDs, user, authDomain) 341 | } 342 | if user.Admin != isAdmin { 343 | t.Fatalf("CurrentBearerTokenUser(%v, %v) = %v; want isAdmin=%v", 344 | scopes, clientIDs, user, isAdmin) 345 | } 346 | } 347 | 348 | func TestCurrentUser(t *testing.T) { 349 | const ( 350 | // Default values from user_service_stub.py of dev_appserver2. 351 | clientID = "123456789.apps.googleusercontent.com" 352 | bearerEmail = "example@example.com" 353 | validScope = "valid.scope" 354 | ) 355 | 356 | inst, err := aetest.NewInstance(nil) 357 | if err != nil { 358 | t.Fatalf("failed to create instance: %v", err) 359 | } 360 | defer inst.Close() 361 | 362 | req, err := inst.NewRequest("GET", "/", nil) 363 | nc, err := appengine.Namespace(appengine.NewContext(req), certNamespace) 364 | if err != nil { 365 | t.Fatal(err) 366 | } 367 | // googCerts are provided in jwt_test.go 368 | item := &memcache.Item{Key: DefaultCertURI, Value: []byte(googCerts)} 369 | if err := memcache.Set(nc, item); err != nil { 370 | t.Fatal(err) 371 | } 372 | 373 | origCurrentUTC := currentUTC 374 | defer func() { currentUTC = origCurrentUTC }() 375 | currentUTC = func() time.Time { 376 | return jwtValidTokenTime 377 | } 378 | 379 | jwtStr, jwt := jwtValidTokenString, jwtValidTokenObject 380 | tts := []struct { 381 | token string 382 | scopes, audiences, clientIDs []string 383 | wantEmail string 384 | }{ 385 | // success 386 | {jwtStr, []string{EmailScope}, []string{jwt.Audience}, []string{jwt.ClientID}, jwt.Email}, 387 | {"ya29.token", []string{EmailScope}, []string{clientID}, []string{clientID}, bearerEmail}, 388 | {"ya29.token", []string{EmailScope, validScope}, []string{clientID}, []string{clientID}, bearerEmail}, 389 | {"1/token", []string{validScope}, []string{clientID}, []string{clientID}, bearerEmail}, 390 | 391 | // failure 392 | {jwtStr, []string{EmailScope}, []string{"other-client"}, []string{"other-client"}, ""}, 393 | {"some.invalid.jwt", []string{EmailScope}, []string{jwt.Audience}, []string{jwt.ClientID}, ""}, 394 | {"", []string{validScope}, []string{clientID}, []string{clientID}, ""}, 395 | // The following test is commented for now because default implementation 396 | // of UserServiceStub in dev_appserver2 allows any scope. 397 | // TODO: figure out how to test this. 398 | //{"ya29.invalid", []string{"invalid.scope"}, []string{clientID}, []string{clientID}, ""}, 399 | 400 | {"doesn't matter", nil, []string{clientID}, []string{clientID}, ""}, 401 | {"doesn't matter", []string{EmailScope}, nil, []string{clientID}, ""}, 402 | {"doesn't matter", []string{EmailScope}, []string{clientID}, nil, ""}, 403 | } 404 | 405 | for i, tt := range tts { 406 | r, err := inst.NewRequest("GET", "/", nil) 407 | c := newContext(r, cachingAuthenticatorFactory) 408 | if tt.token != "" { 409 | r.Header.Set("authorization", "oauth "+tt.token) 410 | } 411 | 412 | user, err := CurrentUser(c, tt.scopes, tt.audiences, tt.clientIDs) 413 | 414 | switch { 415 | case tt.wantEmail == "" && err == nil: 416 | t.Errorf("%d: CurrentUser(%v, %v, %v) = %v; want error", 417 | i, tt.scopes, tt.audiences, tt.clientIDs, user) 418 | case tt.wantEmail != "" && user == nil: 419 | t.Errorf("%d: CurrentUser(%v, %v, %v) = %v; want email = %q", 420 | i, tt.scopes, tt.audiences, tt.clientIDs, err, tt.wantEmail) 421 | case tt.wantEmail != "" && tt.wantEmail != user.Email: 422 | t.Errorf("%d: CurrentUser(%v, %v, %v) = %v; want email = %q", 423 | i, tt.scopes, tt.audiences, tt.clientIDs, user, tt.wantEmail) 424 | } 425 | } 426 | } 427 | 428 | func newContext(r *http.Request, factory func() Authenticator) context.Context { 429 | defer func(old func() Authenticator) { 430 | AuthenticatorFactory = old 431 | }(AuthenticatorFactory) 432 | AuthenticatorFactory = factory 433 | return NewContext(r) 434 | } 435 | -------------------------------------------------------------------------------- /endpoints/server_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | 15 | "golang.org/x/net/context" 16 | 17 | "appengine/aetest" 18 | ) 19 | 20 | type TestMsg struct { 21 | Name string `json:"name"` 22 | } 23 | 24 | type BytesMsg struct { 25 | Bytes []byte 26 | } 27 | 28 | type ServerTestService struct{} 29 | 30 | func (s *ServerTestService) Void(r *http.Request, _, _ *VoidMessage) error { 31 | return nil 32 | } 33 | 34 | func (s *ServerTestService) Error(r *http.Request, _, _ *VoidMessage) error { 35 | return errors.New("Dummy error") 36 | } 37 | 38 | func (s *ServerTestService) Msg(r *http.Request, req, resp *TestMsg) error { 39 | resp.Name = req.Name 40 | return nil 41 | } 42 | 43 | func (s *ServerTestService) CustomAPIError(r *http.Request, req, resp *TestMsg) error { 44 | return NewAPIError("MethodNotAllowed", "MethodNotAllowed", http.StatusMethodNotAllowed) 45 | } 46 | 47 | func (s *ServerTestService) InternalServer(r *http.Request, req, resp *TestMsg) error { 48 | return InternalServerError 49 | } 50 | 51 | func (s *ServerTestService) BadRequest(r *http.Request, req, resp *TestMsg) error { 52 | return BadRequestError 53 | } 54 | 55 | func (s *ServerTestService) NotFound(r *http.Request, req, resp *TestMsg) error { 56 | return NotFoundError 57 | } 58 | 59 | func (s *ServerTestService) Forbidden(r *http.Request, req, resp *TestMsg) error { 60 | return ForbiddenError 61 | } 62 | 63 | func (s *ServerTestService) Unauthorized(r *http.Request, req, resp *TestMsg) error { 64 | return UnauthorizedError 65 | } 66 | 67 | func (s *ServerTestService) Conflict(r *http.Request, req, resp *TestMsg) error { 68 | return ConflictError 69 | } 70 | 71 | type RequiredMsg struct { 72 | Name string `endpoints:"req"` 73 | } 74 | 75 | func (s *ServerTestService) TestRequired(r *http.Request, req *RequiredMsg) error { 76 | return nil 77 | } 78 | 79 | type DefaultMsg struct { 80 | Name string `endpoints:"d=gopher"` 81 | Age int `endpoints:"d=10"` 82 | Weight float64 `endpoints:"d=0.5"` 83 | } 84 | 85 | func (s *ServerTestService) TestDefault(r *http.Request, req *DefaultMsg) error { 86 | var sent *DefaultMsg 87 | if err := json.NewDecoder(r.Body).Decode(&sent); err != nil { 88 | return fmt.Errorf("decoding original message: %v", err) 89 | } 90 | 91 | // check that rcv is a good value given sent and default values. 92 | check := func(sent, z, rcv, def interface{}) bool { 93 | return (sent == z && rcv == def) || (sent != z && sent == rcv) 94 | } 95 | if !check(sent.Name, "", req.Name, "gopher") { 96 | return fmt.Errorf("wrong name: %q", req.Name) 97 | } 98 | if !check(sent.Age, 0, req.Age, 10) { 99 | return fmt.Errorf("wrong age: %v", req.Age) 100 | } 101 | if !check(sent.Weight, 0.0, req.Weight, 0.5) { 102 | return fmt.Errorf("wrong weight: %v", req.Weight) 103 | } 104 | return nil 105 | } 106 | 107 | type SliceMsg struct { 108 | Strings []string 109 | Ints []int 110 | Bytes []byte 111 | Bools []bool 112 | } 113 | 114 | func (s *ServerTestService) TestSliceMsg(r *http.Request, req *SliceMsg) error { 115 | return nil 116 | } 117 | 118 | type MinMaxMsg struct { 119 | Age int32 `endpoints:"min=0,max=100"` 120 | Weight float32 `endpoints:"min=3.14,max=31.4"` 121 | Grade string `endpoints:"min=A,max=F"` 122 | } 123 | 124 | func (s *ServerTestService) TestMinMax(r *http.Request, req *MinMaxMsg) error { 125 | return nil 126 | } 127 | 128 | // Service methods for args testing 129 | 130 | func (s *ServerTestService) MsgWithRequest(r *http.Request, req, resp *TestMsg) error { 131 | if r == nil { 132 | return errors.New("MsgWithRequest: r = nil") 133 | } 134 | resp.Name = req.Name 135 | return nil 136 | } 137 | 138 | func (s *ServerTestService) MsgWithContext(c context.Context, req, resp *TestMsg) error { 139 | if c == nil { 140 | return errors.New("MsgWithContext: c = nil") 141 | } 142 | resp.Name = req.Name 143 | return nil 144 | } 145 | 146 | func (s *ServerTestService) MsgWithReturn(c context.Context, req *TestMsg) (*TestMsg, error) { 147 | if c == nil { 148 | return nil, errors.New("MsgWithReturn: c = nil") 149 | } 150 | return &TestMsg{req.Name}, nil 151 | } 152 | 153 | func (s *ServerTestService) MsgWithoutRequest(c context.Context) (*TestMsg, error) { 154 | if c == nil { 155 | return nil, errors.New("MsgWithoutRequest: c = nil") 156 | } 157 | return &TestMsg{}, nil 158 | } 159 | 160 | func (s *ServerTestService) MsgWithoutResponse(c context.Context, req *TestMsg) error { 161 | if c == nil { 162 | return errors.New("MsgWithoutResponse: c = nil") 163 | } 164 | return nil 165 | } 166 | 167 | func (s *ServerTestService) MsgWithoutRequestNorResponse(c context.Context) error { 168 | if c == nil { 169 | return errors.New("MsgWithoutRequestNorResponse: c = nil") 170 | } 171 | return nil 172 | } 173 | 174 | func (s *ServerTestService) EchoRequest(r *http.Request, req *TestMsg) (*BytesMsg, error) { 175 | b, err := ioutil.ReadAll(r.Body) 176 | r.Body.Close() 177 | if err != nil { 178 | return nil, err 179 | } 180 | return &BytesMsg{b}, nil 181 | } 182 | 183 | func createAPIServer() *Server { 184 | s := &ServerTestService{} 185 | rpc := &RPCService{ 186 | name: "ServerTestService", 187 | rcvr: reflect.ValueOf(s), 188 | rcvrType: reflect.TypeOf(s), 189 | methods: make(map[string]*ServiceMethod), 190 | } 191 | for i := 0; i < rpc.rcvrType.NumMethod(); i++ { 192 | m := rpc.rcvrType.Method(i) 193 | sm := &ServiceMethod{ 194 | method: &m, 195 | wantsContext: m.Type.In(1).Implements(typeOfContext), 196 | } 197 | if m.Type.NumIn() > 2 { 198 | sm.ReqType = m.Type.In(2).Elem() 199 | } else { 200 | sm.ReqType = typeOfVoidMessage.Elem() 201 | } 202 | if m.Type.NumOut() == 2 { 203 | sm.RespType = m.Type.Out(0).Elem() 204 | } else if m.Type.NumIn() > 3 { 205 | sm.RespType = m.Type.In(3).Elem() 206 | } 207 | rpc.methods[m.Name] = sm 208 | } 209 | 210 | smap := &serviceMap{services: make(map[string]*RPCService)} 211 | smap.services[rpc.name] = rpc 212 | return &Server{root: "/_ah/spi", services: smap} 213 | } 214 | 215 | func TestServerServeHTTP(t *testing.T) { 216 | server := createAPIServer() 217 | inst, err := aetest.NewInstance(nil) 218 | if err != nil { 219 | t.Fatalf("failed to create instance: %v", err) 220 | } 221 | defer inst.Close() 222 | 223 | tts := []struct { 224 | httpVerb string 225 | srvMethod, in, out string 226 | code int 227 | }{ 228 | 229 | {"POST", "Void", `{}`, `{}`, http.StatusOK}, 230 | {"POST", "Msg", `{"name":"alex"}`, `{"name":"alex"}`, http.StatusOK}, 231 | 232 | {"POST", "Error", `{}`, ``, http.StatusBadRequest}, 233 | {"POST", "Msg", ``, ``, http.StatusBadRequest}, 234 | {"POST", "DoesNotExist", `{}`, ``, http.StatusBadRequest}, 235 | 236 | {"POST", "InternalServer", `{}`, ``, http.StatusInternalServerError}, 237 | {"POST", "BadRequest", `{}`, ``, http.StatusBadRequest}, 238 | {"POST", "NotFound", `{}`, ``, http.StatusNotFound}, 239 | {"POST", "Forbidden", `{}`, ``, http.StatusForbidden}, 240 | {"POST", "Unauthorized", `{}`, ``, http.StatusUnauthorized}, 241 | {"POST", "CustomAPIError", `{}`, ``, http.StatusMethodNotAllowed}, 242 | 243 | {"GET", "Void", `{}`, ``, http.StatusBadRequest}, 244 | {"PUT", "Void", `{}`, ``, http.StatusBadRequest}, 245 | {"HEAD", "Void", `{}`, ``, http.StatusBadRequest}, 246 | {"DELETE", "Void", `{}`, ``, http.StatusBadRequest}, 247 | 248 | {"POST", "TestRequired", `{}`, ``, http.StatusBadRequest}, 249 | {"POST", "TestRequired", `{"name":"francesc"}`, ``, http.StatusOK}, 250 | {"POST", "TestDefault", `{}`, ``, http.StatusOK}, 251 | {"POST", "TestDefault", `{"name":"francesc"}`, ``, http.StatusOK}, 252 | {"POST", "TestDefault", `{"age": 20}`, ``, http.StatusOK}, 253 | {"POST", "TestDefault", `{"weight": 3.14}`, ``, http.StatusOK}, 254 | {"POST", "TestDefault", `{"name":"francesc", "age": 20}`, ``, http.StatusOK}, 255 | 256 | {"POST", "TestSliceMsg", `{}`, ``, http.StatusOK}, 257 | {"POST", "TestSliceMsg", `{"strings":["a", "b"]}`, ``, http.StatusOK}, 258 | {"POST", "TestSliceMsg", `{"ints":[1, 2]}`, ``, http.StatusOK}, 259 | {"POST", "TestSliceMsg", `{"bytes":[0, 1]}`, ``, http.StatusOK}, 260 | {"POST", "TestSliceMsg", `{"bools":[true, false]}`, ``, http.StatusOK}, 261 | 262 | {"POST", "TestMinMax", `{"age":10,"weight":5,"grade":"C"}`, ``, http.StatusOK}, 263 | {"POST", "TestMinMax", `{"age":123,"weight":5,"grade":"C"}`, ``, http.StatusBadRequest}, 264 | {"POST", "TestMinMax", `{"age":10,"weight":1,"grade":"C"}`, ``, http.StatusBadRequest}, 265 | {"POST", "TestMinMax", `{"age":10,"weight":5,"grade":"G"}`, ``, http.StatusBadRequest}, 266 | 267 | {"POST", "MsgWithoutRequest", `{}`, `{"name":""}`, http.StatusOK}, 268 | {"POST", "MsgWithoutResponse", `{}`, ``, http.StatusOK}, 269 | {"POST", "MsgWithoutRequestNorResponse", `{}`, ``, http.StatusOK}, 270 | } 271 | 272 | for i, tt := range tts { 273 | path := "/ServerTestService." + tt.srvMethod 274 | var body io.Reader 275 | if tt.httpVerb == "POST" || tt.httpVerb == "PUT" { 276 | body = strings.NewReader(tt.in) 277 | } 278 | var r *http.Request 279 | if r, err = inst.NewRequest(tt.httpVerb, path, body); err != nil { 280 | t.Fatalf("failed to create req: %v", r) 281 | } 282 | 283 | w := httptest.NewRecorder() 284 | 285 | // do the fake request 286 | server.ServeHTTP(w, r) 287 | 288 | // make sure the response is correct 289 | out := strings.TrimSpace(w.Body.String()) 290 | if tt.code == http.StatusOK && out != tt.out { 291 | t.Errorf("%d: %s %s = %q; want %q", i, tt.httpVerb, path, out, tt.out) 292 | } 293 | if w.Code != tt.code { 294 | t.Errorf("%d: %s %s w.Code = %d; want %d", 295 | i, tt.httpVerb, path, w.Code, tt.code) 296 | } 297 | } 298 | } 299 | 300 | func TestServerMethodCall(t *testing.T) { 301 | server := createAPIServer() 302 | inst, err := aetest.NewInstance(nil) 303 | if err != nil { 304 | t.Fatalf("failed to create instance: %v", err) 305 | } 306 | defer inst.Close() 307 | 308 | tts := []struct { 309 | name, body string 310 | }{ 311 | {"MsgWithRequest", `{"name":"request"}`}, 312 | {"MsgWithContext", `{"name":"context"}`}, 313 | {"MsgWithReturn", `{"name":"return"}`}, 314 | } 315 | 316 | for i, tt := range tts { 317 | path := "/ServerTestService." + tt.name 318 | body := strings.NewReader(tt.body) 319 | r, err := inst.NewRequest("POST", path, body) 320 | if err != nil { 321 | t.Fatalf("%d: failed to create req: %v", t, err) 322 | } 323 | 324 | w := httptest.NewRecorder() 325 | server.ServeHTTP(w, r) 326 | 327 | res := strings.TrimSpace(w.Body.String()) 328 | if res != tt.body { 329 | t.Errorf("%d: %s res = %q; want %q", i, tt.name, res, tt.body) 330 | } 331 | if w.Code != http.StatusOK { 332 | t.Errorf("%d: %s code = %d; want %d", i, tt.name, w.Code, http.StatusOK) 333 | } 334 | } 335 | } 336 | 337 | func TestServerRegisterService(t *testing.T) { 338 | s, err := NewServer(""). 339 | RegisterService(&ServerTestService{}, "ServerTestService", "v1", "", true) 340 | if err != nil { 341 | t.Fatalf("error registering service: %v", err) 342 | } 343 | 344 | tts := []struct { 345 | name string 346 | wantsContext bool 347 | returnsResp bool 348 | }{ 349 | {"MsgWithRequest", false, false}, 350 | {"MsgWithContext", true, false}, 351 | {"MsgWithReturn", true, true}, 352 | } 353 | for i, tt := range tts { 354 | m := s.MethodByName(tt.name) 355 | if m == nil { 356 | t.Errorf("%d: MethodByName(%q) = nil", i, tt.name) 357 | continue 358 | } 359 | if m.wantsContext != tt.wantsContext { 360 | t.Errorf("%d: wantsContext = %v; want %v", i, m.wantsContext, tt.wantsContext) 361 | } 362 | } 363 | } 364 | 365 | func TestServerMustRegisterService(t *testing.T) { 366 | s := NewServer("") 367 | 368 | var panicked interface{} 369 | func() { 370 | defer func() { panicked = recover() }() 371 | Must(s.RegisterService(&ServerTestService{}, "ServerTestService", "v1", "", true)) 372 | }() 373 | if panicked != nil { 374 | t.Fatalf("unexpected panic: %v", panicked) 375 | } 376 | 377 | type badService struct{} 378 | func() { 379 | defer func() { panicked = recover() }() 380 | Must(s.RegisterService(&badService{}, "BadService", "v1", "", true)) 381 | }() 382 | if panicked == nil { 383 | t.Fatalf("expected panic didn't occur") 384 | } 385 | } 386 | 387 | func TestServerRequestNotEmpty(t *testing.T) { 388 | server := createAPIServer() 389 | inst, err := aetest.NewInstance(nil) 390 | if err != nil { 391 | t.Fatalf("failed to create instance: %v", err) 392 | } 393 | defer inst.Close() 394 | 395 | path := "/ServerTestService.EchoRequest" 396 | body := `{"name": "francesc"}` 397 | r, err := inst.NewRequest("POST", path, strings.NewReader(body)) 398 | if err != nil { 399 | t.Fatalf("failed to create req: %v", err) 400 | } 401 | 402 | w := httptest.NewRecorder() 403 | server.ServeHTTP(w, r) 404 | 405 | var res BytesMsg 406 | if err := json.NewDecoder(w.Body).Decode(&res); err != nil { 407 | t.Fatalf("decode response %q: %v", w.Body.String(), err) 408 | } 409 | 410 | if string(res.Bytes) != body { 411 | t.Fatalf("expected %q; got %q", body, res) 412 | } 413 | } 414 | 415 | const ( 416 | contextDecoratorKey = "context_decorator_key" 417 | contextDecoratorValue = "context_decorator_value" 418 | ) 419 | 420 | func (s *ServerTestService) ContextDecorator(ctx context.Context) (*VoidMessage, error) { 421 | fmt.Println("ContextDecorator called") 422 | if got := ctx.Value(contextDecoratorKey); got != contextDecoratorValue { 423 | return nil, NewBadRequestError("wrong context value: %q", got) 424 | } 425 | return &VoidMessage{}, nil 426 | } 427 | 428 | func TestContextDecorator(t *testing.T) { 429 | server := createAPIServer() 430 | inst, err := aetest.NewInstance(nil) 431 | if err != nil { 432 | t.Fatalf("failed to create instance: %v", err) 433 | } 434 | defer inst.Close() 435 | 436 | server.ContextDecorator = func(ctx context.Context) (context.Context, error) { 437 | return nil, ConflictError 438 | } 439 | path := "/ServerTestService.ContextDecorator" 440 | r, _ := inst.NewRequest("GET", path, strings.NewReader("")) 441 | w := httptest.NewRecorder() 442 | server.ServeHTTP(w, r) 443 | if w.Code != http.StatusConflict { 444 | t.Errorf("expected status code Conflict (409); got %v", w.Code) 445 | msg, _ := ioutil.ReadAll(w.Body) 446 | t.Errorf("response body: %s", msg) 447 | } 448 | 449 | server.ContextDecorator = func(ctx context.Context) (context.Context, error) { 450 | fmt.Println("context decorated") 451 | return context.WithValue(ctx, contextDecoratorKey, contextDecoratorValue), nil 452 | } 453 | 454 | r, _ = inst.NewRequest("POST", path, strings.NewReader("{}")) 455 | w = httptest.NewRecorder() 456 | server.ServeHTTP(w, r) 457 | if w.Code != http.StatusOK { 458 | t.Errorf("expected status OK (200); got %v", w.Code) 459 | msg, _ := ioutil.ReadAll(w.Body) 460 | t.Errorf("response body: %s", msg) 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /endpoints/auth.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "math/big" 12 | "net/http" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "golang.org/x/net/context" 19 | "google.golang.org/appengine" 20 | "google.golang.org/appengine/log" 21 | "google.golang.org/appengine/memcache" 22 | "google.golang.org/appengine/user" 23 | ) 24 | 25 | const ( 26 | // DefaultCertURI is Google's public URL which points to JWT certs. 27 | DefaultCertURI = ("https://www.googleapis.com/service_accounts/" + 28 | "v1/metadata/raw/federated-signon@system.gserviceaccount.com") 29 | // EmailScope is Google's OAuth 2.0 email scope 30 | EmailScope = "https://www.googleapis.com/auth/userinfo.email" 31 | // TokeninfoURL is Google's OAuth 2.0 access token verification URL 32 | TokeninfoURL = "https://www.googleapis.com/oauth2/v1/tokeninfo" 33 | // APIExplorerClientID is the client ID of API explorer. 34 | APIExplorerClientID = "292824132082.apps.googleusercontent.com" 35 | ) 36 | 37 | var ( 38 | allowedAuthSchemesUpper = [2]string{"OAUTH", "BEARER"} 39 | certNamespace = "__verify_jwt" 40 | clockSkewSecs = int64(300) // 5 minutes in seconds 41 | maxTokenLifetimeSecs = int64(86400) // 1 day in seconds 42 | maxAgePattern = regexp.MustCompile(`\s*max-age\s*=\s*(\d+)\s*`) 43 | 44 | // This is a variable on purpose: can be stubbed with a different (fake) 45 | // implementation during tests. 46 | // 47 | // endpoints package code should always call jwtParser() 48 | // instead of directly invoking verifySignedJWT(). 49 | jwtParser = verifySignedJWT 50 | 51 | // currentUTC returns current time in UTC. 52 | // This is a variable on purpose to be able to stub during testing. 53 | currentUTC = func() time.Time { 54 | return time.Now().UTC() 55 | } 56 | 57 | // AuthenticatorFactory creates a new Authenticator. 58 | // 59 | // It is a variable on purpose. You can set it to a stub implementation 60 | // in tests. 61 | AuthenticatorFactory func() Authenticator 62 | ) 63 | 64 | // An Authenticator can identify the current user. 65 | type Authenticator interface { 66 | // CurrentOAuthClientID returns a clientID associated with the scope. 67 | CurrentOAuthClientID(ctx context.Context, scope string) (string, error) 68 | 69 | // CurrentOAuthUser returns a user of this request for the given scope. 70 | // It caches OAuth info at the first call for future invocations. 71 | // 72 | // Returns an error if data for this scope is not available. 73 | CurrentOAuthUser(ctx context.Context, scope string) (*user.User, error) 74 | } 75 | 76 | // contextKey is used to store values on a context. 77 | type contextKey int 78 | 79 | // Context value keys. 80 | const ( 81 | invalidKey contextKey = iota 82 | requestKey 83 | authenticatorKey 84 | ) 85 | 86 | // HTTPRequest returns the request associated with a context. 87 | func HTTPRequest(c context.Context) *http.Request { 88 | r, _ := c.Value(requestKey).(*http.Request) 89 | return r 90 | } 91 | 92 | // authenticator returns the Authenticator associated with a 93 | // context, or nil if there is not one. 94 | func authenticator(c context.Context) Authenticator { 95 | a, _ := c.Value(authenticatorKey).(Authenticator) 96 | return a 97 | } 98 | 99 | // Errors for incorrect contexts. 100 | var ( 101 | errNoAuthenticator = errors.New("context has no authenticator (use endpoints.NewContext to create a context)") 102 | errNoRequest = errors.New("no request for context (use endpoints.NewContext to create a context)") 103 | ) 104 | 105 | // NewContext returns a new context for an in-flight API (HTTP) request. 106 | func NewContext(r *http.Request) context.Context { 107 | c := appengine.NewContext(r) 108 | c = context.WithValue(c, requestKey, r) 109 | c = context.WithValue(c, authenticatorKey, AuthenticatorFactory()) 110 | return c 111 | } 112 | 113 | // parseToken looks for Authorization header and returns a token. 114 | // 115 | // Returns empty string if req does not contain authorization header 116 | // or its value is not prefixed with allowedAuthSchemesUpper. 117 | func parseToken(r *http.Request) string { 118 | // TODO(dhermes): Allow a struct with access_token and bearer_token 119 | // fields here as well. 120 | pieces := strings.Fields(r.Header.Get("Authorization")) 121 | if len(pieces) != 2 { 122 | return "" 123 | } 124 | authHeaderSchemeUpper := strings.ToUpper(pieces[0]) 125 | for _, authScheme := range allowedAuthSchemesUpper { 126 | if authHeaderSchemeUpper == authScheme { 127 | return pieces[1] 128 | } 129 | } 130 | return "" 131 | } 132 | 133 | type certInfo struct { 134 | Algorithm string `json:"algorithm"` 135 | Exponent string `json:"exponent"` 136 | KeyID string `json:"keyid"` 137 | Modulus string `json:"modulus"` 138 | } 139 | 140 | type certsList struct { 141 | KeyValues []*certInfo `json:"keyvalues"` 142 | } 143 | 144 | // maxAge parses Cache-Control header value and extracts max-age (in seconds) 145 | func maxAge(s string) int { 146 | match := maxAgePattern.FindStringSubmatch(s) 147 | if len(match) != 2 { 148 | return 0 149 | } 150 | if maxAge, err := strconv.Atoi(match[1]); err == nil { 151 | return maxAge 152 | } 153 | return 0 154 | } 155 | 156 | // certExpirationTime computes a cert freshness based on Cache-Control 157 | // and Age headers of h. 158 | // 159 | // Returns 0 if one of the required headers is not present or cert lifetime 160 | // is expired. 161 | func certExpirationTime(h http.Header) time.Duration { 162 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 indicates only 163 | // a comma-separated header is valid, so it should be fine to split this on 164 | // commas. 165 | var max int 166 | for _, entry := range strings.Split(h.Get("Cache-Control"), ",") { 167 | max = maxAge(entry) 168 | if max > 0 { 169 | break 170 | } 171 | } 172 | if max <= 0 { 173 | return 0 174 | } 175 | 176 | age, err := strconv.Atoi(h.Get("Age")) 177 | if err != nil { 178 | return 0 179 | } 180 | 181 | remainingTime := max - age 182 | if remainingTime <= 0 { 183 | return 0 184 | } 185 | 186 | return time.Duration(remainingTime) * time.Second 187 | } 188 | 189 | // cachedCerts fetches public certificates info from DefaultCertURI and 190 | // caches it for the duration specified in Age header of a response. 191 | func cachedCerts(c context.Context) (*certsList, error) { 192 | namespacedContext, err := appengine.Namespace(c, certNamespace) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | var certs *certsList 198 | 199 | _, err = memcache.JSON.Get(namespacedContext, DefaultCertURI, &certs) 200 | if err == nil { 201 | return certs, nil 202 | } 203 | 204 | // Cache miss or server error. 205 | // If any error other than cache miss, it's proably not a good time 206 | // to use memcache. 207 | var cacheResults = err == memcache.ErrCacheMiss 208 | if !cacheResults { 209 | log.Debugf(c, "%s", err.Error()) 210 | } 211 | 212 | log.Debugf(c, "Fetching provider certs from: %s", DefaultCertURI) 213 | resp, err := newHTTPClient(c).Get(DefaultCertURI) 214 | if err != nil { 215 | return nil, err 216 | } 217 | defer resp.Body.Close() 218 | if resp.StatusCode != http.StatusOK { 219 | return nil, errors.New("Could not reach Cert URI or bad response.") 220 | } 221 | 222 | certBytes, err := ioutil.ReadAll(resp.Body) 223 | if err != nil { 224 | return nil, err 225 | } 226 | err = json.Unmarshal(certBytes, &certs) 227 | if err != nil { 228 | return nil, err 229 | } 230 | 231 | if cacheResults { 232 | expiration := certExpirationTime(resp.Header) 233 | if expiration > 0 { 234 | item := &memcache.Item{ 235 | Key: DefaultCertURI, 236 | Value: certBytes, 237 | Expiration: expiration, 238 | } 239 | err = memcache.Set(namespacedContext, item) 240 | if err != nil { 241 | log.Errorf(c, "Error adding Certs to memcache: %v", err) 242 | } 243 | } 244 | } 245 | return certs, nil 246 | } 247 | 248 | type signedJWTHeader struct { 249 | Algorithm string `json:"alg"` 250 | } 251 | 252 | type signedJWT struct { 253 | Audience string `json:"aud"` 254 | ClientID string `json:"azp"` 255 | Subject string `json:"sub"` 256 | Email string `json:"email"` 257 | Expires int64 `json:"exp"` 258 | IssuedAt int64 `json:"iat"` 259 | Issuer string `json:"iss"` 260 | } 261 | 262 | // addBase64Pad pads s to be a valid base64-encoded string. 263 | func addBase64Pad(s string) string { 264 | switch len(s) % 4 { 265 | case 2: 266 | s += "==" 267 | case 3: 268 | s += "=" 269 | } 270 | return s 271 | } 272 | 273 | // base64ToBig converts base64-encoded string to a big int. 274 | // Returns error if the encoding is invalid. 275 | func base64ToBig(s string) (*big.Int, error) { 276 | b, err := base64.StdEncoding.DecodeString(addBase64Pad(s)) 277 | if err != nil { 278 | return nil, err 279 | } 280 | z := big.NewInt(0) 281 | z.SetBytes(b) 282 | return z, nil 283 | } 284 | 285 | // zeroPad prepends 0s to b so that length of the returned slice is size. 286 | func zeroPad(b []byte, size int) []byte { 287 | padded := make([]byte, size-len(b), size) 288 | return append(padded, b...) 289 | } 290 | 291 | // contains returns true if value is one of the items of strList. 292 | func contains(strList []string, value string) bool { 293 | for _, choice := range strList { 294 | if choice == value { 295 | return true 296 | } 297 | } 298 | return false 299 | } 300 | 301 | // verifySignedJWT decodes and verifies JWT token string. 302 | // 303 | // Verification is based on 304 | // - a certificate exponent and modulus 305 | // - expiration and issue timestamps ("exp" and "iat" fields) 306 | // 307 | // This method expects JWT token string to be in the standard format, e.g. as 308 | // read from Authorization request header: "
..", 309 | // where all segments are encoded with URL-base64. 310 | // 311 | // The caller is responsible for performing further token verification. 312 | // (Issuer, Audience, ClientID, etc.) 313 | // 314 | // NOTE: do not call this function directly, use jwtParser() instead. 315 | func verifySignedJWT(c context.Context, jwt string, now int64) (*signedJWT, error) { 316 | segments := strings.Split(jwt, ".") 317 | if len(segments) != 3 { 318 | return nil, fmt.Errorf("Wrong number of segments in token: %s", jwt) 319 | } 320 | 321 | // Check that header (first segment) is valid 322 | headerBytes, err := base64.URLEncoding.DecodeString(addBase64Pad(segments[0])) 323 | if err != nil { 324 | return nil, err 325 | } 326 | var header signedJWTHeader 327 | err = json.Unmarshal(headerBytes, &header) 328 | if err != nil { 329 | return nil, err 330 | } 331 | if header.Algorithm != "RS256" { 332 | return nil, fmt.Errorf("Unexpected encryption algorithm: %s", header.Algorithm) 333 | } 334 | 335 | // Check that token (second segment) is valid 336 | tokenBytes, err := base64.URLEncoding.DecodeString(addBase64Pad(segments[1])) 337 | if err != nil { 338 | return nil, err 339 | } 340 | var token signedJWT 341 | err = json.Unmarshal(tokenBytes, &token) 342 | if err != nil { 343 | return nil, err 344 | } 345 | 346 | // Get current certs 347 | certs, err := cachedCerts(c) 348 | if err != nil { 349 | return nil, err 350 | } 351 | 352 | signatureBytes, err := base64.URLEncoding.DecodeString(addBase64Pad(segments[2])) 353 | if err != nil { 354 | return nil, err 355 | } 356 | signature := big.NewInt(0) 357 | signature.SetBytes(signatureBytes) 358 | 359 | signed := []byte(fmt.Sprintf("%s.%s", segments[0], segments[1])) 360 | h := sha256.New() 361 | h.Write(signed) 362 | signatureHash := h.Sum(nil) 363 | if len(signatureHash) < 32 { 364 | signatureHash = zeroPad(signatureHash, 32) 365 | } 366 | 367 | z := big.NewInt(0) 368 | verified := false 369 | for _, cert := range certs.KeyValues { 370 | exponent, err := base64ToBig(cert.Exponent) 371 | if err != nil { 372 | return nil, err 373 | } 374 | modulus, err := base64ToBig(cert.Modulus) 375 | if err != nil { 376 | return nil, err 377 | } 378 | signatureHashFromCert := z.Exp(signature, exponent, modulus).Bytes() 379 | // Only consider last 32 bytes 380 | if len(signatureHashFromCert) > 32 { 381 | firstIndex := len(signatureHashFromCert) - 32 382 | signatureHashFromCert = signatureHashFromCert[firstIndex:] 383 | } else if len(signatureHashFromCert) < 32 { 384 | signatureHashFromCert = zeroPad(signatureHashFromCert, 32) 385 | } 386 | verified = bytes.Equal(signatureHash, signatureHashFromCert) 387 | if verified { 388 | break 389 | } 390 | } 391 | 392 | if !verified { 393 | return nil, fmt.Errorf("Invalid token signature: %s", jwt) 394 | } 395 | 396 | // Check time 397 | if token.IssuedAt == 0 { 398 | return nil, fmt.Errorf("Invalid iat value in token: %s", tokenBytes) 399 | } 400 | earliest := token.IssuedAt - clockSkewSecs 401 | if now < earliest { 402 | return nil, fmt.Errorf("Token used too early, %d < %d: %s", now, earliest, tokenBytes) 403 | } 404 | 405 | if token.Expires == 0 { 406 | return nil, fmt.Errorf("Invalid exp value in token: %s", tokenBytes) 407 | } else if token.Expires >= now+maxTokenLifetimeSecs { 408 | return nil, fmt.Errorf("exp value is too far in the future: %s", tokenBytes) 409 | } 410 | latest := token.Expires + clockSkewSecs 411 | if now > latest { 412 | return nil, fmt.Errorf("Token used too late, %d > %d: %s", now, latest, tokenBytes) 413 | } 414 | 415 | return &token, nil 416 | } 417 | 418 | // verifyParsedToken performs further verification of a parsed JWT token and 419 | // checks for the validity of Issuer, Audience, ClientID and Email fields. 420 | // 421 | // Returns true if token passes verification and can be accepted as indicated 422 | // by audiences and clientIDs args. 423 | func verifyParsedToken(c context.Context, token signedJWT, audiences []string, clientIDs []string) bool { 424 | // Verify the issuer. 425 | if token.Issuer != "accounts.google.com" { 426 | log.Warningf(c, "Issuer was not valid: %s", token.Issuer) 427 | return false 428 | } 429 | 430 | // Check audiences. 431 | if token.Audience == "" { 432 | log.Warningf(c, "Invalid aud value in token") 433 | return false 434 | } 435 | 436 | if token.ClientID == "" { 437 | log.Warningf(c, "Invalid azp value in token") 438 | return false 439 | } 440 | 441 | // This is only needed if Audience and ClientID differ, which (currently) only 442 | // happens on Android. In the case they are equal, we only need the ClientID to 443 | // be in the listed of accepted Client IDs. 444 | if token.ClientID != token.Audience && !contains(audiences, token.Audience) { 445 | log.Warningf(c, "Audience not allowed: %s", token.Audience) 446 | return false 447 | } 448 | 449 | // Check allowed client IDs. 450 | if len(clientIDs) == 0 { 451 | log.Warningf(c, "No allowed client IDs specified. ID token cannot be verified.") 452 | return false 453 | } else if !contains(clientIDs, token.ClientID) { 454 | log.Warningf(c, "Client ID is not allowed: %s", token.ClientID) 455 | return false 456 | } 457 | 458 | if token.Email == "" { 459 | log.Warningf(c, "Invalid email value in token") 460 | return false 461 | } 462 | 463 | return true 464 | } 465 | 466 | // currentIDTokenUser returns "appengine/user".User object if provided JWT token 467 | // was successfully decoded and passed all verifications. 468 | func currentIDTokenUser(c context.Context, jwt string, audiences []string, clientIDs []string, now int64) (*user.User, error) { 469 | parsedToken, err := jwtParser(c, jwt, now) 470 | if err != nil { 471 | return nil, err 472 | } 473 | 474 | if verifyParsedToken(c, *parsedToken, audiences, clientIDs) { 475 | return &user.User{ 476 | ID: parsedToken.Subject, 477 | Email: parsedToken.Email, 478 | ClientID: parsedToken.ClientID, 479 | }, nil 480 | } 481 | 482 | return nil, errors.New("No ID token user found.") 483 | } 484 | 485 | // CurrentBearerTokenScope compares given scopes and clientIDs with those in c. 486 | // 487 | // Both scopes and clientIDs args must have at least one element. 488 | // 489 | // Returns a single scope (one of provided scopes) if the two conditions are met: 490 | // - it is found in Context c 491 | // - client ID on that scope matches one of clientIDs in the args 492 | func CurrentBearerTokenScope(c context.Context, scopes []string, clientIDs []string) (string, error) { 493 | auth := authenticator(c) 494 | if auth == nil { 495 | return "", errNoAuthenticator 496 | } 497 | for _, scope := range scopes { 498 | currentClientID, err := auth.CurrentOAuthClientID(c, scope) 499 | if err != nil { 500 | continue 501 | } 502 | 503 | for _, id := range clientIDs { 504 | if id == currentClientID { 505 | return scope, nil 506 | } 507 | } 508 | 509 | // If none of the client IDs matches, return nil 510 | log.Debugf(c, "Couldn't find current client ID %q in %v", currentClientID, clientIDs) 511 | return "", errors.New("Mismatched Client ID") 512 | } 513 | // No client ID found for any of the scopes 514 | return "", errors.New("No valid scope") 515 | } 516 | 517 | // CurrentBearerTokenUser returns a user associated with the request which is 518 | // expected to have a Bearer token. 519 | // 520 | // Both scopes and clientIDs must have at least one element. 521 | // 522 | // Returns an error if the client did not make a valid request, or none of 523 | // clientIDs are allowed to make requests, or user did not authorize any of 524 | // the scopes. 525 | func CurrentBearerTokenUser(c context.Context, scopes []string, clientIDs []string) (*user.User, error) { 526 | auth := authenticator(c) 527 | if auth == nil { 528 | return nil, errNoAuthenticator 529 | } 530 | scope, err := CurrentBearerTokenScope(c, scopes, clientIDs) 531 | if err != nil { 532 | return nil, err 533 | } 534 | 535 | return auth.CurrentOAuthUser(c, scope) 536 | } 537 | 538 | // CurrentUser checks for both JWT and Bearer tokens. 539 | // 540 | // It first tries to decode and verify JWT token (if conditions are met) 541 | // and falls back to Bearer token. 542 | // 543 | // The returned user will have only ID, Email and ClientID fields set. 544 | // User.ID is a Google Account ID, which is different from GAE user ID. 545 | // For more info on User.ID see 'sub' claim description on 546 | // https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo 547 | func CurrentUser(c context.Context, scopes []string, audiences []string, clientIDs []string) (*user.User, error) { 548 | // The user hasn't provided any information to allow us to parse either 549 | // an ID token or a Bearer token. 550 | if len(scopes) == 0 && len(audiences) == 0 && len(clientIDs) == 0 { 551 | return nil, errors.New("no client ID or scope info provided.") 552 | } 553 | r := HTTPRequest(c) 554 | if r == nil { 555 | return nil, errNoRequest 556 | } 557 | 558 | token := parseToken(r) 559 | if token == "" { 560 | return nil, errors.New("No token in the current context.") 561 | } 562 | 563 | // If the only scope is the email scope, check an ID token. Alternatively, 564 | // we dould check if token starts with "ya29." or "1/" to decide that it 565 | // is a Bearer token. This is what is done in Java. 566 | if len(scopes) == 1 && scopes[0] == EmailScope && len(clientIDs) > 0 { 567 | log.Debugf(c, "Checking for ID token.") 568 | now := currentUTC().Unix() 569 | u, err := currentIDTokenUser(c, token, audiences, clientIDs, now) 570 | // Only return in case of success, else pass along and try 571 | // parsing Bearer token. 572 | if err == nil { 573 | return u, err 574 | } 575 | } 576 | 577 | log.Debugf(c, "Checking for Bearer token.") 578 | return CurrentBearerTokenUser(c, scopes, clientIDs) 579 | } 580 | 581 | func init() { 582 | if appengine.IsDevAppServer() { 583 | AuthenticatorFactory = tokeninfoAuthenticatorFactory 584 | } else { 585 | AuthenticatorFactory = cachingAuthenticatorFactory 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /endpoints/apiconfig_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | const ( 12 | dummyClientID = "dummy-client-id" 13 | dummyScope1 = "http://dummy.scope.1" 14 | dummyScope2 = "http://dummy.scope.2" 15 | dummyAudience = "people" 16 | ) 17 | 18 | var ( 19 | emptySlice = []string{} 20 | clientIDs = []string{dummyClientID} 21 | scopes = []string{dummyScope1, dummyScope2} 22 | audiences = []string{dummyAudience} 23 | ) 24 | 25 | type canMarshal struct { 26 | name string 27 | } 28 | 29 | func (m *canMarshal) MarshalJSON() ([]byte, error) { 30 | return []byte("Hello, " + m.name), nil 31 | } 32 | 33 | func (m *canMarshal) UnmarshalJSON(b []byte) error { 34 | parts := strings.SplitN(string(b), " ", 2) 35 | if len(parts) > 1 { 36 | m.name = parts[1] 37 | } else { 38 | m.name = parts[0] 39 | } 40 | return nil 41 | } 42 | 43 | // make sure canMarshal type implements json.Marshaler and json.Unmarshaler 44 | // interfaces 45 | var _ = json.Marshaler((*canMarshal)(nil)) 46 | var _ = json.Unmarshaler((*canMarshal)(nil)) 47 | 48 | type DummyMsg struct { 49 | String string `json:"str" endpoints:"req,desc=A string field"` 50 | Int int `json:"i" endpoints:"min=-200,max=200,d=-100"` 51 | Uint uint `endpoints:"min=0,max=100"` 52 | Int64 int64 `endpoints:"d=123"` 53 | Uint64 uint64 `endpoints:"d=123"` 54 | Float32 float32 `endpoints:"d=123.456"` 55 | Float64 float64 `endpoints:"d=123.456"` 56 | BoolField bool `json:"bool_field" endpoints:"d=true"` 57 | Pstring *string `json:"pstring"` 58 | Pint *int `json:"pint"` 59 | Puint *uint `json:"puint"` 60 | Pint64 *int64 `json:"pint64"` 61 | Puint64 *uint64 `json:"puint64"` 62 | Pfloat32 *float32 `json:"pfloat32"` 63 | Pfloat64 *float64 `json:"pfloat64"` 64 | PBool *bool `json:"pbool"` 65 | Bytes []byte 66 | Internal string `json:"-"` 67 | Marshal *canMarshal 68 | } 69 | 70 | type DummySubMsg struct { 71 | Simple string `json:"simple" endpoints:"d=Hello gopher"` 72 | Message *DummyMsg `json:"msg"` 73 | } 74 | 75 | type DummyListReq struct { 76 | Limit int `json:"limit" endpoints:"d=10,max=100"` 77 | Cursor *canMarshal `json:"cursor"` 78 | } 79 | 80 | type DummyListMsg struct { 81 | Messages []*DummyMsg `json:"items"` 82 | } 83 | 84 | type DummyService struct { 85 | } 86 | 87 | func (s *DummyService) Post(*http.Request, *DummyMsg, *DummySubMsg) error { 88 | return nil 89 | } 90 | 91 | func (s *DummyService) PutAuth(*http.Request, *DummyMsg, *VoidMessage) error { 92 | return nil 93 | } 94 | 95 | func (s *DummyService) GetSub(*http.Request, *DummySubMsg, *DummyMsg) error { 96 | return nil 97 | } 98 | 99 | func (s *DummyService) GetList(*http.Request, *DummyListReq, *DummyListMsg) error { 100 | return nil 101 | } 102 | 103 | // createDescriptor creates APIDescriptor for DummyService. 104 | func createDescriptor(t *testing.T) *APIDescriptor { 105 | dummy := &DummyService{} 106 | server := NewServer("") 107 | s, err := server.RegisterService(dummy, "Dummy", "v1", "A service", true) 108 | if err != nil { 109 | t.Fatalf("createDescriptor: error registering service: %v", err) 110 | } 111 | 112 | info := s.MethodByName("Post").Info() 113 | info.Name, info.Path, info.HTTPMethod, info.Desc = 114 | "post", "post/{i}/{bool_field}/{Float64}", "POST", "A POST method" 115 | 116 | info = s.MethodByName("PutAuth").Info() 117 | info.Name, info.Path, info.HTTPMethod, info.Desc = 118 | "auth", "auth", "PUT", "Method with auth" 119 | info.ClientIds, info.Scopes, info.Audiences = 120 | clientIDs, scopes, audiences 121 | 122 | info = s.MethodByName("GetSub").Info() 123 | info.Name, info.Path, info.HTTPMethod, info.Desc = 124 | "sub.sub", "sub/{simple}/{msg.i}/{msg.str}", "GET", "With substruct" 125 | 126 | info = s.MethodByName("GetList").Info() 127 | info.Name, info.Path, info.HTTPMethod, info.Desc = 128 | "list", "list", "GET", "Messages list" 129 | 130 | d := &APIDescriptor{} 131 | if err := s.APIDescriptor(d, "testhost:1234"); err != nil { 132 | t.Fatalf("createDescriptor: error creating descriptor: %v", err) 133 | } 134 | return d 135 | } 136 | 137 | func TestAPIDescriptor(t *testing.T) { 138 | d := createDescriptor(t) 139 | verifyPairs(t, 140 | d.Extends, "thirdParty.api", 141 | d.Root, "https://testhost:1234/_ah/api", 142 | d.Name, "dummy", 143 | d.Version, "v1", 144 | d.Default, true, 145 | d.Adapter.Bns, "https://testhost:1234/_ah/spi", 146 | d.Adapter.Type, "lily", 147 | len(d.Methods), 4, 148 | len(d.Descriptor.Methods), 4, 149 | len(d.Descriptor.Schemas), 3, 150 | d.Desc, "A service", 151 | ) 152 | } 153 | 154 | // --------------------------------------------------------------------------- 155 | // $METHOD_MAP 156 | 157 | func TestAPIPostMethod(t *testing.T) { 158 | d := createDescriptor(t) 159 | meth := d.Methods["dummy.post"] 160 | if meth == nil { 161 | t.Fatal("want APIMethod 'dummy.post'") 162 | return 163 | } 164 | verifyPairs(t, 165 | meth.Path, "post/{i}/{bool_field}/{Float64}", 166 | meth.HTTPMethod, "POST", 167 | meth.RosyMethod, "DummyService.Post", 168 | meth.Request.Body, "autoTemplate(backendRequest)", 169 | meth.Request.BodyName, "resource", 170 | meth.Response.Body, "autoTemplate(backendResponse)", 171 | meth.Response.BodyName, "resource", 172 | len(meth.Scopes), 0, 173 | len(meth.Audiences), 0, 174 | len(meth.ClientIds), 0, 175 | meth.Desc, "A POST method", 176 | ) 177 | 178 | params := meth.Request.Params 179 | tts := [][]interface{}{ 180 | {"i", "int32", true, -100, -200, 200, false, 0}, 181 | {"bool_field", "boolean", true, true, nil, nil, false, 0}, 182 | {"Float64", "double", true, 123.456, nil, nil, false, 0}, 183 | } 184 | 185 | for _, tt := range tts { 186 | name := tt[0].(string) 187 | p := params[name] 188 | if p == nil { 189 | t.Errorf("want param %q in %v", name, params) 190 | } 191 | verifyPairs(t, 192 | p.Type, tt[1], 193 | p.Required, tt[2], 194 | p.Default, tt[3], 195 | p.Min, tt[4], 196 | p.Max, tt[5], 197 | p.Repeated, tt[6], 198 | len(p.Enum), tt[7], 199 | ) 200 | } 201 | if lp, ltts := len(params), len(tts); lp != ltts { 202 | t.Errorf("have %d params for %q; want %d", lp, meth.RosyMethod, ltts) 203 | } 204 | } 205 | 206 | func TestAPIPutAuthMethod(t *testing.T) { 207 | d := createDescriptor(t) 208 | meth := d.Methods["dummy.auth"] 209 | if meth == nil { 210 | t.Fatal("want APIMethod 'dummy.auth'") 211 | return 212 | } 213 | verifyPairs(t, 214 | meth.HTTPMethod, "PUT", 215 | meth.RosyMethod, "DummyService.PutAuth", 216 | meth.Request.Body, "autoTemplate(backendRequest)", 217 | meth.Request.BodyName, "resource", 218 | meth.Response.Body, "empty", 219 | meth.Response.BodyName, "", 220 | meth.ClientIds, []string{dummyClientID}, 221 | meth.Scopes, []string{dummyScope1, dummyScope2}, 222 | meth.Audiences, []string{dummyAudience}, 223 | len(meth.Request.Params), 0, 224 | ) 225 | } 226 | 227 | func TestAPIGetSubMethod(t *testing.T) { 228 | d := createDescriptor(t) 229 | // apiname.resource.method 230 | meth := d.Methods["dummy.sub.sub"] 231 | if meth == nil { 232 | t.Fatal("want APIMethod 'dummy.sub.sub'") 233 | } 234 | verifyPairs(t, 235 | meth.Path, "sub/{simple}/{msg.i}/{msg.str}", 236 | meth.HTTPMethod, "GET", 237 | meth.RosyMethod, "DummyService.GetSub", 238 | meth.Request.Body, "empty", 239 | meth.Request.BodyName, "", 240 | meth.Response.Body, "autoTemplate(backendResponse)", 241 | meth.Response.BodyName, "resource", 242 | len(meth.Scopes), 0, 243 | len(meth.Audiences), 0, 244 | len(meth.ClientIds), 0, 245 | ) 246 | 247 | params := meth.Request.Params 248 | tts := [][]interface{}{ 249 | {"simple", "string", false, "Hello gopher", nil, nil, false, 0}, 250 | {"msg.i", "int32", false, -100, -200, 200, false, 0}, 251 | {"msg.str", "string", true, nil, nil, nil, false, 0}, 252 | {"msg.Int64", "int64", false, int64(123), nil, nil, false, 0}, 253 | {"msg.Uint", "uint32", false, nil, uint32(0), uint32(100), false, 0}, 254 | {"msg.Uint64", "uint64", false, uint64(123), nil, nil, false, 0}, 255 | {"msg.Float32", "float", false, float32(123.456), nil, nil, false, 0}, 256 | {"msg.Float64", "double", false, 123.456, nil, nil, false, 0}, 257 | {"msg.bool_field", "boolean", false, true, nil, nil, false, 0}, 258 | {"msg.pstring", "string", false, nil, nil, nil, false, 0}, 259 | {"msg.pint", "int32", false, nil, nil, nil, false, 0}, 260 | {"msg.puint", "uint32", false, nil, nil, nil, false, 0}, 261 | {"msg.pint64", "int64", false, nil, nil, nil, false, 0}, 262 | {"msg.puint64", "uint64", false, nil, nil, nil, false, 0}, 263 | {"msg.pfloat32", "float", false, nil, nil, nil, false, 0}, 264 | {"msg.pfloat64", "double", false, nil, nil, nil, false, 0}, 265 | {"msg.pbool", "boolean", false, nil, nil, nil, false, 0}, 266 | {"msg.Bytes", "bytes", false, nil, nil, nil, false, 0}, 267 | {"msg.Marshal", "string", false, nil, nil, nil, false, 0}, 268 | } 269 | 270 | for _, tt := range tts { 271 | name := tt[0].(string) 272 | p := params[name] 273 | if p == nil { 274 | t.Errorf("want param %q in %#v", name, params) 275 | continue 276 | } 277 | verifyPairs(t, 278 | p.Type, tt[1], 279 | p.Required, tt[2], 280 | p.Default, tt[3], 281 | p.Min, tt[4], 282 | p.Max, tt[5], 283 | p.Repeated, tt[6], 284 | len(p.Enum), tt[7], 285 | ) 286 | } 287 | 288 | if lp, ltts := len(params), len(tts); lp != ltts { 289 | t.Errorf("have %d params for %q; want %d", lp, meth.RosyMethod, ltts) 290 | } 291 | } 292 | 293 | func TestAPIGetListMethod(t *testing.T) { 294 | d := createDescriptor(t) 295 | meth := d.Methods["dummy.list"] 296 | if meth == nil { 297 | t.Fatal("want APIMethod 'dummy.list'") 298 | return 299 | } 300 | verifyPairs(t, 301 | meth.HTTPMethod, "GET", 302 | meth.RosyMethod, "DummyService.GetList", 303 | meth.Request.Body, "empty", 304 | meth.Request.BodyName, "", 305 | meth.Response.Body, "autoTemplate(backendResponse)", 306 | meth.Response.BodyName, "resource", 307 | len(meth.Scopes), 0, 308 | len(meth.Audiences), 0, 309 | len(meth.ClientIds), 0, 310 | ) 311 | 312 | params := meth.Request.Params 313 | tts := [][]interface{}{ 314 | {"limit", "int32", false, 10, nil, 100, false, 0}, 315 | {"cursor", "string", false, nil, nil, nil, false, 0}, 316 | } 317 | 318 | for _, tt := range tts { 319 | name := tt[0].(string) 320 | p := params[name] 321 | if p == nil { 322 | t.Errorf("want param %q in %v", name, params) 323 | continue 324 | } 325 | verifyPairs(t, 326 | p.Type, tt[1], 327 | p.Required, tt[2], 328 | p.Default, tt[3], 329 | p.Min, tt[4], 330 | p.Max, tt[5], 331 | p.Repeated, tt[6], 332 | len(p.Enum), tt[7], 333 | ) 334 | } 335 | if lp, ltts := len(params), len(tts); lp != ltts { 336 | t.Errorf("have %d params for %q; want %d", lp, meth.RosyMethod, ltts) 337 | } 338 | } 339 | 340 | func TestDuplicateMethodName(t *testing.T) { 341 | dummy := &DummyService{} 342 | server := NewServer("") 343 | s, err := server.RegisterService(dummy, "Dummy", "v1", "A service", true) 344 | if err != nil { 345 | t.Fatalf("error registering service: %v", err) 346 | } 347 | 348 | s.MethodByName("GetSub").Info().Name = "someMethod" 349 | s.MethodByName("GetList").Info().Name = "someMethod" 350 | 351 | d := &APIDescriptor{} 352 | err = s.APIDescriptor(d, "testhost:1234") 353 | if err == nil { 354 | t.Fatal("want duplicate method error") 355 | } 356 | t.Logf("dup method error: %v", err) 357 | } 358 | 359 | func TestDuplicateHTTPMethodPath(t *testing.T) { 360 | dummy := &DummyService{} 361 | server := NewServer("") 362 | s, err := server.RegisterService(dummy, "Dummy", "v1", "A service", true) 363 | if err != nil { 364 | t.Fatalf("error registering service: %s", err) 365 | } 366 | 367 | info := s.MethodByName("GetSub").Info() 368 | info.HTTPMethod, info.Path = "GET", "some/{param}/path" 369 | info = s.MethodByName("GetList").Info() 370 | info.HTTPMethod, info.Path = "GET", "some/{param}/path" 371 | 372 | d := &APIDescriptor{} 373 | err = s.APIDescriptor(d, "testhost:1234") 374 | if err == nil { 375 | t.Fatal("want duplicate HTTP method + path error") 376 | } 377 | t.Logf("dup method error: %v", err) 378 | } 379 | 380 | func TestNotDuplicateHTTPMethodPathWhenMultipleBrackets(t *testing.T) { 381 | // Test for https://github.com/GoogleCloudPlatform/go-endpoints/issues/74 382 | dummy := &DummyService{} 383 | server := NewServer("") 384 | s, err := server.RegisterService(dummy, "Dummy", "v1", "A service", true) 385 | if err != nil { 386 | t.Fatalf("error registering service: %s", err) 387 | } 388 | 389 | info := s.MethodByName("GetSub").Info() 390 | info.HTTPMethod, info.Path = "GET", "some/{param}" 391 | info = s.MethodByName("GetList").Info() 392 | info.HTTPMethod, info.Path = "GET", "some/{param1}/path/{param2}" 393 | 394 | d := &APIDescriptor{} 395 | err = s.APIDescriptor(d, "testhost:1234") 396 | if err != nil { 397 | t.Logf("dup method error: %v", err) 398 | t.Fatal("Don't want duplicate HTTP method + path error") 399 | } 400 | } 401 | 402 | func TestPrefixedSchemaName(t *testing.T) { 403 | const prefix = "SomePrefix" 404 | 405 | origSchemaNameForType := SchemaNameForType 406 | defer func() { SchemaNameForType = origSchemaNameForType }() 407 | SchemaNameForType = func(t reflect.Type) string { 408 | return prefix + t.Name() 409 | } 410 | 411 | d := createDescriptor(t) 412 | for name := range d.Descriptor.Schemas { 413 | if !strings.HasPrefix(name, prefix) { 414 | t.Errorf("HasPrefix(%q, %q) = false", name, prefix) 415 | } 416 | } 417 | 418 | for mname, meth := range d.Descriptor.Methods { 419 | if meth.Request != nil { 420 | if !strings.HasPrefix(meth.Request.Ref, prefix) { 421 | t.Errorf("HasPrefix(%q, %q) = false; request of %q", 422 | meth.Request.Ref, prefix, mname) 423 | } 424 | } 425 | if meth.Response != nil { 426 | if !strings.HasPrefix(meth.Response.Ref, prefix) { 427 | t.Errorf("HasPrefix(%q, %q) = false; response of %q", 428 | meth.Response.Ref, prefix, mname) 429 | } 430 | } 431 | } 432 | } 433 | 434 | // --------------------------------------------------------------------------- 435 | // $SCHEMA_DESCRIPTOR (SCHEMAS) 436 | 437 | func verifySchema(t *testing.T, schemaID string, schemaProps [][]interface{}) { 438 | d := createDescriptor(t) 439 | s := d.Descriptor.Schemas[schemaID] 440 | if s == nil { 441 | t.Errorf("want %q schema", schemaID) 442 | return 443 | } 444 | 445 | verifyPairs(t, 446 | s.ID, schemaID, 447 | s.Type, "object", 448 | s.Desc, "") 449 | 450 | for _, tt := range schemaProps { 451 | name := tt[0].(string) 452 | p := s.Properties[name] 453 | if p == nil { 454 | t.Errorf("want %q in %#v", name, s.Properties) 455 | continue 456 | } 457 | verifyPairs(t, 458 | p.Type, tt[1], 459 | p.Format, tt[2], 460 | p.Required, tt[3], 461 | p.Default, tt[4], 462 | p.Ref, tt[5], 463 | p.Desc, tt[6], 464 | ) 465 | if len(tt) == 7 && p.Items != nil { 466 | t.Errorf("want %s.Items of %s to be nil", name, s.ID) 467 | } else if len(tt) == 13 { 468 | verifyPairs(t, 469 | p.Items.Type, tt[7], 470 | p.Items.Format, tt[8], 471 | p.Items.Required, tt[9], 472 | p.Items.Default, tt[10], 473 | p.Items.Ref, tt[11], 474 | p.Items.Desc, tt[12], 475 | ) 476 | } 477 | } 478 | 479 | if lp, ltts := len(s.Properties), len(schemaProps); lp != ltts { 480 | t.Errorf("have %d props in %q; want %d", lp, s.ID, ltts) 481 | } 482 | } 483 | 484 | func TestDummyMsgSchema(t *testing.T) { 485 | props := [][]interface{}{ 486 | // name, type, format, required, default, ref, desc 487 | {"str", "string", "", true, nil, "", "A string field"}, 488 | {"i", "integer", "int32", false, -100, "", ""}, 489 | {"Uint", "integer", "uint32", false, nil, "", ""}, 490 | {"Int64", "string", "int64", false, int64(123), "", ""}, 491 | {"Uint64", "string", "uint64", false, uint64(123), "", ""}, 492 | {"Float32", "number", "float", false, float32(123.456), "", ""}, 493 | {"Float64", "number", "double", false, float64(123.456), "", ""}, 494 | {"bool_field", "boolean", "", false, true, "", ""}, 495 | {"pstring", "string", "", false, nil, "", ""}, 496 | {"pint", "integer", "int32", false, nil, "", ""}, 497 | {"puint", "integer", "uint32", false, nil, "", ""}, 498 | {"pint64", "string", "int64", false, nil, "", ""}, 499 | {"puint64", "string", "uint64", false, nil, "", ""}, 500 | {"pfloat32", "number", "float", false, nil, "", ""}, 501 | {"pfloat64", "number", "double", false, nil, "", ""}, 502 | {"pbool", "boolean", "", false, nil, "", ""}, 503 | {"Bytes", "string", "byte", false, nil, "", ""}, 504 | {"Marshal", "string", "", false, nil, "", ""}, 505 | } 506 | 507 | verifySchema(t, "DummyMsg", props) 508 | } 509 | 510 | func TestDummySubMsgSchema(t *testing.T) { 511 | props := [][]interface{}{ 512 | {"simple", "string", "", false, "Hello gopher", "", ""}, 513 | {"msg", "", "", false, nil, "DummyMsg", ""}, 514 | } 515 | 516 | verifySchema(t, "DummySubMsg", props) 517 | } 518 | 519 | func TestDummyListMsgSchema(t *testing.T) { 520 | props := [][]interface{}{ 521 | // name, type, format, required, default, ref, desc 522 | {"items", "array", "", false, nil, "", "", 523 | // item format 524 | "", "", false, nil, "DummyMsg", "", 525 | }, 526 | } 527 | 528 | verifySchema(t, "DummyListMsg", props) 529 | } 530 | 531 | // --------------------------------------------------------------------------- 532 | // $SCHEMA_DESCRIPTOR (METHODS) 533 | 534 | func TestDescriptorMethods(t *testing.T) { 535 | d := createDescriptor(t) 536 | 537 | tts := []*struct { 538 | name, in, out string 539 | }{ 540 | {"DummyService.Post", "DummyMsg", "DummySubMsg"}, 541 | {"DummyService.PutAuth", "DummyMsg", ""}, 542 | {"DummyService.GetSub", "", "DummyMsg"}, 543 | {"DummyService.GetList", "", "DummyListMsg"}, 544 | } 545 | for _, tt := range tts { 546 | meth := d.Descriptor.Methods[tt.name] 547 | if meth == nil { 548 | t.Errorf("want %q method descriptor", tt.name) 549 | continue 550 | } 551 | 552 | switch { 553 | case tt.in == "": 554 | if meth.Request != nil { 555 | t.Errorf("%s: have req %#v; want nil", 556 | tt.name, meth.Request) 557 | } 558 | case tt.in != "": 559 | if meth.Request == nil || meth.Request.Ref != tt.in { 560 | t.Errorf("%s: have req %#v; want %q", 561 | tt.name, meth.Request, tt.in) 562 | } 563 | } 564 | switch { 565 | case tt.out == "": 566 | if meth.Response != nil { 567 | t.Errorf("%s: have req %#v; want nil", 568 | tt.name, meth.Response) 569 | } 570 | case tt.out != "": 571 | if meth.Response == nil || meth.Response.Ref != tt.out { 572 | t.Errorf("%s: have resp %#v; want %q", 573 | tt.name, meth.Response, tt.out) 574 | } 575 | } 576 | } 577 | } 578 | 579 | // --------------------------------------------------------------------------- 580 | // Types tests 581 | 582 | func TestFieldNamesSimple(t *testing.T) { 583 | s := struct { 584 | Name string `json:"name"` 585 | Age int 586 | unexported string 587 | Internal string `json:"-"` 588 | Marshal *canMarshal 589 | }{} 590 | 591 | m := fieldNames(reflect.TypeOf(s), true) 592 | names := []string{"name", "Age", "Marshal"} 593 | 594 | for _, k := range names { 595 | field, exists := m[k] 596 | switch { 597 | case !exists: 598 | t.Errorf("want %q in %#v", k, m) 599 | case field == nil: 600 | t.Errorf("want non-nil field %q", k) 601 | } 602 | } 603 | if len(m) != len(names) { 604 | t.Errorf("have %d elements; want %d", len(m), len(names)) 605 | } 606 | } 607 | 608 | func TestFieldNamesNested(t *testing.T) { 609 | s := struct { 610 | Root string 611 | Nested struct { 612 | Param string 613 | TwoPtr *struct { 614 | Param string `json:"param"` 615 | } 616 | } `json:"n"` 617 | }{} 618 | 619 | tts := []struct { 620 | flatten bool 621 | names []string 622 | }{ 623 | {true, []string{"Root", "n.Param", "n.TwoPtr.param"}}, 624 | {false, []string{"Root", "n"}}, 625 | } 626 | 627 | for _, tt := range tts { 628 | m := fieldNames(reflect.TypeOf(s), tt.flatten) 629 | for _, k := range tt.names { 630 | if _, exists := m[k]; !exists { 631 | t.Errorf("want %q in %#v", k, m) 632 | } 633 | } 634 | if len(m) != len(tt.names) { 635 | t.Errorf("have %d items in %#v; want %d", len(m), m, len(tt.names)) 636 | } 637 | } 638 | } 639 | 640 | func TestFieldNamesAnonymous(t *testing.T) { 641 | type Inner struct { 642 | Msg string 643 | } 644 | outer := struct { 645 | Inner 646 | Root string 647 | }{} 648 | 649 | m := fieldNames(reflect.TypeOf(outer), false) 650 | 651 | if len(m) != 2 { 652 | t.Fatalf("have %d fields; want 2", len(m)) 653 | } 654 | 655 | if _, ok := m["Root"]; !ok { 656 | t.Errorf("want 'Root' field") 657 | } 658 | 659 | f, ok := m["Msg"] 660 | if !ok { 661 | t.Fatal("want 'Msg' field from 'Inner'") 662 | } 663 | if f.Type.Kind() != reflect.String { 664 | t.Errorf("have 'Msg' kind = %v; want %v", f.Type.Kind(), reflect.String) 665 | } 666 | } 667 | 668 | // --------------------------------------------------------------------------- 669 | // Parse tests 670 | 671 | func TestParsePath(t *testing.T) { 672 | const in = "one/{a}/two/{b}/three/{c.d}" 673 | out, _ := parsePath(in) 674 | want := []string{"a", "b", "c.d"} 675 | if !reflect.DeepEqual(out, want) { 676 | t.Errorf("parsePath(%v) = %v; want %v", in, out, want) 677 | } 678 | } 679 | 680 | func TestParseValue(t *testing.T) { 681 | tts := []struct { 682 | kind reflect.Kind 683 | val string 684 | want interface{} 685 | shouldError bool 686 | }{ 687 | {reflect.Int, "123", 123, false}, 688 | {reflect.Int8, "123", 123, false}, 689 | {reflect.Int16, "123", 123, false}, 690 | {reflect.Int32, "123", 123, false}, 691 | {reflect.Int64, "123", int64(123), false}, 692 | {reflect.Uint, "123", uint32(123), false}, 693 | {reflect.Uint8, "123", uint32(123), false}, 694 | {reflect.Uint16, "123", uint32(123), false}, 695 | {reflect.Uint32, "123", uint32(123), false}, 696 | {reflect.Uint64, "123", uint64(123), false}, 697 | {reflect.Float32, "123", float32(123), false}, 698 | {reflect.Float64, "123", float64(123), false}, 699 | {reflect.Bool, "true", true, false}, 700 | {reflect.String, "hello", "hello", false}, 701 | 702 | {reflect.Int, "", nil, false}, 703 | {reflect.Struct, "{}", nil, true}, 704 | {reflect.Float32, "x", nil, true}, 705 | } 706 | 707 | for i, tt := range tts { 708 | out, err := parseValue(tt.val, tt.kind) 709 | switch { 710 | case err == nil && !tt.shouldError && out != tt.want: 711 | t.Errorf("%d: parseValue(%q) = %v (%T); want %v (%T)", 712 | i, tt.val, out, out, tt.want, tt.want) 713 | case err == nil && tt.shouldError: 714 | t.Errorf("%d: parseValue(%q) = %#v; want error", i, tt.val, out) 715 | case err != nil && !tt.shouldError: 716 | t.Errorf("%d: parseValue(%q) = %v", i, tt.val, err) 717 | } 718 | } 719 | } 720 | 721 | func TestParseTag(t *testing.T) { 722 | type s struct { 723 | Empty string 724 | Ignored string `endpoints:"req,ignored_part,desc=Some field"` 725 | Opt int `endpoints:"d=123,min=1,max=200,desc=Int field"` 726 | Invalid uint `endpoints:"req,d=100"` 727 | } 728 | 729 | testFields := []struct { 730 | name string 731 | tag *endpointsTag 732 | }{ 733 | {"Empty", &endpointsTag{false, "", "", "", ""}}, 734 | {"Ignored", &endpointsTag{true, "", "", "", "Some field"}}, 735 | {"Opt", &endpointsTag{false, "123", "1", "200", "Int field"}}, 736 | {"Invalid", nil}, 737 | } 738 | 739 | typ := reflect.TypeOf(s{}) 740 | for i, tf := range testFields { 741 | field, _ := typ.FieldByName(tf.name) 742 | parsed, err := parseTag(field.Tag) 743 | switch { 744 | case err != nil && tf.tag != nil: 745 | t.Errorf("%d: parseTag(%q) = %v; want %v", i, field.Tag, err, tf.tag) 746 | case err == nil && tf.tag != nil && !reflect.DeepEqual(parsed, tf.tag): 747 | t.Errorf("%d: parseTag(%q) = %#+v; want %#+v", 748 | i, field.Tag, parsed, tf.tag) 749 | case err == nil && tf.tag == nil: 750 | t.Errorf("%d: parseTag(%q) = %#v; want error", i, field.Tag, parsed) 751 | } 752 | } 753 | } 754 | -------------------------------------------------------------------------------- /endpoints/apiconfig.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // curlyBrackets is used for generating the key for dups map in APIDescriptor(). 15 | var curlyBrackets = regexp.MustCompile("{.+?}") 16 | 17 | // APIDescriptor is the top-level struct for a single Endpoints API config. 18 | type APIDescriptor struct { 19 | // Required 20 | Extends string `json:"extends"` 21 | Root string `json:"root"` 22 | Name string `json:"name"` 23 | Version string `json:"version"` 24 | Default bool `json:"defaultVersion"` 25 | Abstract bool `json:"abstract"` 26 | Adapter struct { 27 | Bns string `json:"bns"` 28 | Type string `json:"type"` 29 | } `json:"adapter"` 30 | 31 | // Optional 32 | Cname string `json:"canonicalName,omitempty"` 33 | Desc string `json:"description,omitempty"` 34 | Auth *struct { 35 | AllowCookie bool `json:"allowCookieAuth"` 36 | } `json:"auth,omitempty"` 37 | 38 | // $METHOD_MAP 39 | Methods map[string]*APIMethod `json:"methods"` 40 | 41 | // $SCHEMA_DESCRIPTOR 42 | Descriptor struct { 43 | Methods map[string]*APIMethodDescriptor `json:"methods"` 44 | Schemas map[string]*APISchemaDescriptor `json:"schemas"` 45 | } `json:"descriptor"` 46 | } 47 | 48 | // APIMethod is an item of $METHOD_MAP 49 | type APIMethod struct { 50 | Path string `json:"path"` 51 | HTTPMethod string `json:"httpMethod"` 52 | RosyMethod string `json:"rosyMethod"` 53 | Request APIReqRespDescriptor `json:"request"` 54 | Response APIReqRespDescriptor `json:"response"` 55 | 56 | Scopes []string `json:"scopes,omitempty"` 57 | Audiences []string `json:"audiences,omitempty"` 58 | ClientIds []string `json:"clientIds,omitempty"` 59 | Desc string `json:"description,omitempty"` 60 | } 61 | 62 | // APIReqRespDescriptor indicates type of request data expected to be found 63 | // in a request or a response. 64 | type APIReqRespDescriptor struct { 65 | Body string `json:"body"` 66 | BodyName string `json:"bodyName,omitempty"` 67 | Params map[string]*APIRequestParamSpec `json:"parameters,omitempty"` 68 | } 69 | 70 | // APIRequestParamSpec is a description of all the expected request parameters. 71 | type APIRequestParamSpec struct { 72 | Type string `json:"type"` 73 | Required bool `json:"required,omitempty"` 74 | Default interface{} `json:"default,omitempty"` 75 | Repeated bool `json:"repeated,omitempty"` 76 | Enum map[string]*APIEnumParamSpec `json:"enum,omitempty"` 77 | // only for int32/int64/uint32/uint64 78 | Min interface{} `json:"minValue,omitempty"` 79 | Max interface{} `json:"maxValue,omitempty"` 80 | } 81 | 82 | // APIEnumParamSpec is the enum type of request/response param spec. 83 | // Not used currently. 84 | type APIEnumParamSpec struct { 85 | BackendVal string `json:"backendValue"` 86 | Desc string `json:"description,omitempty"` 87 | // TODO: add 'number' field? 88 | } 89 | 90 | // APIMethodDescriptor item of Descriptor.Methods map ($SCHEMA_DESCRIPTOR). 91 | type APIMethodDescriptor struct { 92 | Request *APISchemaRef `json:"request,omitempty"` 93 | Response *APISchemaRef `json:"response,omitempty"` 94 | // Original method of an RPCService 95 | serviceMethod *ServiceMethod 96 | } 97 | 98 | // APISchemaRef is used when referencing a schema from a method or array elem. 99 | type APISchemaRef struct { 100 | Ref string `json:"$ref"` 101 | } 102 | 103 | // APISchemaDescriptor item of Descriptor.Schemas map ($SCHEMA_DESCRIPTOR) 104 | type APISchemaDescriptor struct { 105 | ID string `json:"id"` 106 | Type string `json:"type"` 107 | Properties map[string]*APISchemaProperty `json:"properties"` 108 | Desc string `json:"description,omitempty"` 109 | } 110 | 111 | // APISchemaProperty is an item of APISchemaDescriptor.Properties map 112 | type APISchemaProperty struct { 113 | Type string `json:"type,omitempty"` 114 | Format string `json:"format,omitempty"` 115 | Items *APISchemaProperty `json:"items,omitempty"` 116 | 117 | Required bool `json:"required,omitempty"` 118 | Default interface{} `json:"default,omitempty"` 119 | 120 | Ref string `json:"$ref,omitempty"` 121 | Desc string `json:"description,omitempty"` 122 | } 123 | 124 | // APIDescriptor populates provided APIDescriptor with all info needed to 125 | // generate a discovery doc from its receiver. 126 | // 127 | // Args: 128 | // - dst, a non-nil pointer to APIDescriptor struct 129 | // - host, a hostname used for discovery API config Root and BNS. 130 | // 131 | // Returns error if malformed params were encountered 132 | // (e.g. ServerMethod.Path, etc.) 133 | func (s *RPCService) APIDescriptor(dst *APIDescriptor, host string) error { 134 | if dst == nil { 135 | return errors.New("Destination APIDescriptor is nil") 136 | } 137 | if host == "" { 138 | return errors.New("Empty host parameter") 139 | } 140 | 141 | dst.Extends = "thirdParty.api" 142 | dst.Root = fmt.Sprintf("https://%s/_ah/api", host) 143 | dst.Name = s.Info().Name 144 | dst.Version = s.Info().Version 145 | dst.Default = s.Info().Default 146 | dst.Desc = s.Info().Description 147 | 148 | dst.Adapter.Bns = fmt.Sprintf("https://%s/_ah/spi", host) 149 | dst.Adapter.Type = "lily" 150 | 151 | schemasToCreate := make(map[string]reflect.Type, 0) 152 | methods := s.Methods() 153 | numMethods := len(methods) 154 | 155 | dst.Methods = make(map[string]*APIMethod, numMethods) 156 | dst.Descriptor.Methods = make(map[string]*APIMethodDescriptor, numMethods) 157 | // Sanity check for duplicate HTTP method + path 158 | dups := make(map[string]string, numMethods) 159 | 160 | for _, m := range methods { 161 | info := m.Info() 162 | dupName := info.HTTPMethod + curlyBrackets.ReplaceAllLiteralString(info.Path, "{}") 163 | if mname, ok := dups[dupName]; ok { 164 | return fmt.Errorf(`"%s %s" is already registered with %s`, 165 | info.HTTPMethod, info.Path, mname) 166 | } 167 | dups[dupName] = dst.Name + "." + info.Name 168 | 169 | // Methods of $SCHEMA_DESCRIPTOR 170 | mdescr := &APIMethodDescriptor{serviceMethod: m} 171 | dst.Descriptor.Methods[s.Name()+"."+m.method.Name] = mdescr 172 | if !info.isBodiless() && !isEmptyStruct(m.ReqType) { 173 | refID := schemaNameForType(m.ReqType) 174 | mdescr.Request = &APISchemaRef{Ref: refID} 175 | schemasToCreate[refID] = m.ReqType 176 | } 177 | if !isEmptyStruct(m.RespType) { 178 | refID := schemaNameForType(m.RespType) 179 | mdescr.Response = &APISchemaRef{Ref: refID} 180 | schemasToCreate[refID] = m.RespType 181 | } 182 | 183 | // $METHOD_MAP 184 | apimeth, err := mdescr.toAPIMethod(s.Name()) 185 | if err != nil { 186 | return err 187 | } 188 | mname := dst.Name + "." + info.Name 189 | if m, ok := dst.Methods[mname]; ok { 190 | return fmt.Errorf("Method %q already exists as %q", mname, m.RosyMethod) 191 | } 192 | dst.Methods[mname] = apimeth 193 | } 194 | 195 | // Schemas of $SCHEMA_DESCRIPTOR 196 | dst.Descriptor.Schemas = make( 197 | map[string]*APISchemaDescriptor, len(schemasToCreate)) 198 | for ref, t := range schemasToCreate { 199 | if err := addSchemaFromType(dst.Descriptor.Schemas, ref, t); err != nil { 200 | return err 201 | } 202 | } 203 | return nil 204 | } 205 | 206 | // toAPIMethod creates a new APIMethod using its receiver info and provided 207 | // rosy service name. 208 | // 209 | // Args: 210 | // - rosySrv, original name of a service, e.g. "MyService" 211 | func (md *APIMethodDescriptor) toAPIMethod(rosySrv string) (*APIMethod, error) { 212 | info := md.serviceMethod.Info() 213 | apim := &APIMethod{ 214 | Path: info.Path, 215 | HTTPMethod: info.HTTPMethod, 216 | RosyMethod: rosySrv + "." + md.serviceMethod.method.Name, 217 | Scopes: info.Scopes, 218 | Audiences: info.Audiences, 219 | ClientIds: info.ClientIds, 220 | Desc: info.Desc, 221 | } 222 | 223 | var err error 224 | if md.serviceMethod.Info().isBodiless() { 225 | apim.Request.Params, err = typeToParamsSpec(md.serviceMethod.ReqType) 226 | } else { 227 | apim.Request.Params, err = typeToParamsSpecFromPath( 228 | md.serviceMethod.ReqType, apim.Path) 229 | } 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | setAPIReqRespBody(&apim.Request, "backendRequest", md.Request == nil) 235 | setAPIReqRespBody(&apim.Response, "backendResponse", md.Response == nil) 236 | return apim, nil 237 | } 238 | 239 | // addSchemaFromType creates a new APISchemaDescriptor from given Type t 240 | // and adds it to the map with the key of provided ref arg. 241 | // 242 | // Returns an error if APISchemaDescriptor cannot be created from this Type. 243 | func addSchemaFromType(dst map[string]*APISchemaDescriptor, ref string, t reflect.Type) error { 244 | if ref == "" { 245 | ref = t.Name() 246 | } 247 | if ref == "" { 248 | return fmt.Errorf("Creating schema from unnamed type is currently not supported: %v", t) 249 | } 250 | if _, exists := dst[ref]; exists { 251 | return nil 252 | } 253 | 254 | ensureSchemas := make(map[string]reflect.Type) 255 | sd := &APISchemaDescriptor{ID: ref} 256 | 257 | switch t.Kind() { 258 | // case reflect.Array: 259 | // sd.Type = "array" 260 | // sd.Items... ? 261 | case reflect.Struct: 262 | fieldsMap := fieldNames(t, false) 263 | sd.Properties = make(map[string]*APISchemaProperty, len(fieldsMap)) 264 | sd.Type = "object" 265 | for name, field := range fieldsMap { 266 | fkind := field.Type.Kind() 267 | prop := &APISchemaProperty{} 268 | 269 | // TODO(alex): add support for reflect.Map? 270 | switch { 271 | default: 272 | prop.Type, prop.Format = typeToPropFormat(field.Type) 273 | 274 | case implements(field.Type, typeOfJSONMarshaler): 275 | prop.Type = "string" 276 | 277 | case fkind == reflect.Ptr, fkind == reflect.Struct: 278 | typ := indirectType(field.Type) 279 | if stype, format := typeToPropFormat(typ); stype != "" { 280 | // pointer to a basic type. 281 | prop.Type, prop.Format = stype, format 282 | } else { 283 | switch { 284 | case typ == typeOfTime: 285 | prop.Type, prop.Format = "string", "date-time" 286 | 287 | case typ.Kind() == reflect.Struct: 288 | prop.Ref = schemaNameForType(typ) 289 | ensureSchemas[prop.Ref] = typ 290 | default: 291 | return fmt.Errorf( 292 | "Unsupported type %#v of property %s.%s", 293 | field.Type, sd.ID, name) 294 | } 295 | } 296 | 297 | case fkind == reflect.Slice: 298 | if field.Type == typeOfBytes { 299 | prop.Type, prop.Format = "string", "byte" 300 | break 301 | } 302 | prop.Type = "array" 303 | prop.Items = &APISchemaProperty{} 304 | el := field.Type.Elem() 305 | if el.Kind() == reflect.Ptr { 306 | el = el.Elem() 307 | } 308 | k := el.Kind() 309 | // TODO(alex): Add support for reflect.Map? 310 | if k == reflect.Struct { 311 | prop.Items.Ref = schemaNameForType(el) 312 | ensureSchemas[prop.Items.Ref] = el 313 | } else { 314 | prop.Items.Type, prop.Items.Format = typeToPropFormat(el) 315 | } 316 | } 317 | 318 | tag, err := parseTag(field.Tag) 319 | if err != nil { 320 | return err 321 | } 322 | prop.Required = tag.required 323 | prop.Desc = tag.desc 324 | prop.Default, err = parseValue(tag.defaultVal, field.Type.Kind()) 325 | if err != nil { 326 | return err 327 | } 328 | 329 | sd.Properties[name] = prop 330 | } 331 | } 332 | 333 | dst[ref] = sd 334 | 335 | for k, t := range ensureSchemas { 336 | if err := addSchemaFromType(dst, k, t); err != nil { 337 | return err 338 | } 339 | } 340 | 341 | return nil 342 | } 343 | 344 | // setAPIReqRespBody populates APIReqRespDescriptor with correct values based 345 | // on provided arguments. 346 | // 347 | // Args: 348 | // - d, a non-nil pointer of APIReqRespDescriptor to populate 349 | // - template, either "backendRequest" or "backendResponse" 350 | // - empty, true if the origial method does not have a request/response body. 351 | func setAPIReqRespBody(d *APIReqRespDescriptor, template string, empty bool) { 352 | if empty { 353 | d.Body = "empty" 354 | } else { 355 | d.Body = fmt.Sprintf("autoTemplate(%s)", template) 356 | d.BodyName = "resource" 357 | } 358 | } 359 | 360 | // isBodiless returns true of this is either GET or DELETE 361 | func (info *MethodInfo) isBodiless() bool { 362 | // "OPTIONS" method is not supported anyway. 363 | return info.HTTPMethod == "GET" || info.HTTPMethod == "DELETE" 364 | } 365 | 366 | // --------------------------------------------------------------------------- 367 | // Types 368 | 369 | // VoidMessage represents the fact that a service method does not expect 370 | // anything in a request (or a response). 371 | type VoidMessage struct{} 372 | 373 | type jsonMarshaler interface { 374 | json.Marshaler 375 | json.Unmarshaler 376 | } 377 | 378 | var ( 379 | typeOfTime = reflect.TypeOf(time.Time{}) 380 | typeOfBytes = reflect.TypeOf([]byte(nil)) 381 | typeOfJSONMarshaler = reflect.TypeOf((*jsonMarshaler)(nil)).Elem() 382 | 383 | // SchemaNameForType returns a name for the given schema type, 384 | // used to reference schema definitions in the API descriptor. 385 | // 386 | // Default is to return just the type name, which does not guarantee 387 | // uniqueness if you have identically named structs in different packages. 388 | // 389 | // You can override this function, for instance to prefix all of your schemas 390 | // with a custom name. It should start from an uppercase letter and contain 391 | // only [a-zA-Z0-9]. 392 | SchemaNameForType = func(t reflect.Type) string { 393 | return t.Name() 394 | } 395 | 396 | // Make sure user-supplied version of SchemaNameForType contains only 397 | // allowed characters. The rest will be removed. 398 | reSchemaName = regexp.MustCompile("[^a-zA-Z0-9]") 399 | ) 400 | 401 | // indirectType returns a type the t is pointing to or a type of the element 402 | // of t if t is either Array, Chan, Map or Slice. 403 | func indirectType(t reflect.Type) reflect.Type { 404 | switch t.Kind() { 405 | case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: 406 | return t.Elem() 407 | } 408 | return t 409 | } 410 | 411 | // indirectKind returns kind of a type the t is pointing to. 412 | func indirectKind(t reflect.Type) reflect.Kind { 413 | return indirectType(t).Kind() 414 | } 415 | 416 | // implements returns true if Type t implements interface of Type impl. 417 | func implements(t reflect.Type, impl reflect.Type) bool { 418 | return t.Implements(impl) || indirectType(t).Implements(impl) 419 | } 420 | 421 | // isEmptyStruct returns true if given Type is either not a Struct or 422 | // has 0 fields 423 | func isEmptyStruct(t reflect.Type) bool { 424 | if t.Kind() == reflect.Ptr { 425 | t = t.Elem() 426 | } 427 | // TODO(alex): check for unexported fields? 428 | return t.Kind() == reflect.Struct && t.NumField() == 0 429 | } 430 | 431 | // typeToPropFormat returns a pair of (item type, type format) strings 432 | // for "simple" kinds. 433 | func typeToPropFormat(t reflect.Type) (string, string) { 434 | switch t.Kind() { 435 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: 436 | return "integer", "int32" 437 | case reflect.Int64: 438 | return "string", "int64" 439 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32: 440 | return "integer", "uint32" 441 | case reflect.Uint64: 442 | return "string", "uint64" 443 | case reflect.Float32: 444 | return "number", "float" 445 | case reflect.Float64: 446 | return "number", "double" 447 | case reflect.Bool: 448 | return "boolean", "" 449 | case reflect.String: 450 | return "string", "" 451 | } 452 | 453 | return "", "" 454 | } 455 | 456 | // typeToParamsSpec creates a new APIRequestParamSpec map from a Type for all 457 | // fields in t. 458 | // 459 | // Normally, t is a Struct type and it's what an original service method 460 | // expects as input (request arg). 461 | func typeToParamsSpec(t reflect.Type) ( 462 | map[string]*APIRequestParamSpec, error) { 463 | 464 | if t.Kind() != reflect.Struct { 465 | return nil, fmt.Errorf( 466 | "typeToParamsSpec: Only structs are supported, got: %v", t) 467 | } 468 | 469 | params := make(map[string]*APIRequestParamSpec) 470 | 471 | for name, field := range fieldNames(t, true) { 472 | param, err := fieldToParamSpec(field) 473 | if err != nil { 474 | return nil, err 475 | } 476 | params[name] = param 477 | } 478 | 479 | return params, nil 480 | } 481 | 482 | // typeToParamsSpecFromPath is almost the same as typeToParamsSpec but considers 483 | // only those params present in template path. 484 | // 485 | // path template is is something like "some/{a}/path/{b}". 486 | func typeToParamsSpecFromPath(t reflect.Type, path string) ( 487 | map[string]*APIRequestParamSpec, error) { 488 | 489 | if t.Kind() != reflect.Struct { 490 | return nil, fmt.Errorf( 491 | "typeToParamsSpecFromPath: Only structs are supported, got: %v", t) 492 | } 493 | 494 | pathKeys, err := parsePath(path) 495 | if err != nil { 496 | return nil, err 497 | } 498 | 499 | fieldsMap := fieldNames(t, true) 500 | params := make(map[string]*APIRequestParamSpec) 501 | 502 | for _, k := range pathKeys { 503 | field, found := fieldsMap[k] 504 | if !found { 505 | return nil, fmt.Errorf( 506 | "typeToParamsSpecFromPath: Can't find field %q in %v (from path %q)", 507 | k, t, path) 508 | } 509 | param, err := fieldToParamSpec(field) 510 | if err != nil { 511 | return nil, err 512 | } 513 | param.Required = true 514 | params[k] = param 515 | } 516 | 517 | return params, nil 518 | } 519 | 520 | // fieldToParamSpec creates a APIRequestParamSpec from the given StructField. 521 | // It returns error if the field's kind/type is not supported. 522 | // 523 | // See parseTag() method for supported tag options. 524 | func fieldToParamSpec(field *reflect.StructField) (p *APIRequestParamSpec, err error) { 525 | p = &APIRequestParamSpec{} 526 | kind := field.Type.Kind() 527 | if kind == reflect.Ptr { 528 | kind = indirectType(field.Type).Kind() 529 | 530 | } 531 | switch { 532 | case reflect.Int <= kind && kind <= reflect.Int32: 533 | p.Type = "int32" 534 | case kind == reflect.Int64: 535 | p.Type = "int64" 536 | case reflect.Uint <= kind && kind <= reflect.Uint32: 537 | p.Type = "uint32" 538 | case kind == reflect.Uint64: 539 | p.Type = "uint64" 540 | case kind == reflect.Float32: 541 | p.Type = "float" 542 | case kind == reflect.Float64: 543 | p.Type = "double" 544 | case kind == reflect.Bool: 545 | p.Type = "boolean" 546 | case field.Type == typeOfBytes: 547 | p.Type = "bytes" 548 | case kind == reflect.String, implements(field.Type, typeOfJSONMarshaler): 549 | p.Type = "string" 550 | default: 551 | return nil, fmt.Errorf("Unsupported field: %#v", field) 552 | } 553 | 554 | var tag *endpointsTag 555 | if tag, err = parseTag(field.Tag); err != nil { 556 | return nil, fmt.Errorf("Tag error on %#v: %s", field, err) 557 | } 558 | 559 | p.Required = tag.required 560 | if p.Default, err = parseValue(tag.defaultVal, kind); err != nil { 561 | return 562 | } 563 | if reflect.Int <= kind && kind <= reflect.Uint64 { 564 | p.Min, err = parseValue(tag.minVal, kind) 565 | if err != nil { 566 | return 567 | } 568 | p.Max, err = parseValue(tag.maxVal, kind) 569 | } 570 | 571 | return 572 | } 573 | 574 | // fieldNames loops over each field of t and creates a map of 575 | // fieldName (string) => *StructField where fieldName is extracted from json 576 | // field tag. Defaults to StructField.Name. 577 | // 578 | // It expands (flattens) nexted structs if flatten == true, and always skips 579 | // unexported fields or thosed tagged with json:"-" 580 | // 581 | // This method accepts only reflect.Struct type. Passing other types will 582 | // most likely make it panic. 583 | func fieldNames(t reflect.Type, flatten bool) map[string]*reflect.StructField { 584 | numField := t.NumField() 585 | m := make(map[string]*reflect.StructField, numField) 586 | 587 | for i := 0; i < numField; i++ { 588 | f := t.Field(i) 589 | // consider only exported fields 590 | if f.PkgPath != "" { 591 | continue 592 | } 593 | 594 | name := strings.Split(f.Tag.Get("json"), ",")[0] 595 | if name == "-" { 596 | continue 597 | } else if name == "" { 598 | name = f.Name 599 | } 600 | 601 | if f.Type.Kind() == reflect.Struct && f.Anonymous { 602 | for nname, nfield := range fieldNames(f.Type, flatten) { 603 | m[nname] = nfield 604 | } 605 | continue 606 | } 607 | 608 | if flatten && indirectKind(f.Type) == reflect.Struct && 609 | !implements(f.Type, typeOfJSONMarshaler) { 610 | 611 | for nname, nfield := range fieldNames(indirectType(f.Type), true) { 612 | m[name+"."+nname] = nfield 613 | } 614 | continue 615 | } 616 | 617 | m[name] = &f 618 | } 619 | 620 | return m 621 | } 622 | 623 | // schemaNameForType always returns a title version of the public method 624 | // SchemaNameForType. 625 | func schemaNameForType(t reflect.Type) string { 626 | name := strings.Title(SchemaNameForType(t)) 627 | return reSchemaName.ReplaceAllLiteralString(name, "") 628 | } 629 | 630 | // ---------------------------------------------------------------------------- 631 | // Parse 632 | 633 | type endpointsTag struct { 634 | required bool 635 | defaultVal, minVal, maxVal string 636 | desc string 637 | } 638 | 639 | const endpointsTagName = "endpoints" 640 | 641 | // parseTag parses "endpoints" field tag into endpointsTag struct. 642 | // 643 | // type MyMessage struct { 644 | // SomeField int `endpoints:"req,min=0,max=100,desc="Int field"` 645 | // WithDefault string `endpoints:"d=Hello gopher"` 646 | // } 647 | // 648 | // - req, required (boolean) 649 | // - d=val, default value 650 | // - min=val, min value 651 | // - max=val, max value 652 | // - desc=val, description 653 | // 654 | // It is an error to specify both default and required. 655 | func parseTag(t reflect.StructTag) (*endpointsTag, error) { 656 | eTag := &endpointsTag{} 657 | if tag := t.Get("endpoints"); tag != "" { 658 | parts := strings.Split(tag, ",") 659 | for _, k := range parts { 660 | switch k { 661 | case "req": 662 | eTag.required = true 663 | default: 664 | // key=value format 665 | kv := strings.SplitN(k, "=", 2) 666 | if len(kv) < 2 { 667 | continue 668 | } 669 | switch kv[0] { 670 | case "d": 671 | eTag.defaultVal = kv[1] 672 | case "min": 673 | eTag.minVal = kv[1] 674 | case "max": 675 | eTag.maxVal = kv[1] 676 | case "desc": 677 | eTag.desc = kv[1] 678 | } 679 | } 680 | } 681 | if eTag.required && eTag.defaultVal != "" { 682 | return nil, fmt.Errorf( 683 | "Can't have both required and default (%#v)", 684 | eTag.defaultVal) 685 | } 686 | } 687 | return eTag, nil 688 | } 689 | 690 | // parsePath parses a path template and returns found placeholders. 691 | // It returns error if the template is malformed. 692 | // 693 | // For instance, parsePath("one/{a}/two/{b}") will return []string{"a","b"}. 694 | func parsePath(path string) ([]string, error) { 695 | var params []string 696 | for { 697 | i := strings.IndexRune(path, '{') 698 | if i < 0 { 699 | break 700 | } 701 | x := strings.IndexRune(path, '}') 702 | if x < i+1 { 703 | return nil, fmt.Errorf("parsePath: Invalid path template: %q", path) 704 | } 705 | params = append(params, path[i+1:x]) 706 | path = path[x+1:] 707 | } 708 | return params, nil 709 | } 710 | 711 | // parseValue parses string s into its "real" value of kind k. 712 | // Only these kinds are supported: (u)int8/16/32/64, float32/64, bool, string. 713 | func parseValue(s string, k reflect.Kind) (interface{}, error) { 714 | if s == "" { 715 | return nil, nil 716 | } 717 | 718 | switch { 719 | case reflect.Int <= k && k <= reflect.Int32: 720 | return strconv.Atoi(s) 721 | case k == reflect.Int64: 722 | return strconv.ParseInt(s, 0, 64) 723 | case reflect.Uint <= k && k <= reflect.Uint32: 724 | v, err := strconv.ParseUint(s, 0, 32) 725 | if err != nil { 726 | return nil, err 727 | } 728 | return uint32(v), nil 729 | case k == reflect.Uint64: 730 | return strconv.ParseUint(s, 0, 64) 731 | case k == reflect.Float32: 732 | v, err := strconv.ParseFloat(s, 32) 733 | if err != nil { 734 | return nil, err 735 | } 736 | return float32(v), nil 737 | case k == reflect.Float64: 738 | return strconv.ParseFloat(s, 64) 739 | case k == reflect.Bool: 740 | return strconv.ParseBool(s) 741 | case k == reflect.String: 742 | return s, nil 743 | } 744 | 745 | return nil, fmt.Errorf("parseValue: Invalid kind %#v value=%q", k, s) 746 | } 747 | 748 | func validateRequest(r interface{}) error { 749 | v := reflect.ValueOf(r) 750 | if v.Kind() != reflect.Ptr { 751 | return fmt.Errorf("%T is not a pointer", r) 752 | } 753 | v = reflect.Indirect(v) 754 | if v.Kind() != reflect.Struct { 755 | return fmt.Errorf("%T is not a pointer to a struct", r) 756 | } 757 | 758 | t := v.Type() 759 | for i := 0; i < v.NumField(); i++ { 760 | if err := validateField(v.Field(i), t.Field(i)); err != nil { 761 | return err 762 | } 763 | } 764 | return nil 765 | } 766 | 767 | func validateField(v reflect.Value, t reflect.StructField) error { 768 | // only validate simple types, ignore arrays, slices, chans, etc. 769 | if v.Kind() > reflect.Float64 && v.Kind() != reflect.String { 770 | return nil 771 | } 772 | 773 | tag, err := parseTag(t.Tag) 774 | if err != nil { 775 | return fmt.Errorf("parse tag: %v", err) 776 | } 777 | 778 | isZero := v.Interface() == reflect.Zero(v.Type()).Interface() 779 | if isZero && tag.required { 780 | return fmt.Errorf("missing field %v", t.Name) 781 | } 782 | 783 | if isZero && tag.defaultVal != "" { 784 | r, err := parseValue(tag.defaultVal, v.Kind()) 785 | if err != nil { 786 | return fmt.Errorf("parse default value: %v", err) 787 | } 788 | v.Set(reflect.ValueOf(r)) 789 | } 790 | 791 | if tag.minVal != "" { 792 | cmp, err := compare(v, tag.minVal) 793 | if err != nil { 794 | return fmt.Errorf("compare with min value: %v", err) 795 | } 796 | if cmp < 0 { 797 | return fmt.Errorf("%v is too small", v) 798 | } 799 | } 800 | 801 | if tag.maxVal != "" { 802 | cmp, err := compare(v, tag.maxVal) 803 | if err != nil { 804 | return fmt.Errorf("compare with min value: %v", err) 805 | } 806 | if cmp > 0 { 807 | return fmt.Errorf("%v is too big", v) 808 | } 809 | } 810 | return nil 811 | } 812 | 813 | // compare parses the given text to a value of the same type of a and 814 | // compares them. It returns -1 if a < b, 1 if a > b, or 0 if a == b. 815 | func compare(a reflect.Value, text string) (int, error) { 816 | val, err := parseValue(text, a.Kind()) 817 | if err != nil { 818 | return 0, fmt.Errorf("parse min value: %v", err) 819 | } 820 | b := reflect.ValueOf(val) 821 | cmp := 0 822 | switch a.Interface().(type) { 823 | case int, int8, int16, int32, int64: 824 | if a, b := a.Int(), b.Int(); a < b { 825 | cmp = -1 826 | } else if a > b { 827 | cmp = 1 828 | } 829 | case uint, uint8, uint16, uint32, uint64: 830 | if a, b := a.Uint(), b.Uint(); a < b { 831 | cmp = -1 832 | } else if a > b { 833 | cmp = 1 834 | } 835 | case float32, float64: 836 | if a, b := a.Float(), b.Float(); a < b { 837 | cmp = -1 838 | } else if a > b { 839 | cmp = 1 840 | } 841 | case string: 842 | if a, b := a.String(), b.String(); a < b { 843 | cmp = -1 844 | } else if a > b { 845 | cmp = 1 846 | } 847 | default: 848 | return 0, fmt.Errorf("unsupported type %v", a.Type()) 849 | } 850 | return cmp, nil 851 | } 852 | --------------------------------------------------------------------------------