mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-11-04 00:11:04 +00:00 
			
		
		
		
	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 <john.olheiser@gmail.com>
		
			
				
	
	
		
			118 lines
		
	
	
	
		
			2.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			118 lines
		
	
	
	
		
			2.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package pwn
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/sha1"
 | 
						|
	"encoding/hex"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
)
 | 
						|
 | 
						|
const passwordURL = "https://api.pwnedpasswords.com/range/"
 | 
						|
 | 
						|
// ErrEmptyPassword is an empty password error
 | 
						|
var ErrEmptyPassword = errors.New("password cannot be empty")
 | 
						|
 | 
						|
// Client is a HaveIBeenPwned client
 | 
						|
type Client struct {
 | 
						|
	ctx  context.Context
 | 
						|
	http *http.Client
 | 
						|
}
 | 
						|
 | 
						|
// New returns a new HaveIBeenPwned Client
 | 
						|
func New(options ...ClientOption) *Client {
 | 
						|
	client := &Client{
 | 
						|
		ctx:  context.Background(),
 | 
						|
		http: http.DefaultClient,
 | 
						|
	}
 | 
						|
 | 
						|
	for _, opt := range options {
 | 
						|
		opt(client)
 | 
						|
	}
 | 
						|
 | 
						|
	return client
 | 
						|
}
 | 
						|
 | 
						|
// ClientOption is a way to modify a new Client
 | 
						|
type ClientOption func(*Client)
 | 
						|
 | 
						|
// WithHTTP will set the http.Client of a Client
 | 
						|
func WithHTTP(httpClient *http.Client) func(pwnClient *Client) {
 | 
						|
	return func(pwnClient *Client) {
 | 
						|
		pwnClient.http = httpClient
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithContext will set the context.Context of a Client
 | 
						|
func WithContext(ctx context.Context) func(pwnClient *Client) {
 | 
						|
	return func(pwnClient *Client) {
 | 
						|
		pwnClient.ctx = ctx
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*http.Request, error) {
 | 
						|
	req, err := http.NewRequestWithContext(ctx, method, url, body)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	req.Header.Add("User-Agent", "Gitea "+setting.AppVer)
 | 
						|
	return req, nil
 | 
						|
}
 | 
						|
 | 
						|
// CheckPassword returns the number of times a password has been compromised
 | 
						|
// Adding padding will make requests more secure, however is also slower
 | 
						|
// because artificial responses will be added to the response
 | 
						|
// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
 | 
						|
func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
 | 
						|
	if strings.TrimSpace(pw) == "" {
 | 
						|
		return -1, ErrEmptyPassword
 | 
						|
	}
 | 
						|
 | 
						|
	sha := sha1.New()
 | 
						|
	sha.Write([]byte(pw))
 | 
						|
	enc := hex.EncodeToString(sha.Sum(nil))
 | 
						|
	prefix, suffix := enc[:5], enc[5:]
 | 
						|
 | 
						|
	req, err := newRequest(c.ctx, http.MethodGet, fmt.Sprintf("%s%s", passwordURL, prefix), nil)
 | 
						|
	if err != nil {
 | 
						|
		return -1, nil
 | 
						|
	}
 | 
						|
	if padding {
 | 
						|
		req.Header.Add("Add-Padding", "true")
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := c.http.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return -1, err
 | 
						|
	}
 | 
						|
 | 
						|
	body, err := io.ReadAll(resp.Body)
 | 
						|
	if err != nil {
 | 
						|
		return -1, err
 | 
						|
	}
 | 
						|
	defer resp.Body.Close()
 | 
						|
 | 
						|
	for _, pair := range strings.Split(string(body), "\n") {
 | 
						|
		parts := strings.Split(pair, ":")
 | 
						|
		if len(parts) != 2 {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if strings.EqualFold(suffix, parts[0]) {
 | 
						|
			count, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
 | 
						|
			if err != nil {
 | 
						|
				return -1, err
 | 
						|
			}
 | 
						|
			return int(count), nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return 0, nil
 | 
						|
}
 |