├── .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 |
28 |
Gretings
29 |
30 |
You can explore the endpoints API on the
31 | API explorer
32 |
33 |
34 |
35 | -
36 | {{g.author}}: {{g.content}}
37 |
38 |
39 |
40 |
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 |
--------------------------------------------------------------------------------