From bfecd5a06f3ade53fc51d478133836ffb86d50b7 Mon Sep 17 00:00:00 2001 From: Rob De Feo Date: Sun, 28 Jul 2019 20:03:26 -0700 Subject: [PATCH] resolve name handler --- .../internal/http/handlers/resolve.go | 153 +++++++++ .../internal/http/handlers/resolve_test.go | 293 ++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 cmd/mailchain/internal/http/handlers/resolve.go create mode 100644 cmd/mailchain/internal/http/handlers/resolve_test.go diff --git a/cmd/mailchain/internal/http/handlers/resolve.go b/cmd/mailchain/internal/http/handlers/resolve.go new file mode 100644 index 000000000..08d72195d --- /dev/null +++ b/cmd/mailchain/internal/http/handlers/resolve.go @@ -0,0 +1,153 @@ +// Copyright 2019 Finobo +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/gorilla/mux" + "github.com/mailchain/mailchain/cmd/mailchain/internal/http/params" + "github.com/mailchain/mailchain/errs" + "github.com/mailchain/mailchain/internal/mailbox" + "github.com/mailchain/mailchain/nameservice" + "github.com/pkg/errors" +) + +// api/resolver/ethereum/mainnet +// api/ethereum/mainnet/resolve +// api/ethereum/mainnet/forward-lookup +// api/ethereum/mainnet/address/forward-lookup +// api/ethereum/mainnet/address/reverse-lookup +// api/ethereum/mainnet/address/resolve +// api/ethereum/mainnet/address/reverse + +// GetResolveName returns a handler get spec +func GetResolveName(resolvers map[string]nameservice.ForwardLookup) func(w http.ResponseWriter, r *http.Request) { + // Get swagger:route GET /nameservice/name/{domain-name}/resolve ResolveName NameService GetResolveName + // + // Get public key from an address. + // + // Get the public key. + // + // Responses: + // 200: GetResolveNameResponse + // 404: NotFoundError + // 422: ValidationError + return func(w http.ResponseWriter, hr *http.Request) { + ctx := hr.Context() + protocol, network, domainName, err := parseGetResolveNameRequest(hr) + if err != nil { + errs.JSONWriter(w, http.StatusUnprocessableEntity, errors.WithStack(err)) + return + } + resolver, ok := resolvers[fmt.Sprintf("%s/%s", protocol, network)] + if !ok { + errs.JSONWriter(w, http.StatusUnprocessableEntity, errors.Errorf("no name servier resolver for chain.network configured")) + return + } + + address, err := resolver.ResolveName(ctx, protocol, network, domainName) + if mailbox.IsNetworkNotSupportedError(err) { + errs.JSONWriter(w, http.StatusNotAcceptable, errors.Errorf("%q not supported", protocol+"/"+network)) + return + } + if nameservice.IsInvalidNameError(err) { + errs.JSONWriter(w, http.StatusPreconditionFailed, err) + return + } + if nameservice.IsNoResolverError(err) { + errs.JSONWriter(w, http.StatusNotFound, err) + return + } + if nameservice.IsNotFoundError(err) { + errs.JSONWriter(w, http.StatusNotFound, err) + return + } + if err != nil { + errs.JSONWriter(w, http.StatusInternalServerError, errors.WithStack(err)) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(GetResolveNameResponseBody{ + Address: hexutil.Encode(address), + }) + } +} + +// GetResolveNameRequest pubic key from address request +// swagger:parameters GetResolveName +type GetResolveNameRequest struct { + // name to query to get address for + // + // in: path + // required: true + // example: name.ens + Name string `json:"domain-name"` + + // Network for the name to resolve + // + // enum: mainnet,ropsten,rinkeby,local + // in: path + // required: true + // example: ropsten + Network string `json:"network"` + + // Protocol for the name to resolve + // + // enum: ethereum + // in: path + // required: true + // example: ethereum + Protocol string `json:"protocol"` +} + +// parseGetResolveNameRequest get all the details for the get request +func parseGetResolveNameRequest(r *http.Request) (protocol, network, domain string, err error) { + protocol, err = params.QueryRequireProtocol(r) + if err != nil { + return "", "", "", err + } + network, err = params.QueryRequireNetwork(r) + if err != nil { + return "", "", "", err + } + domain = strings.ToLower(mux.Vars(r)["domain-name"]) + + return protocol, network, domain, nil +} + +// GetResolveNameResponse address of resolved name +// +// swagger:response GetResolveNameResponse +type GetResolveNameResponse struct { + // in: body + Body GetPublicKeyResponseBody +} + +// GetBody body response +// +// swagger:model GetResolveNameResponseBody +type GetResolveNameResponseBody struct { + // The public key + // + // Required: true + // example: 0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae + Address string `json:"address"` +} diff --git a/cmd/mailchain/internal/http/handlers/resolve_test.go b/cmd/mailchain/internal/http/handlers/resolve_test.go new file mode 100644 index 000000000..d4d277882 --- /dev/null +++ b/cmd/mailchain/internal/http/handlers/resolve_test.go @@ -0,0 +1,293 @@ +// Copyright 2019 Finobo +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handlers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + "github.com/gorilla/mux" + "github.com/mailchain/mailchain/nameservice" + "github.com/mailchain/mailchain/nameservice/nameservicetest" + "github.com/mailchain/mailchain/internal/testutil" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func Test_parseGetResolveNameRequest(t *testing.T) { + type args struct { + r *http.Request + } + tests := []struct { + name string + args args + wantProtocol string + wantNetwork string + wantDomain string + wantErr bool + }{ + { + "success", + args{ + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "address.ens", + }) + return req + }(), + }, + "ethereum", + "mainnet", + "address.ens", + false, + }, + { + "err-protocol", + args{ + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "address.ens", + }) + return req + }(), + }, + "", + "", + "", + true, + }, + { + "err-network", + args{ + func() *http.Request { + req := httptest.NewRequest("GET", "/?protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "address.ens", + }) + return req + }(), + }, + "", + "", + "", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotProtocol, gotNetwork, gotDomain, err := parseGetResolveNameRequest(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("parseGetResolveNameRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotProtocol != tt.wantProtocol { + t.Errorf("parseGetResolveNameRequest() gotProtocol = %v, want %v", gotProtocol, tt.wantProtocol) + } + if gotNetwork != tt.wantNetwork { + t.Errorf("parseGetResolveNameRequest() gotNetwork = %v, want %v", gotNetwork, tt.wantNetwork) + } + if gotDomain != tt.wantDomain { + t.Errorf("parseGetResolveNameRequest() gotDomain = %v, want %v", gotDomain, tt.wantDomain) + } + }) + } +} + +func TestGetResolveName(t *testing.T) { + assert := assert.New(t) + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + type args struct { + resolvers map[string]nameservice.ForwardLookup + } + tests := []struct { + name string + args args + req *http.Request + wantBody string + wantStatus int + }{ + { + "err-invalid-request", + args{ + nil, + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/", nil) + req = mux.SetURLVars(req, map[string]string{}) + return req + }(), + "{\"code\":422,\"message\":\"protocol must be specified exactly once\"}\n", + http.StatusUnprocessableEntity, + }, + { + "no-network-finder", + args{ + func() map[string]nameservice.ForwardLookup { + m := nameservicetest.NewMockForwardLookup(mockCtrl) + return map[string]nameservice.ForwardLookup{"ethereum.no-network": m} + }(), + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "name.ens", + }) + return req + }(), + "{\"code\":422,\"message\":\"no name servier resolver for chain.network configured\"}\n", + http.StatusUnprocessableEntity, + }, + { + "networkNotSupportedError", + args{ + func() map[string]nameservice.ForwardLookup { + m := nameservicetest.NewMockForwardLookup(mockCtrl) + m.EXPECT().ResolveName(gomock.Any(), "ethereum", "mainnet", "name.ens").Return(nil, errors.New("network not supported")).Times(1) + return map[string]nameservice.ForwardLookup{"ethereum/mainnet": m} + }(), + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "name.ens", + }) + return req + }(), + "{\"code\":406,\"message\":\"\\\"ethereum/mainnet\\\" not supported\"}\n", + http.StatusNotAcceptable, + }, + { + "err-resolve-name", + args{ + func() map[string]nameservice.ForwardLookup { + m := nameservicetest.NewMockForwardLookup(mockCtrl) + m.EXPECT().ResolveName(gomock.Any(), "ethereum", "mainnet", "name.ens").Return(nil, errors.New("error")).Times(1) + return map[string]nameservice.ForwardLookup{"ethereum/mainnet": m} + }(), + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "name.ens", + }) + return req + }(), + "{\"code\":500,\"message\":\"error\"}\n", + http.StatusInternalServerError, + }, + { + "err-not-found", + args{ + func() map[string]nameservice.ForwardLookup { + m := nameservicetest.NewMockForwardLookup(mockCtrl) + m.EXPECT().ResolveName(gomock.Any(), "ethereum", "mainnet", "name.ens").Return(nil, nameservice.ErrNotFound).Times(1) + return map[string]nameservice.ForwardLookup{"ethereum/mainnet": m} + }(), + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "name.ens", + }) + return req + }(), + "{\"code\":404,\"message\":\"not found\"}\n", + http.StatusNotFound, + }, + { + "err-invalid-name", + args{ + func() map[string]nameservice.ForwardLookup { + m := nameservicetest.NewMockForwardLookup(mockCtrl) + m.EXPECT().ResolveName(gomock.Any(), "ethereum", "mainnet", "name.ens").Return(nil, nameservice.ErrInvalidName).Times(1) + return map[string]nameservice.ForwardLookup{"ethereum/mainnet": m} + }(), + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "name.ens", + }) + return req + }(), + "{\"code\":412,\"message\":\"invalid name\"}\n", + http.StatusPreconditionFailed, + }, + { + "err-unable-to-resolve", + args{ + func() map[string]nameservice.ForwardLookup { + m := nameservicetest.NewMockForwardLookup(mockCtrl) + m.EXPECT().ResolveName(gomock.Any(), "ethereum", "mainnet", "name.ens").Return(nil, nameservice.ErrUnableToResolve).Times(1) + return map[string]nameservice.ForwardLookup{"ethereum/mainnet": m} + }(), + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "name.ens", + }) + return req + }(), + "{\"code\":404,\"message\":\"unable to resolve\"}\n", + http.StatusNotFound, + }, + { + "success", + args{ + func() map[string]nameservice.ForwardLookup { + m := nameservicetest.NewMockForwardLookup(mockCtrl) + m.EXPECT().ResolveName(gomock.Any(), "ethereum", "mainnet", "name.ens").Return(testutil.MustHexDecodeString("5602ea95540bee46d03ba335eed6f49d117eab95c8ab8b71bae2cdd1e564a761"), nil).Times(1) + return map[string]nameservice.ForwardLookup{"ethereum/mainnet": m} + }(), + }, + func() *http.Request { + req := httptest.NewRequest("GET", "/?network=mainnet&protocol=ethereum", nil) + req = mux.SetURLVars(req, map[string]string{ + "domain-name": "name.ens", + }) + return req + }(), + "{\"address\":\"0x5602ea95540bee46d03ba335eed6f49d117eab95c8ab8b71bae2cdd1e564a761\"}\n", + http.StatusOK, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetResolveName(tt.args.resolvers)) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, tt.req) + + // Check the status code is what we expect. + if !assert.Equal(tt.wantStatus, rr.Code) { + t.Errorf("handler returned wrong status code: got %v want %v", + rr.Code, tt.wantStatus) + } + if !assert.Equal(tt.wantBody, rr.Body.String()) { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), tt.wantBody) + } + }) + } +}