mirror of
https://github.com/PrivateCaptcha/PrivateCaptcha.git
synced 2026-02-11 00:08:47 -06:00
288 lines
5.5 KiB
Go
288 lines
5.5 KiB
Go
package common
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"maps"
|
|
|
|
"github.com/jpillora/backoff"
|
|
)
|
|
|
|
var (
|
|
HeaderValueContentTypeJSON = []string{ContentTypeJSON}
|
|
errEmptyDomain = errors.New("domain name is empty")
|
|
)
|
|
|
|
func RelURL(prefix, url string) string {
|
|
url = strings.TrimPrefix(url, "/")
|
|
p := strings.Trim(prefix, "/")
|
|
if len(p) == 0 {
|
|
return "/" + url
|
|
}
|
|
return "/" + p + "/" + url
|
|
}
|
|
|
|
func MaskEmail(email string, mask rune) string {
|
|
parts := strings.Split(email, "@")
|
|
if len(parts) != 2 {
|
|
return email
|
|
}
|
|
|
|
username := parts[0]
|
|
length := len(username)
|
|
|
|
var keep int
|
|
switch length {
|
|
case 0, 1:
|
|
keep = length
|
|
case 2, 3:
|
|
keep = 1
|
|
case 4, 5:
|
|
keep = 2
|
|
case 6, 7:
|
|
keep = 3
|
|
case 8, 9:
|
|
keep = 4
|
|
default:
|
|
keep = 5
|
|
}
|
|
|
|
prefix := username[:keep]
|
|
suffix := ""
|
|
|
|
n := length - keep
|
|
if n > 5 {
|
|
n = 5
|
|
suffix = ".."
|
|
}
|
|
|
|
xxx := strings.Repeat(string(mask), n)
|
|
|
|
return prefix + xxx + suffix + "@" + parts[1]
|
|
}
|
|
|
|
func SendReponse(ctx context.Context, w http.ResponseWriter, response []byte, headers ...map[string][]string) {
|
|
wHeader := w.Header()
|
|
for _, hh := range headers {
|
|
for key, value := range hh {
|
|
wHeader[key] = value
|
|
}
|
|
}
|
|
|
|
n, err := w.Write(response)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "Failed to send response", ErrAttr(err))
|
|
} else {
|
|
slog.Log(ctx, LevelTrace, "Sent response", "size", len(response), "sent", n)
|
|
}
|
|
}
|
|
|
|
func SendJSONResponse(ctx context.Context, w http.ResponseWriter, data interface{}, headers ...map[string][]string) {
|
|
response, err := json.Marshal(data)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "Failed to serialise response", ErrAttr(err))
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
wHeader := w.Header()
|
|
wHeader[HeaderContentType] = HeaderValueContentTypeJSON
|
|
for _, hh := range headers {
|
|
maps.Copy(wHeader, hh)
|
|
}
|
|
|
|
n, err := w.Write(response)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "Failed to send response", ErrAttr(err))
|
|
} else {
|
|
slog.Log(ctx, LevelTrace, "Sent response", "serialized", len(response), "sent", n)
|
|
}
|
|
}
|
|
|
|
func ParseBoolean(value string) bool {
|
|
switch value {
|
|
case "1", "Y", "y", "yes", "Yes", "true":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func containsAlphabetic(s string) bool {
|
|
for _, r := range s {
|
|
if unicode.IsLetter(r) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func onlyAlphabetic(s string) bool {
|
|
for _, r := range s {
|
|
if !unicode.IsLetter(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isLowerCase(s string) bool {
|
|
for _, r := range s {
|
|
if !unicode.IsLower(r) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func GuessFirstName(username string) string {
|
|
parts := strings.Fields(username)
|
|
|
|
for _, p := range parts {
|
|
if containsAlphabetic(p) {
|
|
if onlyAlphabetic(p) && isLowerCase(p) {
|
|
runes := []rune(p)
|
|
runes[0] = unicode.ToUpper(runes[0])
|
|
return string(runes)
|
|
}
|
|
|
|
return p
|
|
}
|
|
}
|
|
|
|
return username
|
|
}
|
|
|
|
func ChunkedCleanup(ctx context.Context, minInterval, maxInterval time.Duration, defaultChunkSize int, deleter func(context.Context, time.Time, int) int) {
|
|
b := &backoff.Backoff{
|
|
Min: minInterval,
|
|
Max: maxInterval,
|
|
Factor: 2,
|
|
Jitter: true,
|
|
}
|
|
|
|
slog.DebugContext(ctx, "Starting chunked clean up", "maxInterval", maxInterval.String(), "size", defaultChunkSize)
|
|
|
|
deleteChunk := defaultChunkSize
|
|
|
|
for running := true; running; {
|
|
select {
|
|
case <-ctx.Done():
|
|
running = false
|
|
case <-time.After(b.Duration()):
|
|
deleted := deleter(ctx, time.Now(), deleteChunk)
|
|
if deleted == 0 {
|
|
deleteChunk = defaultChunkSize
|
|
continue
|
|
}
|
|
|
|
slog.DebugContext(ctx, "Deleted records", "count", deleted)
|
|
|
|
// in case of any deletes, we want to go back to small interval first
|
|
b.Reset()
|
|
|
|
if deleted == deleteChunk {
|
|
// 1.5 scaling factor
|
|
deleteChunk += deleteChunk / 2
|
|
}
|
|
}
|
|
}
|
|
|
|
slog.DebugContext(ctx, "Finished cleaning up")
|
|
}
|
|
|
|
func ParseDomainName(input string) (string, error) {
|
|
if len(input) == 0 {
|
|
return "", errEmptyDomain
|
|
}
|
|
|
|
parsedURL, err := url.Parse(input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
domain := parsedURL.Host
|
|
if domain == "" {
|
|
domain = input
|
|
}
|
|
|
|
if slashIndex := strings.LastIndex(domain, "/"); slashIndex != -1 {
|
|
domain = domain[:slashIndex]
|
|
}
|
|
|
|
if colonIndex := strings.LastIndex(domain, ":"); colonIndex != -1 {
|
|
domain = domain[:colonIndex]
|
|
}
|
|
|
|
return domain, nil
|
|
}
|
|
|
|
func IsLocalhost(address string) bool {
|
|
return (address == "localhost") ||
|
|
(address == "127.0.0.1") ||
|
|
(address == "::1") ||
|
|
(address == "0:0:0:0:0:0:0:1")
|
|
}
|
|
|
|
func IsIPAddress(str string) bool {
|
|
_, err := netip.ParseAddr(str)
|
|
return err == nil
|
|
}
|
|
|
|
func IsSubDomainOrDomain(subDomain, domain string) bool {
|
|
if len(subDomain) == 0 || len(domain) == 0 {
|
|
return false
|
|
}
|
|
|
|
if len(subDomain) < len(domain) {
|
|
return false
|
|
}
|
|
|
|
if strings.HasSuffix(subDomain, domain) {
|
|
if lenDiff := len(subDomain) - len(domain); lenDiff > 0 {
|
|
prefix := subDomain[:lenDiff]
|
|
return strings.HasSuffix(prefix, ".") && lenDiff > 1
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func EnvToBool(value string) bool {
|
|
switch value {
|
|
case "1", "Y", "y", "yes", "true", "YES", "TRUE":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// RetriableError is a wrapper for errors that should be retried.
|
|
type RetriableError struct {
|
|
err error
|
|
}
|
|
|
|
func NewRetriableError(err error) RetriableError {
|
|
return RetriableError{err}
|
|
}
|
|
|
|
func (e RetriableError) Error() string {
|
|
return e.err.Error()
|
|
}
|
|
|
|
func (e RetriableError) Unwrap() error {
|
|
return e.err
|
|
}
|