614 lines
18 KiB
Go
614 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"database/sql"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
func verifyJwt(token string, publicKey ed25519.PublicKey) (jwt.MapClaims, bool) {
|
|
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
|
return publicKey, nil
|
|
}, jwt.WithValidMethods([]string{"EdDSA"}))
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
if !parsedToken.Valid {
|
|
return nil, false
|
|
}
|
|
|
|
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
return claims, true
|
|
}
|
|
|
|
func renderJSON(data interface{}, w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err := json.NewEncoder(w).Encode(data)
|
|
if err != nil {
|
|
http.Error(w, "Internal server error", 500)
|
|
slog.Error("Failed to encode JSON: " + err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
//goland:noinspection SqlNoDataSourceInspection,GoDfaErrorMayBeNotNil,GoDfaNilDereference
|
|
func main() {
|
|
var publicKey ed25519.PublicKey
|
|
var privateKey ed25519.PrivateKey
|
|
privateKey, err := os.ReadFile("key.pem")
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
publicKey, privateKey, err = ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
slog.Error("Failed to generate private key: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
err = os.WriteFile("key.pem", privateKey, 0600)
|
|
if err != nil {
|
|
slog.Error("Failed to write private key: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
slog.Error("Failed to read private key: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
publicKey = privateKey.Public().(ed25519.PublicKey)
|
|
}
|
|
|
|
conn, err := sql.Open("sqlite3", "database.db")
|
|
if err != nil || conn == nil {
|
|
slog.Error("Failed to open database: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB NOT NULL PRIMARY KEY, username TEXT NOT NULL UNIQUE, publicKey BLOB NOT NULL, administrator INTEGER NOT NULL DEFAULT 0 CHECK(administrator IN (0, 1)))")
|
|
if err != nil {
|
|
slog.Error("Failed to create users table: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS invites (code BLOB NOT NULL UNIQUE, uses INTEGER NOT NULL)")
|
|
if err != nil {
|
|
slog.Error("Failed to create invites table: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS messages (sender BLOB NOT NULL, senderName TEXT NOT NULL, message TEXT NOT NULL, sent INTEGER NOT NULL, id BLOB NOT NULL PRIMARY KEY)")
|
|
if err != nil {
|
|
slog.Error("Failed to create messages table: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
subscriptions := make(map[time.Time]chan bool)
|
|
|
|
router.Post("/api/signup", func(w http.ResponseWriter, r *http.Request) {
|
|
var data struct {
|
|
Username string `json:"username"`
|
|
PublicKey string `json:"publicKey"`
|
|
Invite string `json:"invite"`
|
|
}
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
if data.Username == "" || data.PublicKey == "" || data.Invite == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
inviteBytes, err := hex.DecodeString(data.Invite)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid invite"}, w)
|
|
return
|
|
}
|
|
|
|
var uses int
|
|
err = conn.QueryRow("SELECT uses FROM invites WHERE code = ?", inviteBytes).Scan(&uses)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid invite"}, w)
|
|
return
|
|
}
|
|
|
|
_, err = conn.Exec("UPDATE invites SET uses = uses - 1 WHERE code = ?", inviteBytes)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to update invite: " + err.Error())
|
|
return
|
|
}
|
|
|
|
if uses-1 <= 0 {
|
|
_, err := conn.Exec("DELETE FROM invites WHERE uses <= 0")
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to delete invite: " + err.Error())
|
|
return
|
|
}
|
|
if uses <= 0 {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid invite"}, w)
|
|
return
|
|
}
|
|
}
|
|
|
|
userId, err := uuid.NewRandom()
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to generate UUID: " + err.Error())
|
|
return
|
|
}
|
|
|
|
publicKeyBytes, err := base64.StdEncoding.DecodeString(data.PublicKey)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid public key"}, w)
|
|
return
|
|
}
|
|
|
|
_, err = conn.Exec("INSERT INTO users (id, username, publicKey) VALUES (?, ?, ?)", userId, data.Username, publicKeyBytes)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Username already taken"}, w)
|
|
return
|
|
}
|
|
|
|
var nonce [32]byte
|
|
read, err := rand.Read(nonce[:])
|
|
if err != nil || read != 32 {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to generate nonce: " + err.Error())
|
|
return
|
|
}
|
|
|
|
token, err := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
|
|
"sub": userId.String(),
|
|
"name": data.Username,
|
|
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
|
"nonce": base64.StdEncoding.EncodeToString(nonce[:]),
|
|
}).SignedString(privateKey)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to sign token: " + err.Error())
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
renderJSON(map[string]interface{}{"token": token, "userId": userId.String()}, w)
|
|
})
|
|
|
|
router.Post("/api/loginChallenge", func(w http.ResponseWriter, r *http.Request) {
|
|
var data struct {
|
|
Username string `json:"username"`
|
|
}
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
var nonce [32]byte
|
|
read, err := rand.Read(nonce[:])
|
|
if err != nil || read != 32 {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to generate nonce: " + err.Error())
|
|
return
|
|
}
|
|
|
|
token, err := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
|
|
"sub": data.Username,
|
|
"exp": time.Now().Add(time.Second * 20).Unix(),
|
|
"nonce": base64.StdEncoding.EncodeToString(nonce[:]),
|
|
}).SignedString(privateKey)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to sign token: " + err.Error())
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
renderJSON(map[string]interface{}{"token": token, "nonce": base64.StdEncoding.EncodeToString(nonce[:])}, w)
|
|
})
|
|
|
|
router.Post("/api/login", func(w http.ResponseWriter, r *http.Request) {
|
|
var data struct {
|
|
Username string `json:"username"`
|
|
Signature string `json:"signature"`
|
|
Challenge string `json:"challenge"`
|
|
}
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
claims, ok := verifyJwt(data.Challenge, publicKey)
|
|
if !ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid challenge"}, w)
|
|
return
|
|
}
|
|
|
|
nonce, err := base64.StdEncoding.DecodeString(claims["nonce"].(string))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid challenge"}, w)
|
|
return
|
|
}
|
|
|
|
signature, err := base64.StdEncoding.DecodeString(data.Signature)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid signature"}, w)
|
|
return
|
|
}
|
|
|
|
var userKey []byte
|
|
err = conn.QueryRow("SELECT publicKey FROM users WHERE username = ?", data.Username).Scan(&userKey)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Println(err.Error())
|
|
renderJSON(map[string]interface{}{"error": "Invalid username"}, w)
|
|
return
|
|
}
|
|
|
|
if !ed25519.Verify(userKey, nonce, signature) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid signature"}, w)
|
|
return
|
|
}
|
|
|
|
var userId uuid.UUID
|
|
var administrator int
|
|
err = conn.QueryRow("SELECT administrator, id FROM users WHERE username = ?", data.Username).Scan(&administrator, &userId)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid username"}, w)
|
|
return
|
|
}
|
|
|
|
var loginNonce [32]byte
|
|
read, err := rand.Read(loginNonce[:])
|
|
if err != nil || read != 32 {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to generate nonce: " + err.Error())
|
|
return
|
|
}
|
|
|
|
token, err := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
|
|
"sub": userId.String(),
|
|
"name": data.Username,
|
|
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
|
"nonce": base64.StdEncoding.EncodeToString(loginNonce[:]),
|
|
}).SignedString(privateKey)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to sign token: " + err.Error())
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
returnJSON := map[string]interface{}{"token": token, "userId": userId.String()}
|
|
if administrator == 1 {
|
|
returnJSON["admin"] = true
|
|
}
|
|
renderJSON(returnJSON, w)
|
|
})
|
|
|
|
router.Post("/api/invite", func(w http.ResponseWriter, r *http.Request) {
|
|
var data struct {
|
|
Token string `json:"token"`
|
|
Uses float64 `json:"uses"`
|
|
}
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
claims, ok := verifyJwt(data.Token, publicKey)
|
|
if !ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
userId, err := uuid.Parse(claims["sub"].(string))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
var admin bool
|
|
err = conn.QueryRow("SELECT administrator FROM users WHERE id = ?", userId).Scan(&admin)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
if !admin {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
var code [8]byte
|
|
read, err := rand.Read(code[:])
|
|
if err != nil || read != 8 {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to generate code: " + err.Error())
|
|
return
|
|
}
|
|
|
|
_, err = conn.Exec("INSERT INTO invites (code, uses) VALUES (?, ?)", code[:], data.Uses)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to insert invite: " + err.Error())
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
renderJSON(map[string]interface{}{"code": hex.EncodeToString(code[:])}, w)
|
|
})
|
|
|
|
router.Post("/api/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
var data struct {
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
_, ok := verifyJwt(data.Token, publicKey)
|
|
if !ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
var messages []map[string]interface{}
|
|
rows, err := conn.Query("SELECT sender, senderName, message, sent, id FROM messages ORDER BY sent ASC")
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to select messages: " + err.Error())
|
|
return
|
|
}
|
|
|
|
for rows.Next() {
|
|
var sender uuid.UUID
|
|
var senderName string
|
|
var message string
|
|
var sent float64
|
|
var id uuid.UUID
|
|
err = rows.Scan(&sender, &senderName, &message, &sent, &id)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to scan message: " + err.Error())
|
|
return
|
|
}
|
|
|
|
messages = append(messages, map[string]interface{}{
|
|
"sender": sender.String(),
|
|
"name": senderName,
|
|
"message": message,
|
|
"sent": sent,
|
|
"id": id.String(),
|
|
})
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
renderJSON(messages, w)
|
|
})
|
|
|
|
router.Post("/api/send", func(w http.ResponseWriter, r *http.Request) {
|
|
var data struct {
|
|
Token string `json:"token"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
claims, ok := verifyJwt(data.Token, publicKey)
|
|
if !ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
userId, err := uuid.Parse(claims["sub"].(string))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
messageId, err := uuid.NewRandom()
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to generate UUID: " + err.Error())
|
|
return
|
|
}
|
|
|
|
_, err = conn.Exec("INSERT INTO messages (sender, message, sent, id, senderName) VALUES (?, ?, ?, ?, ?)", userId, data.Message, time.Now().Unix(), messageId, claims["name"])
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to insert message: " + err.Error())
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
for subscriptionId, subscription := range subscriptions {
|
|
select {
|
|
case subscription <- false:
|
|
default:
|
|
subscriptions[subscriptionId] = nil
|
|
}
|
|
}
|
|
}()
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
router.Post("/api/delete", func(w http.ResponseWriter, r *http.Request) {
|
|
var data struct {
|
|
Token string `json:"token"`
|
|
Id string `json:"id"`
|
|
}
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid request"}, w)
|
|
return
|
|
}
|
|
|
|
claims, ok := verifyJwt(data.Token, publicKey)
|
|
if !ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
userId, err := uuid.Parse(claims["sub"].(string))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid token"}, w)
|
|
return
|
|
}
|
|
|
|
messageId, err := uuid.Parse(data.Id)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
renderJSON(map[string]interface{}{"error": "Invalid message ID"}, w)
|
|
return
|
|
}
|
|
|
|
_, err = conn.Exec("DELETE FROM messages WHERE sender = ? AND id = ?", userId, messageId)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
renderJSON(map[string]interface{}{"error": "Internal server error"}, w)
|
|
slog.Error("Failed to delete message: " + err.Error())
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
for subscriptionId, subscription := range subscriptions {
|
|
select {
|
|
case subscription <- false:
|
|
default:
|
|
subscriptions[subscriptionId] = nil
|
|
}
|
|
}
|
|
}()
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
router.HandleFunc("/api/ping", func(w http.ResponseWriter, r *http.Request) {
|
|
slog.Info("New client online at " + r.RemoteAddr)
|
|
r.Host = "example.org"
|
|
r.Header.Set("Origin", "https://example.org")
|
|
connection, err := websocket.Accept(w, r, nil)
|
|
if err != nil {
|
|
slog.Error("Failed to accept WebSocket connection: " + err.Error())
|
|
return
|
|
}
|
|
|
|
subscriptionId := time.Now()
|
|
|
|
subscription := make(chan bool)
|
|
subscriptions[subscriptionId] = subscription
|
|
|
|
go func() {
|
|
for {
|
|
shouldClose := <-subscription
|
|
if shouldClose {
|
|
return
|
|
} else {
|
|
err := connection.Write(context.Background(), websocket.MessageBinary, []byte{0x00})
|
|
if err != nil {
|
|
println(err.Error())
|
|
slog.Info("Client disconnected: " + r.RemoteAddr)
|
|
subscriptions[subscriptionId] = nil
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
})
|
|
|
|
err = http.ListenAndServe(":1974", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
if r.Method != "OPTIONS" {
|
|
router.ServeHTTP(w, r)
|
|
} else {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}))
|
|
if err != nil {
|
|
slog.Error("Failed to start server: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|