Compare commits

...

2 Commits

12 changed files with 261 additions and 14 deletions

30
bootstrap/loader.go Normal file
View File

@@ -0,0 +1,30 @@
package bootstrap
import (
"encoding/json"
"fmt"
"os"
"synlotto-website/models"
)
type AppState struct {
Config *models.Config
}
func LoadAppState(configPath string) (*AppState, error) {
file, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("open config: %w", err)
}
defer file.Close()
var config models.Config
if err := json.NewDecoder(file).Decode(&config); err != nil {
return nil, fmt.Errorf("decode config: %w", err)
}
return &AppState{
Config: &config,
}, nil
}

22
config/config.go Normal file
View File

@@ -0,0 +1,22 @@
package config
import (
"sync"
"synlotto-website/models"
)
var (
appConfig *models.Config
once sync.Once
)
func Init(config *models.Config) {
once.Do(func() {
appConfig = config
})
}
func Get() *models.Config {
return appConfig
}

View File

@@ -8,14 +8,16 @@ import (
"regexp"
"sort"
"strconv"
"synlotto-website/helpers"
"synlotto-website/middleware"
"synlotto-website/models"
)
func ResultsThunderball(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
limiter := helpers.GetVisitorLimiter(ip)
limiter := middleware.GetVisitorLimiter(ip)
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)

26
handlers/security/csrf.go Normal file
View File

@@ -0,0 +1,26 @@
package security
import (
"fmt"
"net/http"
"github.com/gorilla/csrf"
)
var CSRFMiddleware func(http.Handler) http.Handler
func InitCSRFProtection(csrfKey []byte, isProduction bool) error {
if len(csrfKey) != 32 {
return fmt.Errorf("csrf key must be 32 bytes, got %d", len(csrfKey))
}
CSRFMiddleware = csrf.Protect(
csrfKey,
csrf.Secure(isProduction),
csrf.SameSite(csrf.SameSiteStrictMode),
csrf.Path("/"),
csrf.HttpOnly(true),
)
return nil
}

View File

@@ -0,0 +1,94 @@
package security
import (
"bytes"
"encoding/gob"
"fmt"
"net/http"
"os"
"time"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
var (
sessionStore *sessions.CookieStore
sessionName string
authKey []byte
encryptKey []byte
)
func init() {
gob.Register(time.Time{})
}
func LoadSessionKeys(authPath, encryptionPath, name string, isProduction bool) error {
var err error
authKey, err = os.ReadFile(authPath)
if err != nil {
return fmt.Errorf("error loading auth key: %w", err)
}
encryptKey, err = os.ReadFile(encryptionPath)
if err != nil {
return fmt.Errorf("error loading encryption key: %w", err)
}
authKey = bytes.TrimSpace(authKey)
encryptKey = bytes.TrimSpace(encryptKey)
if len(authKey) != 32 || len(encryptKey) != 32 {
return fmt.Errorf("auth and encryption keys must be 32 bytes each")
}
sessionStore = sessions.NewCookieStore(authKey, encryptKey)
sessionStore.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 1,
HttpOnly: true,
Secure: isProduction,
SameSite: http.SameSiteLaxMode,
}
sessionName = name
return nil
}
func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
return sessionStore.Get(r, sessionName)
}
func SecureCookie(w http.ResponseWriter, name, value string, isProduction bool) error {
s := securecookie.New(authKey, encryptKey)
encoded, err := s.Encode(name, value)
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: name,
Value: encoded,
Path: "/",
HttpOnly: true,
Secure: isProduction,
SameSite: http.SameSiteStrictMode,
})
return nil
}
func LoadKeyFromFile(path string) ([]byte, error) {
key, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return bytes.TrimSpace(key), nil
}
func ZeroBytes(b []byte) {
for i := range b {
b[i] = 0
}
}

24
logging/config.go Normal file
View File

@@ -0,0 +1,24 @@
package logging
import (
"encoding/json"
"log"
"synlotto-website/models"
)
func LogConfig(config *models.Config) {
safeConfig := *config
safeConfig.CSRF.CSRFKey = "[REDACTED]"
safeConfig.Session.SessionAuthKey = "[REDACTED]"
safeConfig.Session.SessionEncryptionKey = "[REDACTED]"
cfg, err := json.MarshalIndent(safeConfig, "", " ")
if err != nil {
log.Println("Failed to log config:", err)
return
}
log.Println("App starting with config:")
log.Println(string(cfg))
}

13
logging/messages.go Normal file
View File

@@ -0,0 +1,13 @@
package logging
import (
"log"
)
func Info(msg string, args ...any) {
log.Printf("[INFO] "+msg, args...)
}
func Error(msg string, args ...any) {
log.Printf("[ERROR] "+msg, args...)
}

32
main.go
View File

@@ -3,27 +3,34 @@ package main
import (
"log"
"net/http"
securityhandlers "synlotto-website/handlers/security"
"synlotto-website/bootstrap"
"synlotto-website/config"
"synlotto-website/handlers"
"synlotto-website/helpers"
"synlotto-website/logging"
"synlotto-website/middleware"
"synlotto-website/models"
"synlotto-website/routes"
"synlotto-website/storage"
"github.com/gorilla/csrf"
)
func main() {
appState, err := bootstrap.LoadAppState("config/config.json")
if err != nil {
logging.Error("Failed to load app state: %v", err)
}
config.Init(appState.Config)
logging.LogConfig(appState.Config)
db := storage.InitDB("synlotto.db")
models.SetDB(db) // Should be in storage not models.
var isProduction = false
csrfMiddleware := csrf.Protect(
[]byte("abcdefghijklmnopqrstuvwx12345678"), // TodO: Make Global
csrf.Secure(true),
csrf.Path("/"),
)
err = securityhandlers.InitCSRFProtection([]byte(appState.Config.CSRF.CSRFKey), appState.Config.HttpServer.ProductionMode)
if err != nil {
logging.Error("Failed to init CSRF: %v", err)
}
mux := http.NewServeMux()
routes.SetupAdminRoutes(mux, db)
@@ -34,8 +41,9 @@ func main() {
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
mux.HandleFunc("/", handlers.Home(db))
wrapped := helpers.RateLimit(csrfMiddleware(mux))
wrapped = middleware.EnforceHTTPS(wrapped, isProduction)
wrapped := securityhandlers.CSRFMiddleware(mux)
wrapped = middleware.RateLimit(wrapped)
wrapped = middleware.EnforceHTTPS(wrapped, appState.Config.HttpServer.ProductionMode)
wrapped = middleware.SecureHeaders(wrapped)
wrapped = middleware.Recover(wrapped)

View File

@@ -1,4 +1,4 @@
package helpers
package middleware
import (
"net"

19
models/config.go Normal file
View File

@@ -0,0 +1,19 @@
package models
type Config struct {
HttpServer struct {
Port int `json:"port"`
Address string `json:"address"`
ProductionMode bool `json:"productionMode"`
} `json:"httpServer"`
CSRF struct {
CSRFKey string `json:"csrfKey"`
} `json:"csrf"`
Session struct {
SessionAuthKey string `json:"authKey"`
SessionEncryptionKey string `json:"encryptionKey"`
SessionName string `json:"sessionName"`
} `json:"session"`
}

View File

@@ -4,10 +4,15 @@ import (
"database/sql"
"log"
"synlotto-website/config"
"synlotto-website/logging"
_ "modernc.org/sqlite"
)
func InitDB(filepath string) *sql.DB {
var err error
cfg := config.Get()
db, err := sql.Open("sqlite", filepath)
if err != nil {
log.Fatal("❌ Failed to open DB:", err)
@@ -30,6 +35,10 @@ func InitDB(filepath string) *sql.DB {
SchemaSyndicateInvites,
SchemaSyndicateInviteTokens,
}
if cfg == nil {
logging.Error("❌ config is nil — did config.Init() run before InitDB?")
panic("config not ready")
}
for _, stmt := range schemas {
if _, err := db.Exec(stmt); err != nil {