aboutsummaryrefslogtreecommitdiff
path: root/src/server/helper
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/helper')
-rw-r--r--src/server/helper/clean.go37
-rw-r--r--src/server/helper/crypto.go46
-rw-r--r--src/server/helper/error.go36
-rw-r--r--src/server/helper/jwt.go66
-rw-r--r--src/server/helper/valid.go70
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
+}