Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consume hcaptcha and pwn deps #22610

Merged
merged 5 commits into from
Jan 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions assets/go-licenses.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions build/generate-go-licenses.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/json"
"io/fs"
"os"
goPath "path"
"path/filepath"
"regexp"
"sort"
Expand Down Expand Up @@ -47,13 +48,15 @@ func main() {

entries := []LicenseEntry{}
for _, path := range paths {
path := filepath.ToSlash(path)

licenseText, err := os.ReadFile(path)
if err != nil {
panic(err)
}

path := strings.Replace(path, base+string(os.PathSeparator), "", 1)
name := filepath.Dir(path)
path = strings.Replace(path, base+"/", "", 1)
name := goPath.Dir(path)

// There might be a bug somewhere in go-licenses that sometimes interprets the
// root package as "." and sometimes as "code.gitea.io/gitea". Workaround by
Expand Down
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ require (
github.com/yuin/goldmark v1.5.3
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
github.com/yuin/goldmark-meta v1.1.0
go.jolheiser.com/hcaptcha v0.0.4
go.jolheiser.com/pwn v0.0.3
golang.org/x/crypto v0.4.0
golang.org/x/net v0.4.0
golang.org/x/oauth2 v0.3.0
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1267,10 +1267,6 @@ go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.jolheiser.com/hcaptcha v0.0.4 h1:RrDERcr/Tz/kWyJenjVtI+V09RtLinXxlAemiwN5F+I=
go.jolheiser.com/hcaptcha v0.0.4/go.mod h1:aw32WQOxnQZ6E06C0LypCf+sxNxPACyOnq+ZGnrIYho=
go.jolheiser.com/pwn v0.0.3 h1:MQowb3QvCL5r5NmHmCPxw93SdjfgJ0q6rAwYn4i1Hjg=
go.jolheiser.com/pwn v0.0.3/go.mod h1:/j5Dl8ftNqqJ8Dlx3YTrJV1wIR2lWOTyrNU3Qe7rk6I=
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
Expand Down
47 changes: 47 additions & 0 deletions modules/hcaptcha/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package hcaptcha

const (
ErrMissingInputSecret ErrorCode = "missing-input-secret"
ErrInvalidInputSecret ErrorCode = "invalid-input-secret"
ErrMissingInputResponse ErrorCode = "missing-input-response"
ErrInvalidInputResponse ErrorCode = "invalid-input-response"
ErrBadRequest ErrorCode = "bad-request"
ErrInvalidOrAlreadySeenResponse ErrorCode = "invalid-or-already-seen-response"
ErrNotUsingDummyPasscode ErrorCode = "not-using-dummy-passcode"
ErrSitekeySecretMismatch ErrorCode = "sitekey-secret-mismatch"
)

// ErrorCode is any possible error from hCaptcha
type ErrorCode string

// String fulfills the Stringer interface
func (err ErrorCode) String() string {
switch err {
case ErrMissingInputSecret:
return "Your secret key is missing."
case ErrInvalidInputSecret:
return "Your secret key is invalid or malformed."
case ErrMissingInputResponse:
return "The response parameter (verification token) is missing."
case ErrInvalidInputResponse:
return "The response parameter (verification token) is invalid or malformed."
case ErrBadRequest:
return "The request is invalid or malformed."
case ErrInvalidOrAlreadySeenResponse:
return "The response parameter has already been checked, or has another issue."
case ErrNotUsingDummyPasscode:
return "You have used a testing sitekey but have not used its matching secret."
case ErrSitekeySecretMismatch:
return "The sitekey is not registered with the provided secret."
default:
return ""
}
}

// Error fulfills the error interface
func (err ErrorCode) Error() string {
return err.String()
}
115 changes: 111 additions & 4 deletions modules/hcaptcha/hcaptcha.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,127 @@ package hcaptcha

import (
"context"
"io"
"net/http"
"net/url"
"strings"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"

"go.jolheiser.com/hcaptcha"
)

const verifyURL = "https://hcaptcha.com/siteverify"

// Client is an hCaptcha client
type Client struct {
ctx context.Context
http *http.Client

secret string
}

// PostOptions are optional post form values
type PostOptions struct {
RemoteIP string
Sitekey string
}

// ClientOption is a func to modify a new Client
type ClientOption func(*Client)

// WithHTTP sets the http.Client of a Client
func WithHTTP(httpClient *http.Client) func(*Client) {
return func(hClient *Client) {
hClient.http = httpClient
}
}

// WithContext sets the context.Context of a Client
func WithContext(ctx context.Context) func(*Client) {
return func(hClient *Client) {
hClient.ctx = ctx
}
}

// New returns a new hCaptcha Client
func New(secret string, options ...ClientOption) (*Client, error) {
if strings.TrimSpace(secret) == "" {
return nil, ErrMissingInputSecret
}

client := &Client{
ctx: context.Background(),
http: http.DefaultClient,
secret: secret,
}

for _, opt := range options {
opt(client)
}

return client, nil
}

// Response is an hCaptcha response
type Response struct {
Success bool `json:"success"`
ChallengeTS string `json:"challenge_ts"`
Hostname string `json:"hostname"`
Credit bool `json:"credit,omitempty"`
ErrorCodes []ErrorCode `json:"error-codes"`
}

// Verify checks the response against the hCaptcha API
func (c *Client) Verify(token string, opts PostOptions) (*Response, error) {
if strings.TrimSpace(token) == "" {
return nil, ErrMissingInputResponse
}

post := url.Values{
"secret": []string{c.secret},
"response": []string{token},
}
if strings.TrimSpace(opts.RemoteIP) != "" {
post.Add("remoteip", opts.RemoteIP)
}
if strings.TrimSpace(opts.Sitekey) != "" {
post.Add("sitekey", opts.Sitekey)
}

// Basically a copy of http.PostForm, but with a context
req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, verifyURL, strings.NewReader(post.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := c.http.Do(req)
if err != nil {
return nil, err
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var response *Response
if err := json.Unmarshal(body, &response); err != nil {
return nil, err
}

return response, nil
}

// Verify calls hCaptcha API to verify token
func Verify(ctx context.Context, response string) (bool, error) {
client, err := hcaptcha.New(setting.Service.HcaptchaSecret, hcaptcha.WithContext(ctx))
client, err := New(setting.Service.HcaptchaSecret, WithContext(ctx))
if err != nil {
return false, err
}

resp, err := client.Verify(response, hcaptcha.PostOptions{
resp, err := client.Verify(response, PostOptions{
Sitekey: setting.Service.HcaptchaSitekey,
})
if err != nil {
Expand Down
106 changes: 106 additions & 0 deletions modules/hcaptcha/hcaptcha_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package hcaptcha

import (
"net/http"
"os"
"strings"
"testing"
"time"
)

const (
dummySiteKey = "10000000-ffff-ffff-ffff-000000000001"
dummySecret = "0x0000000000000000000000000000000000000000"
dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
)

func TestMain(m *testing.M) {
os.Exit(m.Run())
}

func TestCaptcha(t *testing.T) {
tt := []struct {
Name string
Secret string
Token string
Error ErrorCode
}{
{
Name: "Success",
Secret: dummySecret,
Token: dummyToken,
},
{
Name: "Missing Secret",
Token: dummyToken,
Error: ErrMissingInputSecret,
},
{
Name: "Missing Token",
Secret: dummySecret,
Error: ErrMissingInputResponse,
},
{
Name: "Invalid Token",
Secret: dummySecret,
Token: "test",
Error: ErrInvalidInputResponse,
},
}

for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
client, err := New(tc.Secret, WithHTTP(&http.Client{
Timeout: time.Second * 5,
}))
if err != nil {
// The only error that can be returned from creating a client
if tc.Error == ErrMissingInputSecret && err == ErrMissingInputSecret {
return
}
t.Log(err)
t.FailNow()
}

resp, err := client.Verify(tc.Token, PostOptions{
Sitekey: dummySiteKey,
})
if err != nil {
// The only error that can be returned prior to the request
if tc.Error == ErrMissingInputResponse && err == ErrMissingInputResponse {
return
}
t.Log(err)
t.FailNow()
}

if tc.Error.String() != "" {
if resp.Success {
t.Log("Verification should fail.")
t.Fail()
}
if len(resp.ErrorCodes) == 0 {
t.Log("hCaptcha should have returned an error.")
t.Fail()
}
var hasErr bool
for _, err := range resp.ErrorCodes {
if strings.EqualFold(err.String(), tc.Error.String()) {
hasErr = true
break
}
}
if !hasErr {
t.Log("hCaptcha did not return the error being tested")
t.Fail()
}
} else if !resp.Success {
t.Log("Verification should succeed.")
t.Fail()
}
})
}
}
3 changes: 1 addition & 2 deletions modules/password/pwn.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ package password
import (
"context"

"code.gitea.io/gitea/modules/password/pwn"
"code.gitea.io/gitea/modules/setting"

"go.jolheiser.com/pwn"
)

// IsPwned checks whether a password has been pwned
Expand Down
Loading