Files
darkstorm-server/internal/backend/darkstorm.go
T
Caleb Gardner 2afbd64dc2 Added CallbackApp to better access backend.Backend from with in an App
Fixed issues with CORS not being able to do an OPTIONS request
Fixed folders not being processed properly.
Fixed some other issues.
2024-08-07 02:24:54 -05:00

157 lines
4.3 KiB
Go

package backend
import (
"crypto/ed25519"
"embed"
"encoding/json"
"errors"
"log"
"net/http"
"sync"
"time"
)
//go:embed robots.txt
var robotEmbed embed.FS
// A simple backend that handles user authentication, user count, and crash reports.
type Backend struct {
userTable Table[User]
keyTable Table[ApiKey]
m *http.ServeMux
apps map[string]App
managementKeyID string
corsAddr string
jwtPriv ed25519.PrivateKey
jwtPub ed25519.PublicKey
userMutex sync.RWMutex
}
// Create a new Backend with the given apps. keyTable must be specified.
func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) {
b := &Backend{
keyTable: keyTable,
m: &http.ServeMux{},
apps: make(map[string]App),
userMutex: sync.RWMutex{},
}
b.m.Handle("GET /robots.txt", http.FileServerFS(robotEmbed))
var hasLog, hasCrash bool
for i := range apps {
_, has := b.apps[apps[i].AppID()]
if has {
return nil, errors.New("duplicate AppIDs found")
}
b.apps[apps[i].AppID()] = apps[i]
if ext, is := apps[i].(ExtendedApp); is {
ext.Extension(b.m)
}
if back, is := apps[i].(CallbackApp); is {
back.AddBackend(b)
}
if !hasLog && apps[i].CountTable() != nil {
hasLog = true
}
if !hasCrash && apps[i].CrashTable() != nil {
hasCrash = true
}
}
if hasLog {
b.m.HandleFunc("POST /count", b.countLog)
b.m.HandleFunc("GET /count", b.getCount)
//TODO: Remove legacy paths
b.m.HandleFunc("POST /log", b.countLog)
}
if hasCrash {
b.m.HandleFunc("POST /crash", b.reportCrash)
b.m.HandleFunc("DELETE /crash/{crashID}", b.deleteCrash)
b.m.HandleFunc("POST /crash/archive", b.archiveCrash)
}
b.m.HandleFunc("OPTIONS /", func(_ http.ResponseWriter, _ *http.Request) {}) //Here to send just CORS data.
go b.cleanupLoop()
return b, nil
}
func (b *Backend) cleanupLoop() {
for range time.Tick(24 * time.Hour) {
old := getDate(time.Now().Add(-30 * 24 * time.Hour))
var err error
for _, a := range b.apps {
log.Printf("Removing logs for %v", a.AppID())
tab := a.CountTable()
if tab == nil {
continue
}
err = tab.RemoveOldLogs(old)
if err != nil {
log.Printf("error removing old logs for %v: %v\n", a.AppID(), err)
}
}
}
}
// Enable CORS for with the given cors address
func (b *Backend) AddCorsAddress(corsAddr string) {
b.corsAddr = corsAddr
}
// http.Handler
func (b *Backend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if b.corsAddr != "" {
w.Header().Set("Access-Control-Allow-Origin", b.corsAddr)
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Authorization, X-API-Key, Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers")
}
}
b.m.ServeHTTP(w, r)
}
func getDate(t time.Time) int {
return (t.Year() * 10000) + (int(t.Month()) * 100) + t.Day()
}
// Enables the use of a management API key for crash and count.
func (b *Backend) EnableManagementKey(managementID string) {
b.managementKeyID = managementID
b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.managementDeleteCrash)
b.m.HandleFunc("POST /{appID}/crash/archive", b.managementArchiveCrash)
b.m.HandleFunc("GET /{appID}/count", b.getCount)
}
// Enables user creation and authentication.
func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) {
b.userTable = userTable
b.jwtPriv = privKey
b.jwtPub = pubKey
b.m.HandleFunc("POST /user/create", b.createUser)
b.m.HandleFunc("DELETE /user/{userID}", b.deleteUser)
b.m.HandleFunc("POST /user/login", b.login)
}
// Add values to the Backend's underlying ServeMux
func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc) {
b.m.HandleFunc(pattern, h)
}
// Try to get the App associated with the given ApiKey. Returns nil if not found.
func (b *Backend) GetApp(a *ApiKey) App {
return b.apps[a.AppID]
}
type retError struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
// Return an error response with the given status code, code, and message.
func ReturnError(w http.ResponseWriter, status int, code, msg string) {
w.WriteHeader(status)
json.NewEncoder(w).Encode(retError{
ErrorCode: code,
ErrorMsg: msg,
})
}