diff options
| author | Nicolas James <Eele1Ephe7uZahRie@tutanota.com> | 2025-02-13 18:04:18 +1100 |
|---|---|---|
| committer | Nicolas James <Eele1Ephe7uZahRie@tutanota.com> | 2025-02-13 18:04:18 +1100 |
| commit | 93dfe2be64e8658839bcfe5356adf35f8cde7075 (patch) | |
| tree | c60b1e20d569b74dbde85123e1b2bf3590c66244 /src/server/helper | |
initial commit
Diffstat (limited to 'src/server/helper')
| -rw-r--r-- | src/server/helper/clean.go | 37 | ||||
| -rw-r--r-- | src/server/helper/crypto.go | 46 | ||||
| -rw-r--r-- | src/server/helper/error.go | 36 | ||||
| -rw-r--r-- | src/server/helper/jwt.go | 66 | ||||
| -rw-r--r-- | src/server/helper/valid.go | 70 |
5 files changed, 255 insertions, 0 deletions
diff --git a/src/server/helper/clean.go b/src/server/helper/clean.go new file mode 100644 index 0000000..35937de --- /dev/null +++ b/src/server/helper/clean.go @@ -0,0 +1,37 @@ +package helper + +import ( + "regexp" +) + +func removeTrailingWhitespace(str string) string { + re := regexp.MustCompile(`(^\s+)|(\s+$)`) + return re.ReplaceAllString(str, "") +} + +func removeDuplicateWhitespace(str string) string { + re := regexp.MustCompile(`\s{2,}`) + return re.ReplaceAllString(str, " ") +} + +func removeNewlines(str string) string { + re := regexp.MustCompile(`\n+`) + return re.ReplaceAllString(str, "") +} + +func removeDuplicateNewlines(str string) string { + re := regexp.MustCompile(`\n{2,}`) + return re.ReplaceAllString(str, "\n") +} + +func CleanTitle(title string) string { + title = removeDuplicateWhitespace(title) + title = removeNewlines(title) + return removeTrailingWhitespace(title) +} + +func CleanContents(contents string) string { + contents = removeDuplicateWhitespace(contents) + contents = removeDuplicateNewlines(contents) + return removeTrailingWhitespace(contents) +} diff --git a/src/server/helper/crypto.go b/src/server/helper/crypto.go new file mode 100644 index 0000000..d676fba --- /dev/null +++ b/src/server/helper/crypto.go @@ -0,0 +1,46 @@ +package helper + +import ( + "crypto/rand" + "encoding/base64" + + "golang.org/x/crypto/scrypt" +) + +// Recommended https://pkg.go.dev/golang.org/x/crypto/scrypt +const SCRYPT_N = 32768 +const SCRYPT_R = 8 +const SCRYPT_P = 1 +const SCRYPT_L = 64 + +const SALT_LENGTH = SCRYPT_L + +func getEncoding() *base64.Encoding { + return base64.StdEncoding.WithPadding(base64.NoPadding) +} + +// Generates truly random bytes. +func generateRandomBytes(len int) ([]byte, error) { + salt := make([]byte, len) + _, err := rand.Read(salt) + if err != nil { + return nil, err + } + return salt, nil +} + +func GenerateSalt() (string, error) { + salt, err := generateRandomBytes(SALT_LENGTH) + if err != nil { + return "", err + } + return getEncoding().EncodeToString(salt), nil +} + +func GenerateHash(password string, salt string) (string, error) { + hash, err := scrypt.Key([]byte(password), []byte(salt), SCRYPT_N, SCRYPT_R, SCRYPT_P, SCRYPT_L) + if err != nil { + return "", err + } + return getEncoding().EncodeToString(hash), nil +} diff --git a/src/server/helper/error.go b/src/server/helper/error.go new file mode 100644 index 0000000..8c76304 --- /dev/null +++ b/src/server/helper/error.go @@ -0,0 +1,36 @@ +package helper + +import ( + "encoding/json" + "log" + "net/http" +) + +type jsonError struct { + Message string `json:"error"` +} + +func WriteInternalError(err error, writer http.ResponseWriter) { + log.Printf("internal server error: %s\n", err) + http.Error(writer, "internal server error", http.StatusInternalServerError) +} + +func WriteErrorJson(message string, writer http.ResponseWriter, code int) { + json_error := &jsonError{ + Message: message, + } + + marshal, err := json.Marshal(json_error) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + + http.Error(writer, string(marshal), code) +} + +// Logs the error and responds with a generic server error. +func WriteInternalErrorJson(err error, writer http.ResponseWriter) { + log.Printf("internal server error: %s\n", err) + WriteErrorJson("internal server error", writer, http.StatusInternalServerError) +} diff --git a/src/server/helper/jwt.go b/src/server/helper/jwt.go new file mode 100644 index 0000000..ae127bd --- /dev/null +++ b/src/server/helper/jwt.go @@ -0,0 +1,66 @@ +package helper + +import ( + "net/http" + "strconv" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +// WARNING this key should be secret and constant between deployments. +// If you use this software in the wild, at the very least change this value! +// https://www.sohamkamani.com/golang/jwt-authentication/ +var jwt_key = []byte("iph7noo1ohQuam5sou5wa2aeChixo7") + +func IssueToken(uid int, writer http.ResponseWriter) error { + expiration := time.Now().Add(60 * time.Minute) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + Subject: strconv.Itoa(uid), + ExpiresAt: jwt.NewNumericDate(expiration), + Issuer: "react-go-forum", + }) + token_string, err := token.SignedString(jwt_key) + if err != nil { + return err + } + + http.SetCookie(writer, &http.Cookie{ + Name: "token", + Value: token_string, + Expires: expiration, + SameSite: http.SameSiteStrictMode, + Path: "/", + }) + return nil +} + +// Returns a non-nil RegisteredClaims if valid, nil otherwise. Handles responding to bad tokens. +func GetValidClaims(writer http.ResponseWriter, request *http.Request) *jwt.RegisteredClaims { + cookie, err := request.Cookie("token") + if err != nil { + if err == http.ErrNoCookie { + WriteErrorJson("access denied", writer, http.StatusUnauthorized) + return nil + } + + WriteInternalErrorJson(err, writer) + return nil + } + + claims := &jwt.RegisteredClaims{} + token, err := jwt.ParseWithClaims(cookie.Value, claims, func(token *jwt.Token) (interface{}, error) { + return jwt_key, nil + }) + if err != nil { + WriteInternalErrorJson(err, writer) + return nil + } + + if !token.Valid { + WriteErrorJson("access denied", writer, http.StatusUnauthorized) + return nil + } + + return claims +} diff --git a/src/server/helper/valid.go b/src/server/helper/valid.go new file mode 100644 index 0000000..9363747 --- /dev/null +++ b/src/server/helper/valid.go @@ -0,0 +1,70 @@ +package helper + +import ( + "image" + "net/http" +) + +// Checks if the subreact exists, responds with a bad status when false. +func IsValidSubreact(subreact string, writer http.ResponseWriter) bool { + switch subreact { + case "": + fallthrough + case "t": + fallthrough + case "g": + fallthrough + case "k": + fallthrough + case "p": + fallthrough + case "a": + fallthrough + case "pr": + fallthrough + case "m": + break + default: + WriteErrorJson("invalid subreact", writer, http.StatusBadRequest) + return false + } + return true +} + +// Range checks the length of the argument, responds with a bad status when false. +func IsValidRange(str string, name string, min int, max int, writer http.ResponseWriter) bool { + if len(str) < min { + WriteErrorJson(name+" too short", writer, http.StatusBadRequest) + return false + } + if len(str) > max { + WriteErrorJson(name+" too long", writer, http.StatusBadRequest) + return false + } + return true +} + +// Checks that the image has supported properties, responds with a bad status when false. +func IsImageSupported(image image.Image, format string, writer http.ResponseWriter) bool { + if bounds := image.Bounds(); bounds.Dx() > 4096 || bounds.Dy() > 4096 { + WriteErrorJson("image dimensions too large", writer, http.StatusBadRequest) + return false + } else if bounds.Dx() < 256 || bounds.Dy() < 256 { + WriteErrorJson("image dimensions too small", writer, http.StatusBadRequest) + return false + } + + switch format { + case "png": + fallthrough + case "jpeg": + fallthrough + case "gif": + break + default: + WriteErrorJson("image format no recognised", writer, http.StatusBadRequest) + return false + } + + return true +} |
