lurkcoin-core/lurkcoin/misc.go

237 lines
6.6 KiB
Go
Raw Permalink Blame History

//
// lurkcoin
// Copyright © 2020 by luk3yx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
package lurkcoin
import (
crypto_rand "crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"log"
"math"
"math/big"
"math/rand"
"net/url"
"regexp"
"strings"
"unicode"
)
const SYMBOL = "¤"
const VERSION = "3.0.10"
// Note that public source code is required by the AGPL
const SOURCE_URL = "https://github.com/luk3yx/lurkcoin-core"
const REPORT_SECURITY = "https://gitlab.com/luk3yx/lurkcoin-core/-/issues/new"
// Copyrights should be separated by newlines
const COPYRIGHT = "Copyright © 2021 by luk3yx"
func PrintASCIIArt() {
log.Print(`/\___/\ _ _ _`)
log.Print(`\ _ / | |_ _ _ __| | _____ ___ (_)_ __`)
log.Print(`| (_) | | | | | | '__| |/ / __/ _ \| | '_ \`)
log.Print(`/ ___ \ | | |_| | | | < (_| (_) | | | | |`)
log.Print(`\/ \/ |_|\__,_|_| |_|\_\___\___/|_|_| |_|`)
log.Print()
log.Printf("Version %s", VERSION)
}
var c0 Currency = CurrencyFromInt64(0)
var invalid_uid = regexp.MustCompile(`[^a-z0-9\_]`)
func HomogeniseUsername(username string) string {
username = strings.ToLower(username)
username = strings.ReplaceAll(username, " ", "")
return invalid_uid.ReplaceAllLiteralString(username, "_")
}
// Remove control characters and leading+trailing whitespace from a username.
// HomogeniseUsername(PasteuriseUsername(username)) should always equal
// HomogeniseUsername(username).
func PasteuriseUsername(username string) (res string, runeCount int) {
res = strings.Map(func(r rune) rune {
runeCount += 1
if unicode.IsGraphic(r) {
return r
}
return '<27>'
}, strings.Trim(username, " "))
return
}
// Gets a random uint64 with crypto/rand, casts it to an int64 and feeds it to
// rand.Seed().
func SeedPRNG() {
max := new(big.Int).SetUint64(math.MaxUint64)
res, err := crypto_rand.Int(crypto_rand.Reader, max)
if err != nil {
panic(err)
}
rand.Seed(int64(res.Uint64()))
}
// Generate a secure random API token. This will probably be around 171
// characters long.
func GenerateToken() string {
// Get 128 random bytes (1024 bits).
raw := make([]byte, 128)
_, err := crypto_rand.Read(raw)
if err != nil {
panic(err)
}
// Encode it with base64.RawURLEncoding
var builder strings.Builder
encoder := base64.NewEncoder(base64.RawURLEncoding, &builder)
encoder.Write(raw)
encoder.Close()
// Return the string
return builder.String()
}
// Validate a webhook URL, returns the actual URL that should be used and a
// boolean indicating success.
func ValidateWebhookURL(rawURL string) (string, bool) {
u, err := url.Parse(rawURL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
return "", false
}
path := u.Path
// Paths always end in /lurkcoin currently.
if !strings.HasSuffix(path, "/lurkcoin") {
if !strings.HasSuffix(path, "/") {
path += "/"
}
path += "lurkcoin"
}
// Create a new URL object without extra parameters.
safeURL := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: path}
return safeURL.String(), true
}
// Get an exchange rate between two servers
func GetExchangeRate(db Database, source, target string, amount Currency) (Currency, error) {
tr := BeginDbTransaction(db)
defer tr.Abort()
source = HomogeniseUsername(source)
target = HomogeniseUsername(target)
if source == target {
return amount, nil
}
// Check the amount against the transaction limit
if amount.Gt(transactionLimit) {
return c0, errors.New("ERR_TRANSACTIONLIMIT")
}
if source != "" {
sourceServer, ok := tr.GetOneServer(source)
if !ok {
return c0, errors.New("ERR_SOURCESERVERNOTFOUND")
}
amount, _ = sourceServer.GetExchangeRate(amount, true)
if amount.Gt(transactionLimit) {
return c0, errors.New("ERR_TRANSACTIONLIMIT")
}
// Abort the transaction now to get the target server
tr.Abort()
}
if target != "" {
targetServer, ok := tr.GetOneServer(target)
if !ok {
return c0, errors.New("ERR_TARGETSERVERNOTFOUND")
}
amount, _ = targetServer.GetExchangeRate(amount, false)
if amount.Gt(transactionLimit) {
return c0, errors.New("ERR_TRANSACTIONLIMIT")
}
}
return amount, nil
}
// A Python-ish repr()
func repr(raw string) string {
res := fmt.Sprintf("%q", raw)
if strings.Count(res, `"`) == 2 && !strings.Contains(res, "'") {
return "'" + res[1:len(res)-1] + "'"
}
return res
}
// A helper used by both lurkcoin/api and lurkcoin/databases.
func GetV2History(summary Summary, appendID bool) (h []string) {
balance := summary.Bal
// This intentionally adds transactions the server sends to itself twice.
for _, transaction := range summary.History {
var suffix string
if appendID {
suffix = " [" + transaction.ID + "]"
}
if transaction.TargetServer == summary.Name {
amount_s := transaction.Amount.DeltaString()
if transaction.SourceServer == "" && transaction.Target == "" {
h = append(h, fmt.Sprintf("%s: %s - %s%s", balance.String(),
amount_s, transaction.Source, suffix))
} else {
h = append(h, fmt.Sprintf(
"%s: %s - Transaction from %s to %s.%s",
balance.String(),
amount_s,
repr(transaction.SourceServer),
repr(transaction.Target),
suffix,
))
}
// Addition and subtraction are swapped because most recent
// transactions are first and we start with the current balance.
balance = balance.Sub(transaction.Amount)
}
if transaction.SourceServer == summary.Name {
amount_s := transaction.Amount.Neg().DeltaString()
h = append(h, fmt.Sprintf("%s: %s - Transaction to %s on %s.%s",
balance.String(), amount_s, repr(transaction.Target),
repr(transaction.TargetServer), suffix))
balance = balance.Add(transaction.Amount)
}
}
if len(h) < 10 {
h = append(h, c0.String()+": Account created")
}
return
}
// Returns true if a == b in a constant time. Note that this will however leak
// string lengths.
func ConstantTimeCompare(a, b string) bool {
// Comparing the length isn't strictly necessary in Go 1.4+, however is
// done anyway.
if len(a) != len(b) {
return false
}
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) != 0
}