Skip to content

Commit

Permalink
email queue feature flag (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-cronus authored Aug 5, 2024
1 parent 07da89f commit fdb28b5
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 31 deletions.
1 change: 1 addition & 0 deletions auth/email_link/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ type (
MaxWrongAttemptsCount int64 `yaml:"maxWrongAttemptsCount"`
} `yaml:"confirmationCode"`
DisableEmailSending bool `yaml:"disableEmailSending"`
QueueProcessing bool `yaml:"queueProcessing"`
ExtraLoadBalancersCount int `yaml:"extraLoadBalancersCount"`
}
loginID struct {
Expand Down
4 changes: 3 additions & 1 deletion auth/email_link/emaillink.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ func NewClient(ctx context.Context, cancel context.CancelFunc, userModifier User
cl.emailClients = append(cl.emailClients, email.New(fmt.Sprintf("%v/%v", applicationYamlKey, i+1)))
cl.fromRecipients = append(cl.fromRecipients, fromRecipient{nestedCfg.FromEmailName, nestedCfg.FromEmailAddress})
}
go cl.processEmailQueue(ctx)
if cfg.QueueProcessing {
go cl.processEmailQueue(ctx)
}
}
go cl.startOldLoginAttemptsCleaner(ctx)

Expand Down
74 changes: 67 additions & 7 deletions auth/email_link/link_start_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"github.com/ice-blockchain/wintr/time"
)

//nolint:funlen,gocognit,revive,gocritic,lll //.
//nolint:funlen,gocognit,revive,gocritic,lll,gocyclo,cyclop //.
func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (posInQueue int64, rateLimit, loginSession string, err error) {
if ctx.Err() != nil {
return 0, "", "", errors.Wrap(ctx.Err(), "send sign in link to email failed because context failed")
Expand All @@ -31,7 +31,7 @@ func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUn
id := loginID{emailValue, deviceUniqueID}
loginSessionNumber := now.Time.Unix() / int64(sameIPCheckRate.Seconds())
oldEmail := users.ConfirmedEmail(ctx)
if oldEmail == "" {
if oldEmail == "" && c.cfg.QueueProcessing {
posInQueue, rateLimit, err = c.enqueueLoginAttempt(ctx, now, emailValue)
if err != nil {
if errors.Is(err, errAlreadyEnqueued) {
Expand Down Expand Up @@ -60,9 +60,16 @@ func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUn
if err != nil {
return 0, "", "", errors.Wrap(err, "can't call generateLoginSession")
}

if (!c.cfg.QueueProcessing) && loginSessionNumber > 0 && clientIP != "" && userIDForPhoneNumberToEmailMigration(ctx) == "" {
if ipErr := c.upsertIPLoginAttempt(ctx, &id, clientIP, loginSessionNumber); ipErr != nil {
return 0, "", "", errors.Wrapf(ipErr, "failed increment login attempts for IP:%v (session num %v)", clientIP, loginSessionNumber)
}
}

if uErr := c.upsertEmailLinkSignIn(ctx, id.Email, id.DeviceUniqueID, confirmationCode, language, now); uErr != nil {
if errors.Is(uErr, ErrUserDuplicate) {
oldLoginSession, oErr := c.restoreOldLoginSession(&id, clientIP, oldEmail, loginSessionNumber)
oldLoginSession, oErr := c.restoreOldLoginSession(ctx, &id, clientIP, oldEmail, loginSessionNumber)
if oErr != nil {
return 0, "", "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(oErr, "failed to calculate oldLoginSession"),
Expand All @@ -73,13 +80,24 @@ func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUn
return posInQueue, rateLimit, oldLoginSession, nil
}

return 0, "", "", errors.Wrapf(uErr, "failed to store/update email link sign ins for id:%#v", id)
return 0, "", "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"),
errors.Wrapf(uErr, "failed to store/update email link sign ins for id:%#v", id),
).ErrorOrNil()
}
if oldEmail != "" {
if sendModEmailErr := c.sendEmailWithType(ctx, modifyEmailType, language, []string{id.Email}, []string{confirmationCode}); sendModEmailErr != nil {
return 0, "", loginSession, errors.Wrapf(sendModEmailErr, "failed to send validation email for id:%#v", id)
}
}
if !c.cfg.QueueProcessing {
if sErr := c.sendEmailWithType(ctx, signInEmailType, language, []string{id.Email}, []string{confirmationCode}); sErr != nil {
return 0, "", "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"),
errors.Wrapf(sErr, "can't send sign in email for id:%#v", id),
).ErrorOrNil()
}
}

return posInQueue, rateLimit, loginSession, nil
}
Expand All @@ -97,13 +115,55 @@ func (c *client) getExistingLoginSession(ctx context.Context, id *loginID, login
return loginSession, nil
}

func (c *client) restoreOldLoginSession(id *loginID, clientIP, oldEmail string, loginSessionNumber int64) (string, error) {
func (c *client) restoreOldLoginSession(ctx context.Context, id *loginID, clientIP, oldEmail string, loginSessionNumber int64) (string, error) {
oldLoginSession, dErr := c.generateLoginSession(id, clientIP, oldEmail, loginSessionNumber)
if dErr != nil {
return "", errors.Wrap(dErr, "can't generate loginSession")
return "", multierror.Append( //nolint:wrapcheck // .
errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"),
errors.Wrap(dErr, "can't generate loginSession"),
).ErrorOrNil()
}

return oldLoginSession, errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber),
"failed to rollback login attempts for ip due to reuse of loginSession")
}

func (c *client) decrementIPLoginAttempts(ctx context.Context, ip string, loginSessionNumber int64) error {
if c.cfg.QueueProcessing {
return nil
}
if ip != "" && loginSessionNumber > 0 && userIDForPhoneNumberToEmailMigration(ctx) == "" {
sql := `UPDATE sign_ins_per_ip SET
login_attempts = GREATEST(sign_ins_per_ip.login_attempts - 1, 0)
WHERE ip = $1 AND login_session_number = $2`
_, err := storage.Exec(ctx, c.db, sql, ip, loginSessionNumber)

return errors.Wrapf(err, "failed to decrease login attempts for ip %v lsn %v", ip, loginSessionNumber)
}

return oldLoginSession, nil
return nil
}

func (c *client) upsertIPLoginAttempt(ctx context.Context, id *loginID, clientIP string, loginSessionNumber int64) error {
if c.cfg.QueueProcessing {
return nil
}
sql := `INSERT INTO sign_ins_per_ip (ip, login_session_number, login_attempts)
VALUES ($1, $2, 1)
ON CONFLICT (login_session_number, ip) DO UPDATE
SET login_attempts = sign_ins_per_ip.login_attempts + 1`
_, err := storage.Exec(ctx, c.db, sql, clientIP, loginSessionNumber)
if err != nil {
if storage.IsErr(err, storage.ErrCheckFailed) {
err = errors.Wrapf(ErrTooManyAttempts, "user %#v is blocked due to a lot of requests from IP %v", id, clientIP)

return terror.New(err, map[string]any{"ip": clientIP})
}

return errors.Wrapf(err, "failed to increment login attempts from IP %v", clientIP)
}

return nil
}

func (c *client) validateEmailSignIn(ctx context.Context, id *loginID) error {
Expand Down
4 changes: 2 additions & 2 deletions cmd/eskimo-hut/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,9 @@ type (
KycFaceAvailable bool `json:"kycFaceAvailable,omitempty" example:"true"`
}
Auth struct {
RateLimit string `json:"rateLimit" example:"1000:24h"`
RateLimit string `json:"rateLimit,omitempty" example:"1000:24h"`
LoginSession string `json:"loginSession" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // .
PositionInQueue int64 `json:"positionInQueue" example:"675"`
PositionInQueue int64 `json:"positionInQueue,omitempty" example:"675"`
}
RefreshedToken struct {
*auth.Tokens
Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/hashicorp/go-multierror v1.1.1
github.com/ice-blockchain/freezer v1.501.0
github.com/ice-blockchain/freezer v1.502.0
github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb
github.com/ice-blockchain/wintr v1.150.0
github.com/imroc/req/v3 v3.43.7
Expand All @@ -24,7 +24,7 @@ require (
github.com/telegram-mini-apps/init-data-golang v1.1.5
github.com/testcontainers/testcontainers-go v0.32.0
github.com/zeebo/xxh3 v1.0.2
golang.org/x/mod v0.19.0
golang.org/x/mod v0.20.0
golang.org/x/net v0.27.0
)

Expand Down Expand Up @@ -186,14 +186,14 @@ require (
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/arch v0.9.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.23.0 // indirect
google.golang.org/api v0.190.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
Expand Down
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ice-blockchain/freezer v1.501.0 h1:LqHrE+eS7sEpp4PUF0aCX0y8gaCo7q+2NGUx+Ly10mA=
github.com/ice-blockchain/freezer v1.501.0/go.mod h1:Qyk4iqw3UeU5mGoiHizVeO+1NO7z74+2VCGBk7wMCkQ=
github.com/ice-blockchain/freezer v1.502.0 h1:zi3+W+5zvRisAQcG2xsb2aYjAMmyM7rzj6wonfanqWY=
github.com/ice-blockchain/freezer v1.502.0/go.mod h1:no3UJjer7Dan+lieiMgJZL3QS5QUr0xSlEfu4A8XR5Q=
github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb h1:8TnFP3mc7O+tc44kv2e0/TpZKnEVUaKH+UstwfBwRkk=
github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb/go.mod h1:ZsQU7i3mxhgBBu43Oev7WPFbIjP4TniN/b1UPNGbrq8=
github.com/ice-blockchain/wintr v1.150.0 h1:ZzQrPKPVFYRpD3xgQ7/Cmrjz8eeu6UMQ9TvE+brVK0U=
Expand Down Expand Up @@ -562,8 +562,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
Expand All @@ -580,8 +580,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand All @@ -599,17 +599,17 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -627,8 +627,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
Expand All @@ -641,8 +641,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
Expand Down

0 comments on commit fdb28b5

Please sign in to comment.