diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3b95c9b..c5e0afb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,7 +71,7 @@ jobs: - name: Run linter uses: golangci/golangci-lint-action@v2 with: - version: v1.37.0 + version: v1.37.1 docker: name: Docker diff --git a/Makefile b/Makefile index 600c557..8e70a73 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ IMAGE_NAME := $(APP_NAME) CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}") -GOLANGCI_LINT_VERSION := v1.37.0 +GOLANGCI_LINT_VERSION := v1.37.1 VERSION_GO := $(shell go version) VERSION_DATE := $(shell date -Ru) diff --git a/go.mod b/go.mod index 11851ad..e81c3ac 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/EvilSuperstars/go-cidrman v0.0.0-20190607145828-28e79e32899a github.com/antchfx/htmlquery v1.2.3 github.com/antchfx/xpath v1.1.11 // indirect - github.com/antzucaro/matchr v0.0.0-20191224151129-ab6ba461ddec + github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0 github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/dgraph-io/ristretto v0.0.3 github.com/hjson/hjson-go v3.1.0+incompatible @@ -22,9 +22,9 @@ require ( github.com/qri-io/jsonschema v0.2.0 github.com/rs/zerolog v1.20.0 github.com/stretchr/testify v1.6.1 - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect - golang.org/x/sys v0.0.0-20210218085108-9555bcde0c6a // indirect + golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 // indirect + golang.org/x/sys v0.0.0-20210223212115-eede4237b368 // indirect golang.org/x/text v0.3.5 // indirect - golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 915df93..b8e163d 100644 --- a/go.sum +++ b/go.sum @@ -4,20 +4,17 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M= github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0= -github.com/antchfx/xpath v1.1.6 h1:6sVh6hB5T6phw1pFpHRQ+C4bd8sNI+O58flqtg7h0R0= github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= github.com/antchfx/xpath v1.1.11 h1:WOFtK8TVAjLm3lbgqeP0arlHpvCEeTANeWZ/csPpJkQ= github.com/antchfx/xpath v1.1.11/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= -github.com/antzucaro/matchr v0.0.0-20191224151129-ab6ba461ddec h1:uurd2LiNfcarvGB05LUzvGzPoNr5eRgC92WwdXoK7Qs= -github.com/antzucaro/matchr v0.0.0-20191224151129-ab6ba461ddec/go.mod h1:v3ZDlfVAL1OrkKHbGSFFK60k0/7hruHPDq2XMs9Gu6U= -github.com/cenkalti/backoff/v3 v3.1.1 h1:UBHElAnr3ODEbpqPzX8g5sBcASjoLFtt3L/xwJ01L6E= +github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0 h1:R/qAiUxFT3mNgQaNqJe0IVznjKRNm23ohAIh9lgtlzc= +github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0/go.mod h1:v3ZDlfVAL1OrkKHbGSFFK60k0/7hruHPDq2XMs9Gu6U= github.com/cenkalti/backoff/v3 v3.1.1/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -72,29 +69,24 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210218085108-9555bcde0c6a h1:+Kiu2GijIw0WaCBk1i7AcqqRx8Xg3HIYaheQazXOu8w= -golang.org/x/sys v0.0.0-20210218085108-9555bcde0c6a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210223212115-eede4237b368 h1:fDE3p0qf2V1co1vfj3/o87Ps8Hq6QTGNxJ5Xe7xSp80= +golang.org/x/sys v0.0.0-20210223212115-eede4237b368/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 h1:4cFkmztxtMslUX2SctSl+blCyXfpzhGOy9LhKAqSMA4= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/openapi.yaml b/openapi.yaml index 48141da..b2c814c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -89,6 +89,38 @@ paths: application/json: schema: $ref: "#/components/schemas/ResponseError" + + /{ip}: + get: + description: Resolve a single IP address + operationId: getIP + parameters: + - in: path + name: ip + required: true + description: IP address to resolve + schema: + $ref: "#/components/schemas/IP" + responses: + "200": + description: Results of resolved geolocation + content: + application/json: + schema: + type: object + required: + - result + additionalProperties: false + properties: + result: + $ref: "#/components/schemas/GeolocationResult" + default: + description: Error response in case if geolocation is impossible + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseError" + /stats: get: description: Different statistics on provider usage diff --git a/topolib/http.go b/topolib/http.go index fde2a6a..864e088 100644 --- a/topolib/http.go +++ b/topolib/http.go @@ -2,6 +2,7 @@ package topolib import ( "encoding/json" + "net" "net/http" "strings" ) @@ -11,17 +12,21 @@ type httpHandler struct { } func (h httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - path := strings.TrimSuffix(req.URL.Path, "/") + path := strings.Trim(req.URL.Path, "/") switch req.Method { case http.MethodGet, http.MethodHead: switch path { case "": h.handleGetResolve(w, req) - case "/stats": + case "stats": h.handleGetStats(w, req) default: - h.sendError(w, nil, "URL not found", http.StatusNotFound) + if ipAddr := net.ParseIP(path); ipAddr != nil { + h.handleGetIP(w, req, ipAddr) + } else { + h.sendError(w, nil, "URL not found", http.StatusNotFound) + } } case http.MethodPost: switch path { diff --git a/topolib/http_handler_get.go b/topolib/http_handler_get.go index 2654dee..bbd33cd 100644 --- a/topolib/http_handler_get.go +++ b/topolib/http_handler_get.go @@ -20,6 +20,10 @@ func (h httpHandler) handleGetResolve(w http.ResponseWriter, req *http.Request) return } + h.handleGetIP(w, req, ipAddr) +} + +func (h httpHandler) handleGetIP(w http.ResponseWriter, req *http.Request, ipAddr net.IP) { resolved, err := h.topo.Resolve(req.Context(), ipAddr, nil) if err != nil { h.sendError(w, err, "Cannot resolve IP address", 0) diff --git a/topolib/http_handler_test.go b/topolib/http_handler_test.go index cee7224..e3dc4d3 100644 --- a/topolib/http_handler_test.go +++ b/topolib/http_handler_test.go @@ -331,7 +331,7 @@ var ( }() ) -type HTTPHanderTestSuite struct { +type HTTPHandlerTestSuite struct { suite.Suite h http.Handler @@ -340,7 +340,7 @@ type HTTPHanderTestSuite struct { resp *httptest.ResponseRecorder } -func (suite *HTTPHanderTestSuite) SetupTest() { +func (suite *HTTPHandlerTestSuite) SetupTest() { suite.providerMock = &ProviderMock{} suite.loggerMock = &LoggerMock{} @@ -360,18 +360,43 @@ func (suite *HTTPHanderTestSuite) SetupTest() { suite.resp = httptest.NewRecorder() } -func (suite *HTTPHanderTestSuite) TearDownTest() { +func (suite *HTTPHandlerTestSuite) TearDownTest() { suite.providerMock.AssertExpectations(suite.T()) suite.loggerMock.AssertExpectations(suite.T()) } -func (suite *HTTPHanderTestSuite) TestIncorrectMethod() { +func (suite *HTTPHandlerTestSuite) TestIncorrectMethod() { suite.h.ServeHTTP(suite.resp, httptest.NewRequest("PATCH", "/", nil)) suite.Equal(http.StatusMethodNotAllowed, suite.resp.Code) } -func (suite *HTTPHanderTestSuite) TestGetOk() { +func (suite *HTTPHandlerTestSuite) TestGetIPOk() { + result := topolib.ProviderLookupResult{ + CountryCode: topolib.Alpha2ToCountryCode("RU"), + City: "Nizhniy Novgorod", + } + ip := net.ParseIP("192.168.1.1").To16() + req := httptest.NewRequest("GET", "/192.168.1.1/", nil) + + suite.providerMock.On("Lookup", mock.Anything, ip).Return(result, nil).Once() + + suite.h.ServeHTTP(suite.resp, req) + + suite.Equal(http.StatusOK, suite.resp.Code) + + errs, err := jsonSchemaGETResolve.ValidateBytes(context.Background(), + suite.resp.Body.Bytes()) + + suite.NoError(err) + suite.Empty(errs) + suite.Contains(suite.resp.Body.String(), "192.168.1.1") + suite.Contains(suite.resp.Body.String(), "RU") + suite.Contains(suite.resp.Body.String(), "RUS") + suite.Contains(suite.resp.Body.String(), "Nizhniy Novgorod") +} + +func (suite *HTTPHandlerTestSuite) TestGetOk() { result := topolib.ProviderLookupResult{ CountryCode: topolib.Alpha2ToCountryCode("RU"), City: "Nizhniy Novgorod", @@ -397,7 +422,7 @@ func (suite *HTTPHanderTestSuite) TestGetOk() { suite.Contains(suite.resp.Body.String(), "Nizhniy Novgorod") } -func (suite *HTTPHanderTestSuite) TestGetUnkownPath() { +func (suite *HTTPHandlerTestSuite) TestGetUnkownPath() { req := httptest.NewRequest("GET", "/lalala", nil) req.RemoteAddr = "192.168.1.1:5678" @@ -406,7 +431,7 @@ func (suite *HTTPHanderTestSuite) TestGetUnkownPath() { suite.Equal(http.StatusNotFound, suite.resp.Code) } -func (suite *HTTPHanderTestSuite) TestGetStats() { +func (suite *HTTPHandlerTestSuite) TestGetStats() { req := httptest.NewRequest("GET", "/stats/", nil) req.RemoteAddr = "192.168.1.1:5678" @@ -421,7 +446,7 @@ func (suite *HTTPHanderTestSuite) TestGetStats() { suite.Empty(errs) } -func (suite *HTTPHanderTestSuite) TestPostUnknownPath() { +func (suite *HTTPHandlerTestSuite) TestPostUnknownPath() { req := httptest.NewRequest("POST", "/lalala", nil) req.RemoteAddr = "192.168.1.1:5678" @@ -430,7 +455,7 @@ func (suite *HTTPHanderTestSuite) TestPostUnknownPath() { suite.Equal(http.StatusNotFound, suite.resp.Code) } -func (suite *HTTPHanderTestSuite) TestPostUnsupportedMediaType() { +func (suite *HTTPHandlerTestSuite) TestPostUnsupportedMediaType() { req := httptest.NewRequest("POST", "/", strings.NewReader("{}")) suite.h.ServeHTTP(suite.resp, req) @@ -438,7 +463,7 @@ func (suite *HTTPHanderTestSuite) TestPostUnsupportedMediaType() { suite.Equal(http.StatusUnsupportedMediaType, suite.resp.Code) } -func (suite *HTTPHanderTestSuite) TestPostBadRequest() { +func (suite *HTTPHandlerTestSuite) TestPostBadRequest() { req := httptest.NewRequest("POST", "/", strings.NewReader("{}")) req.Header.Add("Content-Type", "application/json") @@ -447,7 +472,7 @@ func (suite *HTTPHanderTestSuite) TestPostBadRequest() { suite.Equal(http.StatusBadRequest, suite.resp.Code) } -func (suite *HTTPHanderTestSuite) TestPostOk() { +func (suite *HTTPHandlerTestSuite) TestPostOk() { req := httptest.NewRequest("POST", "/", strings.NewReader(`{"ips": ["192.168.1.1"]}`)) @@ -476,5 +501,5 @@ func (suite *HTTPHanderTestSuite) TestPostOk() { } func TestHTTPHandler(t *testing.T) { - suite.Run(t, &HTTPHanderTestSuite{}) + suite.Run(t, &HTTPHandlerTestSuite{}) }