@@ -0,0 +1,204 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/altcha-org/altcha-lib-go"
|
||||
"source.offmarket.win/aleagle/eaglecast/internal/tmptokens"
|
||||
)
|
||||
|
||||
const (
|
||||
hCaptchaURL = "https://hcaptcha.com/siteverify"
|
||||
)
|
||||
|
||||
type hCaptchaResp struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorCodes []string `json:"error_codes"`
|
||||
}
|
||||
|
||||
const (
|
||||
ProviderNone = ""
|
||||
ProviderHCaptcha = "hcaptcha"
|
||||
ProviderAltcha = "altcha"
|
||||
)
|
||||
|
||||
// Captcha is a captcha client supporting multiple providers.
|
||||
type Captcha struct {
|
||||
provider string
|
||||
hCaptcha hCaptchaOpt
|
||||
altcha altchaOpt
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type Opt struct {
|
||||
HCaptcha struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Key string `json:"key"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"hcaptcha"`
|
||||
Altcha struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Complexity int `json:"complexity"`
|
||||
} `json:"altcha"`
|
||||
}
|
||||
|
||||
type hCaptchaOpt struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
type altchaOpt struct {
|
||||
Complexity int
|
||||
HMACKey string
|
||||
}
|
||||
|
||||
// New returns a new instance of the CAPTCHA client.
|
||||
func New(o Opt) *Captcha {
|
||||
timeout := time.Second * 5
|
||||
|
||||
c := &Captcha{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConnsPerHost: 10,
|
||||
MaxConnsPerHost: 100,
|
||||
ResponseHeaderTimeout: timeout,
|
||||
IdleConnTimeout: timeout,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Determine which provider is enabled
|
||||
if o.Altcha.Enabled {
|
||||
c.provider = ProviderAltcha
|
||||
|
||||
// Generate an random HMAC key for Altcha.
|
||||
b := make([]byte, 24) // 24 bytes will give 32 characters when base64 encoded
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error generating Altcha HMAC key: %v", err))
|
||||
}
|
||||
hmacKey := base64.URLEncoding.EncodeToString(b)[:32]
|
||||
|
||||
c.altcha = altchaOpt{
|
||||
Complexity: o.Altcha.Complexity,
|
||||
HMACKey: hmacKey,
|
||||
}
|
||||
} else if o.HCaptcha.Enabled {
|
||||
c.provider = ProviderHCaptcha
|
||||
c.hCaptcha = hCaptchaOpt{
|
||||
Secret: o.HCaptcha.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// IsEnabled returns true if any captcha provider is enabled.
|
||||
func (c *Captcha) IsEnabled() bool {
|
||||
return c.provider != ProviderNone
|
||||
}
|
||||
|
||||
// GetProvider returns the active captcha provider.
|
||||
func (c *Captcha) GetProvider() string {
|
||||
return c.provider
|
||||
}
|
||||
|
||||
// GenerateChallenge generates a challenge for the active provider.
|
||||
// For hCaptcha, this returns empty string as challenges are generated client-side.
|
||||
// For Altcha, this returns a JSON challenge.
|
||||
func (c *Captcha) GenerateChallenge() (string, error) {
|
||||
switch c.provider {
|
||||
case ProviderAltcha:
|
||||
exp := time.Now().Add(5 * time.Minute)
|
||||
challenge, err := altcha.CreateChallenge(altcha.ChallengeOptions{
|
||||
Algorithm: altcha.SHA256,
|
||||
MaxNumber: int64(c.altcha.Complexity),
|
||||
SaltLength: 12,
|
||||
HMACKey: c.altcha.HMACKey,
|
||||
Expires: &exp,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create Altcha challenge: %w", err)
|
||||
}
|
||||
|
||||
challengeJSON, err := json.Marshal(challenge)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal Altcha challenge: %w", err)
|
||||
}
|
||||
|
||||
return string(challengeJSON), nil
|
||||
case ProviderHCaptcha:
|
||||
// hCaptcha generates challenges client-side.
|
||||
return "", nil
|
||||
default:
|
||||
return "", fmt.Errorf("no captcha provider enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify verifies a CAPTCHA response.
|
||||
func (c *Captcha) Verify(token string) (error, bool) {
|
||||
switch c.provider {
|
||||
case ProviderAltcha:
|
||||
return c.verifyAltcha(token)
|
||||
case ProviderHCaptcha:
|
||||
return c.verifyHCaptcha(token)
|
||||
default:
|
||||
return fmt.Errorf("no captcha provider enabled"), false
|
||||
}
|
||||
}
|
||||
|
||||
// verifyHCaptcha verifies an hCaptcha response.
|
||||
func (c *Captcha) verifyHCaptcha(token string) (error, bool) {
|
||||
resp, err := c.client.PostForm(hCaptchaURL, url.Values{
|
||||
"secret": {c.hCaptcha.Secret},
|
||||
"response": {token},
|
||||
})
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
var r hCaptchaResp
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return err, true
|
||||
}
|
||||
|
||||
if !r.Success {
|
||||
return fmt.Errorf("hCaptcha failed: %s", strings.Join(r.ErrorCodes, ",")), false
|
||||
}
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
// verifyAltcha verifies an Altcha response.
|
||||
func (c *Captcha) verifyAltcha(payload string) (error, bool) {
|
||||
valid, err := altcha.VerifySolution(payload, c.altcha.HMACKey, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify captcha solution: %w", err), false
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("captcha verification failed"), false
|
||||
}
|
||||
|
||||
// Disallow token reuse.
|
||||
if _, err := tmptokens.Check(payload); err == nil {
|
||||
return fmt.Errorf("captcha token already used"), false
|
||||
}
|
||||
tmptokens.Set(payload, 5*time.Minute, nil)
|
||||
|
||||
return nil, true
|
||||
}
|
||||
Reference in New Issue
Block a user