Skip to content

Commit 2052a9e

Browse files
authored
Consume hcaptcha and pwn deps (#22610)
This PR just consumes the [hcaptcha](https://gitea.com/jolheiser/hcaptcha) and [haveibeenpwned](https://gitea.com/jolheiser/pwn) modules directly into Gitea. Also let this serve as a notice that I'm fine with transferring my license (which was already MIT) from my own name to "The Gitea Authors". Signed-off-by: jolheiser <[email protected]>
1 parent e88b529 commit 2052a9e

File tree

10 files changed

+530
-24
lines changed

10 files changed

+530
-24
lines changed

assets/go-licenses.json

-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/generate-go-licenses.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/json"
1010
"io/fs"
1111
"os"
12+
goPath "path"
1213
"path/filepath"
1314
"regexp"
1415
"sort"
@@ -47,13 +48,15 @@ func main() {
4748

4849
entries := []LicenseEntry{}
4950
for _, path := range paths {
51+
path := filepath.ToSlash(path)
52+
5053
licenseText, err := os.ReadFile(path)
5154
if err != nil {
5255
panic(err)
5356
}
5457

55-
path := strings.Replace(path, base+string(os.PathSeparator), "", 1)
56-
name := filepath.Dir(path)
58+
path = strings.Replace(path, base+"/", "", 1)
59+
name := goPath.Dir(path)
5760

5861
// There might be a bug somewhere in go-licenses that sometimes interprets the
5962
// root package as "." and sometimes as "code.gitea.io/gitea". Workaround by

go.mod

-2
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ require (
9696
github.com/yuin/goldmark v1.5.3
9797
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
9898
github.com/yuin/goldmark-meta v1.1.0
99-
go.jolheiser.com/hcaptcha v0.0.4
100-
go.jolheiser.com/pwn v0.0.3
10199
golang.org/x/crypto v0.4.0
102100
golang.org/x/net v0.4.0
103101
golang.org/x/oauth2 v0.3.0

go.sum

-4
Original file line numberDiff line numberDiff line change
@@ -1267,10 +1267,6 @@ go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
12671267
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
12681268
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
12691269
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
1270-
go.jolheiser.com/hcaptcha v0.0.4 h1:RrDERcr/Tz/kWyJenjVtI+V09RtLinXxlAemiwN5F+I=
1271-
go.jolheiser.com/hcaptcha v0.0.4/go.mod h1:aw32WQOxnQZ6E06C0LypCf+sxNxPACyOnq+ZGnrIYho=
1272-
go.jolheiser.com/pwn v0.0.3 h1:MQowb3QvCL5r5NmHmCPxw93SdjfgJ0q6rAwYn4i1Hjg=
1273-
go.jolheiser.com/pwn v0.0.3/go.mod h1:/j5Dl8ftNqqJ8Dlx3YTrJV1wIR2lWOTyrNU3Qe7rk6I=
12741270
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
12751271
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
12761272
go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=

modules/hcaptcha/error.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package hcaptcha
5+
6+
const (
7+
ErrMissingInputSecret ErrorCode = "missing-input-secret"
8+
ErrInvalidInputSecret ErrorCode = "invalid-input-secret"
9+
ErrMissingInputResponse ErrorCode = "missing-input-response"
10+
ErrInvalidInputResponse ErrorCode = "invalid-input-response"
11+
ErrBadRequest ErrorCode = "bad-request"
12+
ErrInvalidOrAlreadySeenResponse ErrorCode = "invalid-or-already-seen-response"
13+
ErrNotUsingDummyPasscode ErrorCode = "not-using-dummy-passcode"
14+
ErrSitekeySecretMismatch ErrorCode = "sitekey-secret-mismatch"
15+
)
16+
17+
// ErrorCode is any possible error from hCaptcha
18+
type ErrorCode string
19+
20+
// String fulfills the Stringer interface
21+
func (err ErrorCode) String() string {
22+
switch err {
23+
case ErrMissingInputSecret:
24+
return "Your secret key is missing."
25+
case ErrInvalidInputSecret:
26+
return "Your secret key is invalid or malformed."
27+
case ErrMissingInputResponse:
28+
return "The response parameter (verification token) is missing."
29+
case ErrInvalidInputResponse:
30+
return "The response parameter (verification token) is invalid or malformed."
31+
case ErrBadRequest:
32+
return "The request is invalid or malformed."
33+
case ErrInvalidOrAlreadySeenResponse:
34+
return "The response parameter has already been checked, or has another issue."
35+
case ErrNotUsingDummyPasscode:
36+
return "You have used a testing sitekey but have not used its matching secret."
37+
case ErrSitekeySecretMismatch:
38+
return "The sitekey is not registered with the provided secret."
39+
default:
40+
return ""
41+
}
42+
}
43+
44+
// Error fulfills the error interface
45+
func (err ErrorCode) Error() string {
46+
return err.String()
47+
}

modules/hcaptcha/hcaptcha.go

+111-4
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,127 @@ package hcaptcha
55

66
import (
77
"context"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
"strings"
812

13+
"code.gitea.io/gitea/modules/json"
914
"code.gitea.io/gitea/modules/setting"
10-
11-
"go.jolheiser.com/hcaptcha"
1215
)
1316

17+
const verifyURL = "https://hcaptcha.com/siteverify"
18+
19+
// Client is an hCaptcha client
20+
type Client struct {
21+
ctx context.Context
22+
http *http.Client
23+
24+
secret string
25+
}
26+
27+
// PostOptions are optional post form values
28+
type PostOptions struct {
29+
RemoteIP string
30+
Sitekey string
31+
}
32+
33+
// ClientOption is a func to modify a new Client
34+
type ClientOption func(*Client)
35+
36+
// WithHTTP sets the http.Client of a Client
37+
func WithHTTP(httpClient *http.Client) func(*Client) {
38+
return func(hClient *Client) {
39+
hClient.http = httpClient
40+
}
41+
}
42+
43+
// WithContext sets the context.Context of a Client
44+
func WithContext(ctx context.Context) func(*Client) {
45+
return func(hClient *Client) {
46+
hClient.ctx = ctx
47+
}
48+
}
49+
50+
// New returns a new hCaptcha Client
51+
func New(secret string, options ...ClientOption) (*Client, error) {
52+
if strings.TrimSpace(secret) == "" {
53+
return nil, ErrMissingInputSecret
54+
}
55+
56+
client := &Client{
57+
ctx: context.Background(),
58+
http: http.DefaultClient,
59+
secret: secret,
60+
}
61+
62+
for _, opt := range options {
63+
opt(client)
64+
}
65+
66+
return client, nil
67+
}
68+
69+
// Response is an hCaptcha response
70+
type Response struct {
71+
Success bool `json:"success"`
72+
ChallengeTS string `json:"challenge_ts"`
73+
Hostname string `json:"hostname"`
74+
Credit bool `json:"credit,omitempty"`
75+
ErrorCodes []ErrorCode `json:"error-codes"`
76+
}
77+
78+
// Verify checks the response against the hCaptcha API
79+
func (c *Client) Verify(token string, opts PostOptions) (*Response, error) {
80+
if strings.TrimSpace(token) == "" {
81+
return nil, ErrMissingInputResponse
82+
}
83+
84+
post := url.Values{
85+
"secret": []string{c.secret},
86+
"response": []string{token},
87+
}
88+
if strings.TrimSpace(opts.RemoteIP) != "" {
89+
post.Add("remoteip", opts.RemoteIP)
90+
}
91+
if strings.TrimSpace(opts.Sitekey) != "" {
92+
post.Add("sitekey", opts.Sitekey)
93+
}
94+
95+
// Basically a copy of http.PostForm, but with a context
96+
req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, verifyURL, strings.NewReader(post.Encode()))
97+
if err != nil {
98+
return nil, err
99+
}
100+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
101+
102+
resp, err := c.http.Do(req)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
body, err := io.ReadAll(resp.Body)
108+
if err != nil {
109+
return nil, err
110+
}
111+
defer resp.Body.Close()
112+
113+
var response *Response
114+
if err := json.Unmarshal(body, &response); err != nil {
115+
return nil, err
116+
}
117+
118+
return response, nil
119+
}
120+
14121
// Verify calls hCaptcha API to verify token
15122
func Verify(ctx context.Context, response string) (bool, error) {
16-
client, err := hcaptcha.New(setting.Service.HcaptchaSecret, hcaptcha.WithContext(ctx))
123+
client, err := New(setting.Service.HcaptchaSecret, WithContext(ctx))
17124
if err != nil {
18125
return false, err
19126
}
20127

21-
resp, err := client.Verify(response, hcaptcha.PostOptions{
128+
resp, err := client.Verify(response, PostOptions{
22129
Sitekey: setting.Service.HcaptchaSitekey,
23130
})
24131
if err != nil {

modules/hcaptcha/hcaptcha_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package hcaptcha
5+
6+
import (
7+
"net/http"
8+
"os"
9+
"strings"
10+
"testing"
11+
"time"
12+
)
13+
14+
const (
15+
dummySiteKey = "10000000-ffff-ffff-ffff-000000000001"
16+
dummySecret = "0x0000000000000000000000000000000000000000"
17+
dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
18+
)
19+
20+
func TestMain(m *testing.M) {
21+
os.Exit(m.Run())
22+
}
23+
24+
func TestCaptcha(t *testing.T) {
25+
tt := []struct {
26+
Name string
27+
Secret string
28+
Token string
29+
Error ErrorCode
30+
}{
31+
{
32+
Name: "Success",
33+
Secret: dummySecret,
34+
Token: dummyToken,
35+
},
36+
{
37+
Name: "Missing Secret",
38+
Token: dummyToken,
39+
Error: ErrMissingInputSecret,
40+
},
41+
{
42+
Name: "Missing Token",
43+
Secret: dummySecret,
44+
Error: ErrMissingInputResponse,
45+
},
46+
{
47+
Name: "Invalid Token",
48+
Secret: dummySecret,
49+
Token: "test",
50+
Error: ErrInvalidInputResponse,
51+
},
52+
}
53+
54+
for _, tc := range tt {
55+
t.Run(tc.Name, func(t *testing.T) {
56+
client, err := New(tc.Secret, WithHTTP(&http.Client{
57+
Timeout: time.Second * 5,
58+
}))
59+
if err != nil {
60+
// The only error that can be returned from creating a client
61+
if tc.Error == ErrMissingInputSecret && err == ErrMissingInputSecret {
62+
return
63+
}
64+
t.Log(err)
65+
t.FailNow()
66+
}
67+
68+
resp, err := client.Verify(tc.Token, PostOptions{
69+
Sitekey: dummySiteKey,
70+
})
71+
if err != nil {
72+
// The only error that can be returned prior to the request
73+
if tc.Error == ErrMissingInputResponse && err == ErrMissingInputResponse {
74+
return
75+
}
76+
t.Log(err)
77+
t.FailNow()
78+
}
79+
80+
if tc.Error.String() != "" {
81+
if resp.Success {
82+
t.Log("Verification should fail.")
83+
t.Fail()
84+
}
85+
if len(resp.ErrorCodes) == 0 {
86+
t.Log("hCaptcha should have returned an error.")
87+
t.Fail()
88+
}
89+
var hasErr bool
90+
for _, err := range resp.ErrorCodes {
91+
if strings.EqualFold(err.String(), tc.Error.String()) {
92+
hasErr = true
93+
break
94+
}
95+
}
96+
if !hasErr {
97+
t.Log("hCaptcha did not return the error being tested")
98+
t.Fail()
99+
}
100+
} else if !resp.Success {
101+
t.Log("Verification should succeed.")
102+
t.Fail()
103+
}
104+
})
105+
}
106+
}

modules/password/pwn.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ package password
66
import (
77
"context"
88

9+
"code.gitea.io/gitea/modules/password/pwn"
910
"code.gitea.io/gitea/modules/setting"
10-
11-
"go.jolheiser.com/pwn"
1211
)
1312

1413
// IsPwned checks whether a password has been pwned

0 commit comments

Comments
 (0)