Skip to content

Commit

Permalink
Refactoring (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
vokomarov committed Jan 6, 2024
2 parents 3f9d5a8 + d6f66d4 commit 0cd874e
Show file tree
Hide file tree
Showing 41 changed files with 1,678 additions and 942 deletions.
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ IMAGE_RELEASE=$(REPO):$(RELEASE_VERSION)
IMAGE_DEV=$(REPO):dev
IMAGE_LATEST=$(REPO):latest

.PHONY: run test build tag push start stop
.PHONY: run test build tag push start stop mock-gen

run:
go run -race main.go
Expand Down Expand Up @@ -42,3 +42,11 @@ start:

stop:
docker stop $(CONTAINER_NAME)

mock-gen:
go install go.uber.org/mock/mockgen@latest
mockgen -source=http/client.go -package=mocks -destination=mocks/http_client_mock.go -mock_names=Client=HttpClientMock
mockgen -source=captcha/provider.go -package=mocks -destination=mocks/captcha_provider_mock.go -mock_names=Provider=CaptchaProviderMock
mockgen -source=service/api/service.go -package=mocks -destination=mocks/api_service_mock.go -mock_names=Service=ApiServiceMock
mockgen -source=router/api/handler.go -package=mocks -destination=mocks/api_handler_mock.go -mock_names=Handler=ApiHandlerMock

60 changes: 28 additions & 32 deletions router/captcha/verify.go → captcha/google_recaptcha.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ import (

"github.com/cash-track/gateway/config"
"github.com/cash-track/gateway/headers"
api "github.com/cash-track/gateway/router/api/client"
"github.com/cash-track/gateway/http"
)

const verifyUrl = "https://www.google.com/recaptcha/api/siteverify"
const (
googleApiReCaptchaVerifyUrl = "https://www.google.com/recaptcha/api/siteverify"
googleApiReadTimeout = 500 * time.Millisecond
googleApiWriteTimeout = time.Second
)

var client api.Client
type GoogleReCaptchaProvider struct {
client http.Client
secret string
}

type VerifyResponse struct {
type googleReCaptchaVerifyResponse struct {
Success bool `json:"success"`
ChallengeTS string `json:"challenge_ts,omitempty"`
Hostname string `json:"hostname,omitempty"`
Expand All @@ -27,31 +34,20 @@ type VerifyResponse struct {
ErrorCodes []string `json:"error-codes,omitempty"`
}

func init() {
newClient()
}

func newClient() {
if client != nil {
return
}
func NewGoogleReCaptchaProvider(httpClient http.Client, options config.Config) *GoogleReCaptchaProvider {
httpClient.WithReadTimeout(googleApiReadTimeout)
httpClient.WithWriteTimeout(googleApiWriteTimeout)

client = &fasthttp.Client{
ReadTimeout: 500 * time.Millisecond,
WriteTimeout: time.Second,
MaxIdleConnDuration: time.Hour,
NoDefaultUserAgentHeader: true,
Dial: (&fasthttp.TCPDialer{
Concurrency: 4096,
DNSCacheDuration: time.Hour,
}).Dial,
return &GoogleReCaptchaProvider{
client: httpClient,
secret: options.CaptchaSecret,
}
}

func Verify(ctx *fasthttp.RequestCtx) (bool, error) {
func (p *GoogleReCaptchaProvider) Verify(ctx *fasthttp.RequestCtx) (bool, error) {
clientIp := headers.GetClientIPFromContext(ctx)

if config.Global.CaptchaSecret == "" {
if p.secret == "" {
log.Printf("[%s] captcha secret empty, skipping verify", clientIp)
return true, nil
}
Expand All @@ -69,19 +65,19 @@ func Verify(ctx *fasthttp.RequestCtx) (bool, error) {
fasthttp.ReleaseResponse(resp)
}()

prepareGoogleReCaptchaVerifyRequest(req, challenge, clientIp)
p.buildReq(req, challenge, clientIp)

if err := client.Do(req, resp); err != nil {
if err := p.client.Do(req, resp); err != nil {
return false, fmt.Errorf("captcha verify request error: %w", err)
}

verifyResponse := VerifyResponse{}
if err := json.Unmarshal(resp.Body(), &verifyResponse); err != nil {
verifyResp := googleReCaptchaVerifyResponse{}
if err := json.Unmarshal(resp.Body(), &verifyResp); err != nil {
return false, fmt.Errorf("captcha verify response unexpected: %w", err)
}

if !verifyResponse.Success {
log.Printf("[%s] captcha verify unsuccessfull: score %f, errors: %s", clientIp, verifyResponse.Score, strings.Join(verifyResponse.ErrorCodes, ", "))
if !verifyResp.Success {
log.Printf("[%s] captcha verify unsuccessfull: score %f, errors: %s", clientIp, verifyResp.Score, strings.Join(verifyResp.ErrorCodes, ", "))
return false, nil
}

Expand All @@ -90,11 +86,11 @@ func Verify(ctx *fasthttp.RequestCtx) (bool, error) {
return true, nil
}

func prepareGoogleReCaptchaVerifyRequest(req *fasthttp.Request, challenge []byte, clientIp string) {
req.SetRequestURI(verifyUrl)
func (p *GoogleReCaptchaProvider) buildReq(req *fasthttp.Request, challenge []byte, clientIp string) {
req.SetRequestURI(googleApiReCaptchaVerifyUrl)
req.Header.SetMethod(fasthttp.MethodPost)
req.Header.SetContentTypeBytes(headers.ContentTypeForm)
req.PostArgs().Set("secret", config.Global.CaptchaSecret)
req.PostArgs().Set("secret", p.secret)
req.PostArgs().Set("remoteip", clientIp)
req.PostArgs().SetBytesV("response", challenge)
}
169 changes: 169 additions & 0 deletions captcha/google_recaptcha_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package captcha

import (
"fmt"
"net"
"testing"

"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
"go.uber.org/mock/gomock"

"github.com/cash-track/gateway/config"
"github.com/cash-track/gateway/headers"
"github.com/cash-track/gateway/mocks"
)

func TestVerify(t *testing.T) {
ctrl := gomock.NewController(t)
c := mocks.NewHttpClientMock(ctrl)

ctx := fasthttp.RequestCtx{}
ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}})
ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2")

c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout))
c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout))
c.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error {
resp.SetStatusCode(fasthttp.StatusOK)
resp.SetBodyString(`{"success":true,"score":0.99,"error-codes":["no-error"]}`)

assert.NotNil(t, req)
assert.Equal(t, fasthttp.MethodPost, string(req.Header.Method()))
assert.Equal(t, googleApiReCaptchaVerifyUrl, req.URI().String())
assert.Equal(t, string(headers.ContentTypeForm), string(req.Header.ContentType()))
assert.Equal(t, "captcha_secret_1", string(req.PostArgs().Peek("secret")))
assert.Equal(t, "10.0.0.1", string(req.PostArgs().Peek("remoteip")))
assert.Equal(t, "captcha_challenge_2", string(req.PostArgs().Peek("response")))

return nil
})

p := NewGoogleReCaptchaProvider(c, config.Config{
CaptchaSecret: "captcha_secret_1",
})
state, err := p.Verify(&ctx)

assert.True(t, state)
assert.NoError(t, err)
}

func TestVerifyUnsuccessful(t *testing.T) {
ctrl := gomock.NewController(t)
c := mocks.NewHttpClientMock(ctrl)

ctx := fasthttp.RequestCtx{}
ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}})
ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2")

c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout))
c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout))
c.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error {
resp.SetStatusCode(fasthttp.StatusOK)
resp.SetBodyString(`{"success":false,"score":0.99,"error-codes":["bad-input"]}`)

assert.NotNil(t, req)
assert.Equal(t, fasthttp.MethodPost, string(req.Header.Method()))
assert.Equal(t, googleApiReCaptchaVerifyUrl, req.URI().String())
assert.Equal(t, string(headers.ContentTypeForm), string(req.Header.ContentType()))
assert.Equal(t, "captcha_secret_1", string(req.PostArgs().Peek("secret")))
assert.Equal(t, "10.0.0.1", string(req.PostArgs().Peek("remoteip")))
assert.Equal(t, "captcha_challenge_2", string(req.PostArgs().Peek("response")))

return nil
})

p := NewGoogleReCaptchaProvider(c, config.Config{
CaptchaSecret: "captcha_secret_1",
})
state, err := p.Verify(&ctx)

assert.False(t, state)
assert.NoError(t, err)
}

func TestVerifyEmptySecret(t *testing.T) {
ctrl := gomock.NewController(t)
c := mocks.NewHttpClientMock(ctrl)

ctx := fasthttp.RequestCtx{}
ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}})
ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2")

c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout))
c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout))

p := NewGoogleReCaptchaProvider(c, config.Config{
CaptchaSecret: "",
})
state, err := p.Verify(&ctx)

assert.True(t, state)
assert.NoError(t, err)
}

func TestVerifyEmptyChallenge(t *testing.T) {
ctrl := gomock.NewController(t)
c := mocks.NewHttpClientMock(ctrl)

ctx := fasthttp.RequestCtx{}
ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}})
ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "")

c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout))
c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout))

p := NewGoogleReCaptchaProvider(c, config.Config{
CaptchaSecret: "captcha_secret_1",
})
state, err := p.Verify(&ctx)

assert.False(t, state)
assert.NoError(t, err)
}

func TestVerifyRequestFail(t *testing.T) {
ctrl := gomock.NewController(t)
c := mocks.NewHttpClientMock(ctrl)

ctx := fasthttp.RequestCtx{}
ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}})
ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2")

c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout))
c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout))
c.EXPECT().Do(gomock.Any(), gomock.Any()).Return(fmt.Errorf("broken pipe"))

p := NewGoogleReCaptchaProvider(c, config.Config{
CaptchaSecret: "captcha_secret_1",
})
state, err := p.Verify(&ctx)

assert.False(t, state)
assert.Error(t, err)
}

func TestVerifyBadResponse(t *testing.T) {
ctrl := gomock.NewController(t)
c := mocks.NewHttpClientMock(ctrl)

ctx := fasthttp.RequestCtx{}
ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}})
ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2")

c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout))
c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout))
c.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error {
resp.SetStatusCode(fasthttp.StatusOK)
resp.SetBodyString(`{"success":true`)
return nil
})

p := NewGoogleReCaptchaProvider(c, config.Config{
CaptchaSecret: "captcha_secret_1",
})
state, err := p.Verify(&ctx)

assert.False(t, state)
assert.Error(t, err)
}
7 changes: 7 additions & 0 deletions captcha/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package captcha

import "github.com/valyala/fasthttp"

type Provider interface {
Verify(ctx *fasthttp.RequestCtx) (bool, error)
}
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module github.com/cash-track/gateway
go 1.21

require (
github.com/fasthttp/router v1.4.21
github.com/fasthttp/router v1.4.22
github.com/flf2ko/fasthttp-prometheus v0.1.0
github.com/stretchr/testify v1.8.0
github.com/valyala/fasthttp v1.50.0
github.com/valyala/fasthttp v1.51.0
)

require (
Expand All @@ -26,7 +26,10 @@ require (
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/sys v0.6.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/tools v0.2.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ 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=
github.com/fasthttp/router v1.4.21 h1:Ysgck9aZwaovqxsfhv7nPx9EgsYvB7t3nthrBMQoeIg=
github.com/fasthttp/router v1.4.21/go.mod h1:wtOlZHmOSGD048li7Nkuhw+ov40rr0tY2+IjT+mN9p4=
github.com/fasthttp/router v1.4.22 h1:qwWcYBbndVDwts4dKaz+A2ehsnbKilmiP6pUhXBfYKo=
github.com/fasthttp/router v1.4.22/go.mod h1:KeMvHLqhlB9vyDWD5TSvTccl9qeWrjSSiTJrJALHKV0=
github.com/flf2ko/fasthttp-prometheus v0.1.0 h1:hj4K3TwJ2B7Fe2E7lWE/eb9mtb7gBvwURXr4+iEFoCI=
github.com/flf2ko/fasthttp-prometheus v0.1.0/go.mod h1:5tGRWsJeP8ABLYovqPxa5c/zCgnsYUhhC1ivs/Kv/c4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -51,9 +53,19 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
Expand Down
Loading

0 comments on commit 0cd874e

Please sign in to comment.