package backend import ( "context" "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(context.Background(), 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, }) }