Compare commits

...

12 Commits

Author SHA1 Message Date
Flashfyre
2704e64e38 Add newclear endpoint 2024-05-10 18:07:14 -04:00
maru
e97e5f73d5
Update ghcr.yml 2024-05-10 16:00:47 -04:00
maru
d4a906a0f1
Move HTTPS-related flags in rogueserver.go 2024-05-10 15:50:19 -04:00
maru
693663103b
Run formatter on files 2024-05-10 15:49:26 -04:00
Up
a8502fcd3f
add GitHub actions workflows and build docker image (#9)
* add default values for CLI args

* add development docker compose file

* prevent crash if userdata dir does not exist

* accounts, acccountStats

* account stats and create db indices

* compensations and daily runs

* ensure uniqueness of daily seed

* start on port 8001 by default for client parity

* add GitHub actions scripts and dockerfile

* add os architecture

* only build docker image on main repo

* add example compose file
2024-05-10 15:47:22 -04:00
maru
b5e8094039
Don't return INVALID on seed-related function error 2024-05-10 15:44:35 -04:00
maru
eea2266920
Remove unneeded assertion check in handleSaveData 2024-05-10 15:40:22 -04:00
maru
17294e5179
Fix handleDailySeed 2024-05-10 15:37:24 -04:00
maru
b91c169b16
endpoints.go consistency 2024-05-10 15:33:37 -04:00
Up
3ed5f41d58
make server automatically create DB schema if not exists (#5)
* add default values for CLI args

* add development docker compose file

* prevent crash if userdata dir does not exist

* accounts, acccountStats

* account stats and create db indices

* compensations and daily runs

* ensure uniqueness of daily seed

* start on port 8001 by default for client parity

* make generated schema match production

* sort imports
2024-05-10 15:30:47 -04:00
maru
8a32efeaa3
Clean up rogueserver.go 2024-05-10 13:40:00 -04:00
maru
633142eb29
Allow serving HTTPS 2024-05-10 13:16:35 -04:00
16 changed files with 366 additions and 32 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
/.github/
Dockerfile*
docker-compose*.yml
/.data/
/secret.key
/rogueserver*
!/rogueserver.go

46
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Build
on:
push:
pull_request:
jobs:
build:
name: Build (${{ matrix.os_name }})
env:
GO_VERSION: 1.22
GOOS: ${{ matrix.os_name }}
GOARCH: ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
os_name: linux
arch: amd64
- os: windows-latest
os_name: windows
arch: amd64
# TODO macos needs universal binary!
# - os: macos-latest
# os_name: macos
steps:
- uses: actions/checkout@v4
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install dependencies
run: go mod download
- name: Test
run: go test -v
- name: Build
run: go build -v
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: rogueserver-${{ matrix.os_name }}-${{ matrix.arch }}-${{ github.sha }}
path: |
rogueserver*
!rogueserver.go

38
.github/workflows/ghcr.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Publish to GHCR
on:
push:
jobs:
build:
name: Build and publish to GHCR
if: github.repository == 'pagefaultgames/rogueserver'
env:
GO_VERSION: 1.22
runs-on: ubuntu-latest
steps:
- name: Setup Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Log into container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Build Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
GO_VERSION=${{ env.GO_VERSION }}
VERSION=${{ github.ref_name }}-SNAPSHOT
COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }}

7
.gitignore vendored
View File

@ -1,11 +1,16 @@
# no extension on linux, .exe on windows # no extension on linux, .exe on windows
rogueserver* rogueserver*
userdata/* !/rogueserver/*
/userdata/*
secret.key secret.key
# local testing
/.data/
# Jetbrains IDEs # Jetbrains IDEs
/.idea/ /.idea/
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
.vscode/launch.json

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
ARG GO_VERSION=1.22
FROM golang:${GO_VERSION} AS builder
WORKDIR /src
COPY ./go.mod /src/
COPY ./go.sum /src/
RUN go mod download && go mod verify
COPY . /src/
RUN CGO_ENABLED=0 \
go build -o rogueserver
RUN chmod +x /src/rogueserver
# ---------------------------------------------
FROM scratch
WORKDIR /app
COPY --from=builder /src/rogueserver .
EXPOSE 8001
ENTRYPOINT ["./rogueserver"]

View File

@ -48,6 +48,7 @@ func Init(mux *http.ServeMux) {
mux.HandleFunc("POST /savedata/update", handleSaveData) mux.HandleFunc("POST /savedata/update", handleSaveData)
mux.HandleFunc("GET /savedata/delete", handleSaveData) mux.HandleFunc("GET /savedata/delete", handleSaveData)
mux.HandleFunc("POST /savedata/clear", handleSaveData) mux.HandleFunc("POST /savedata/clear", handleSaveData)
mux.HandleFunc("GET /savedata/newclear", handleNewClear)
// daily // daily
mux.HandleFunc("GET /daily/seed", handleDailySeed) mux.HandleFunc("GET /daily/seed", handleDailySeed)

View File

@ -61,21 +61,26 @@ func Init() error {
secret = newSecret secret = newSecret
} }
err = recordNewDaily() seed, err := recordNewDaily()
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
log.Printf("Daily Run Seed: %s", Seed()) log.Printf("Daily Run Seed: %s", seed)
scheduler.AddFunc("@daily", func() { _, err = scheduler.AddFunc("@daily", func() {
time.Sleep(time.Second) time.Sleep(time.Second)
err := recordNewDaily() seed, err = recordNewDaily()
if err != nil { if err != nil {
log.Printf("error while recording new daily: %s", err) log.Printf("error while recording new daily: %s", err)
} else {
log.Printf("Daily Run Seed: %s", seed)
} }
}) })
if err != nil {
return err
}
scheduler.Start() scheduler.Start()
@ -95,11 +100,6 @@ func deriveSeed(seedTime time.Time) []byte {
return hashedSeed[:] return hashedSeed[:]
} }
func recordNewDaily() error { func recordNewDaily() (string, error) {
err := db.TryAddDailyRun(Seed()) return db.TryAddDailyRun(Seed())
if err != nil {
return err
}
return nil
} }

View File

@ -296,14 +296,15 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
break break
} }
s, ok := save.(defs.SessionSaveData) var seed string
if !ok { seed, err = db.GetDailyRunSeed()
err = fmt.Errorf("save data is not type SessionSaveData") if err != nil {
break httpError(w, r, err, http.StatusInternalServerError)
return
} }
// doesn't return a save, but it works // doesn't return a save, but it works
save, err = savedata.Clear(uuid, slot, daily.Seed(), s) save, err = savedata.Clear(uuid, slot, seed, save.(defs.SessionSaveData))
} }
if err != nil { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) httpError(w, r, err, http.StatusInternalServerError)
@ -324,10 +325,47 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
} }
func handleNewClear(w http.ResponseWriter, r *http.Request) {
uuid, err := uuidFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
var slot int
if r.URL.Query().Has("slot") {
slot, err = strconv.Atoi(r.URL.Query().Get("slot"))
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
}
newClear, err := savedata.NewClear(uuid, slot)
if err != nil {
httpError(w, r, fmt.Errorf("failed to read new clear: %s", err), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(newClear)
if err != nil {
httpError(w, r, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
}
// daily // daily
func handleDailySeed(w http.ResponseWriter, r *http.Request) { func handleDailySeed(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(daily.Seed())) seed, err := db.GetDailyRunSeed()
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
w.Write([]byte(seed))
} }
func handleDailyRankings(w http.ResponseWriter, r *http.Request) { func handleDailyRankings(w http.ResponseWriter, r *http.Request) {

View File

@ -19,9 +19,10 @@ package savedata
import ( import (
"fmt" "fmt"
"log"
"github.com/pagefaultgames/rogueserver/db" "github.com/pagefaultgames/rogueserver/db"
"github.com/pagefaultgames/rogueserver/defs" "github.com/pagefaultgames/rogueserver/defs"
"log"
) )
type ClearResponse struct { type ClearResponse struct {
@ -56,7 +57,7 @@ func Clear(uuid []byte, slot int, seed string, save defs.SessionSaveData) (Clear
} }
if sessionCompleted { if sessionCompleted {
response.Success, err = db.TryAddDailyRunCompletion(uuid, save.Seed, int(save.GameMode)) response.Success, err = db.TryAddSeedCompletion(uuid, save.Seed, int(save.GameMode))
if err != nil { if err != nil {
log.Printf("failed to mark seed as completed: %s", err) log.Printf("failed to mark seed as completed: %s", err)
} }

44
api/savedata/newclear.go Normal file
View File

@ -0,0 +1,44 @@
/*
Copyright (C) 2024 Pagefault Games
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package savedata
import (
"fmt"
"github.com/pagefaultgames/rogueserver/db"
"github.com/pagefaultgames/rogueserver/defs"
)
// /savedata/newclear - return whether a session is a new clear for its seed
func NewClear(uuid []byte, slot int) (bool, error) {
if slot < 0 || slot >= defs.SessionSlotCount {
return false, fmt.Errorf("slot id %d out of range", slot)
}
session, err := db.ReadSessionSaveData(uuid, slot)
if err != nil {
return false, err
}
completed, err := db.ReadSeedCompleted(uuid, session.Seed)
if err != nil {
return false, fmt.Errorf("failed to read seed completed: %s", err)
}
return !completed, nil
}

View File

@ -23,13 +23,25 @@ import (
"github.com/pagefaultgames/rogueserver/defs" "github.com/pagefaultgames/rogueserver/defs"
) )
func TryAddDailyRun(seed string) error { func TryAddDailyRun(seed string) (string, error) {
_, err := handle.Exec("INSERT INTO dailyRuns (seed, date) VALUES (?, UTC_DATE()) ON DUPLICATE KEY UPDATE date = date", seed) var actualSeed string
err := handle.QueryRow("INSERT INTO dailyRuns (seed, date) VALUES (?, UTC_DATE()) ON DUPLICATE KEY UPDATE date = date RETURNING seed", seed).Scan(&actualSeed)
if err != nil { if err != nil {
return err return "", err
} }
return nil return actualSeed, nil
}
func GetDailyRunSeed() (string, error) {
var seed string
err := handle.QueryRow("SELECT seed FROM dailyRuns WHERE date = UTC_DATE()").Scan(&seed)
if err != nil {
return "", err
}
return seed, nil
} }
func AddOrUpdateAccountDailyRun(uuid []byte, score int, wave int) error { func AddOrUpdateAccountDailyRun(uuid []byte, score int, wave int) error {

View File

@ -52,14 +52,51 @@ func Init(username, password, protocol, address, database string) error {
if err != nil { if err != nil {
panic(err) panic(err)
} }
// accounts
tx.Exec("CREATE TABLE IF NOT EXISTS accounts (uuid BINARY(16) NOT NULL PRIMARY KEY, username VARCHAR(16) UNIQUE NOT NULL, hash BINARY(32) NOT NULL, salt BINARY(16) NOT NULL, registered TIMESTAMP NOT NULL, lastLoggedIn TIMESTAMP DEFAULT NULL, lastActivity TIMESTAMP DEFAULT NULL, banned TINYINT(1) NOT NULL DEFAULT 0, trainerId SMALLINT(5) UNSIGNED DEFAULT 0, secretId SMALLINT(5) UNSIGNED DEFAULT 0)")
// sessions
tx.Exec("CREATE TABLE IF NOT EXISTS sessions (token BINARY(32) NOT NULL PRIMARY KEY, uuid BINARY(16) NOT NULL, active TINYINT(1) NOT NULL DEFAULT 0, expire TIMESTAMP DEFAULT NULL, CONSTRAINT sessions_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
tx.Exec("CREATE INDEX IF NOT EXISTS sessionsByUuid ON sessions (uuid)")
// stats
tx.Exec("CREATE TABLE IF NOT EXISTS accountStats (uuid BINARY(16) NOT NULL PRIMARY KEY, playTime INT(11) NOT NULL DEFAULT 0, battles INT(11) NOT NULL DEFAULT 0, classicSessionsPlayed INT(11) NOT NULL DEFAULT 0, sessionsWon INT(11) NOT NULL DEFAULT 0, highestEndlessWave INT(11) NOT NULL DEFAULT 0, highestLevel INT(11) NOT NULL DEFAULT 0, pokemonSeen INT(11) NOT NULL DEFAULT 0, pokemonDefeated INT(11) NOT NULL DEFAULT 0, pokemonCaught INT(11) NOT NULL DEFAULT 0, pokemonHatched INT(11) NOT NULL DEFAULT 0, eggsPulled INT(11) NOT NULL DEFAULT 0, regularVouchers INT(11) NOT NULL DEFAULT 0, plusVouchers INT(11) NOT NULL DEFAULT 0, premiumVouchers INT(11) NOT NULL DEFAULT 0, goldenVouchers INT(11) NOT NULL DEFAULT 0, CONSTRAINT accountStats_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
// compensations
tx.Exec("CREATE TABLE IF NOT EXISTS accountCompensations (id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, uuid BINARY(16) NOT NULL, voucherType INT(11) NOT NULL, count INT(11) NOT NULL DEFAULT 1, claimed BIT(1) NOT NULL DEFAULT b'0', CONSTRAINT accountCompensations_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
tx.Exec("CREATE INDEX IF NOT EXISTS accountCompensationsByUuid ON accountCompensations (uuid)")
// daily runs
tx.Exec("CREATE TABLE IF NOT EXISTS dailyRuns (date DATE NOT NULL PRIMARY KEY, seed CHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL)")
tx.Exec("CREATE INDEX IF NOT EXISTS dailyRunsByDateAndSeed ON dailyRuns (date, seed)")
tx.Exec("CREATE TABLE IF NOT EXISTS dailyRunCompletions (uuid BINARY(16) NOT NULL, seed CHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, mode INT(11) NOT NULL DEFAULT 0, score INT(11) NOT NULL DEFAULT 0, timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (uuid, seed), CONSTRAINT dailyRunCompletions_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
tx.Exec("CREATE INDEX IF NOT EXISTS dailyRunCompletionsByUuidAndSeed ON dailyRunCompletions (uuid, seed)")
tx.Exec("CREATE TABLE IF NOT EXISTS accountDailyRuns (uuid BINARY(16) NOT NULL, date DATE NOT NULL, score INT(11) NOT NULL DEFAULT 0, wave INT(11) NOT NULL DEFAULT 0, timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (uuid, date), CONSTRAINT accountDailyRuns_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT accountDailyRuns_ibfk_2 FOREIGN KEY (date) REFERENCES dailyRuns (date) ON DELETE NO ACTION ON UPDATE NO ACTION)")
tx.Exec("CREATE INDEX IF NOT EXISTS accountDailyRunsByDate ON accountDailyRuns (date)")
// save data
tx.Exec("CREATE TABLE IF NOT EXISTS systemSaveData (uuid BINARY(16) PRIMARY KEY, data LONGBLOB, timestamp TIMESTAMP)") tx.Exec("CREATE TABLE IF NOT EXISTS systemSaveData (uuid BINARY(16) PRIMARY KEY, data LONGBLOB, timestamp TIMESTAMP)")
tx.Exec("CREATE TABLE IF NOT EXISTS sessionSaveData (uuid BINARY(16), slot TINYINT, data LONGBLOB, timestamp TIMESTAMP, PRIMARY KEY (uuid, slot))") tx.Exec("CREATE TABLE IF NOT EXISTS sessionSaveData (uuid BINARY(16), slot TINYINT, data LONGBLOB, timestamp TIMESTAMP, PRIMARY KEY (uuid, slot))")
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
panic(err) panic(err)
} }
// TODO temp code // TODO temp code
_, err = os.Stat("userdata")
if err != nil {
if os.IsNotExist(err) { // not found, do not migrate
return nil
} else {
log.Fatalf("failed to stat userdata directory: %s", err)
return err
}
}
entries, err := os.ReadDir("userdata") entries, err := os.ReadDir("userdata")
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)

View File

@ -24,7 +24,7 @@ import (
"github.com/pagefaultgames/rogueserver/defs" "github.com/pagefaultgames/rogueserver/defs"
) )
func TryAddDailyRunCompletion(uuid []byte, seed string, mode int) (bool, error) { func TryAddSeedCompletion(uuid []byte, seed string, mode int) (bool, error) {
var count int var count int
err := handle.QueryRow("SELECT COUNT(*) FROM dailyRunCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count) err := handle.QueryRow("SELECT COUNT(*) FROM dailyRunCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count)
if err != nil { if err != nil {
@ -41,6 +41,16 @@ func TryAddDailyRunCompletion(uuid []byte, seed string, mode int) (bool, error)
return true, nil return true, nil
} }
func ReadSeedCompleted(uuid []byte, seed string) (bool, error) {
var count int
err := handle.QueryRow("SELECT COUNT(*) FROM dailyRunCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
func ReadSystemSaveData(uuid []byte) (defs.SystemSaveData, error) { func ReadSystemSaveData(uuid []byte) (defs.SystemSaveData, error) {
var system defs.SystemSaveData var system defs.SystemSaveData

View File

@ -0,0 +1,14 @@
services:
db:
image: mariadb:11
container_name: pokerogue-db-local
restart: on-failure
environment:
MYSQL_ROOT_PASSWORD: admin
MYSQL_DATABASE: pokeroguedb
MYSQL_USER: pokerogue
MYSQL_PASSWORD: pokerogue
ports:
- "3306:3306"
volumes:
- ./.data/db:/var/lib/mysql

View File

@ -0,0 +1,27 @@
services:
server:
image: ghcr.io/pagefaultgames/pokerogue:latest
command: --debug --dbaddr db:3306 --dbuser pokerogue --dbpass pokerogue --dbname pokeroguedb
restart: unless-stopped
depends_on:
- db
networks:
- internal
ports:
- "8001:8001"
db:
image: mariadb:11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: admin
MYSQL_DATABASE: pokeroguedb
MYSQL_USER: pokerogue
MYSQL_PASSWORD: pokerogue
volumes:
- database:/var/lib/mysql
volumes:
database:
networks:
internal:

View File

@ -34,10 +34,12 @@ func main() {
debug := flag.Bool("debug", false, "use debug mode") debug := flag.Bool("debug", false, "use debug mode")
proto := flag.String("proto", "tcp", "protocol for api to use (tcp, unix)") proto := flag.String("proto", "tcp", "protocol for api to use (tcp, unix)")
addr := flag.String("addr", "0.0.0.0", "network address for api to listen on") addr := flag.String("addr", "0.0.0.0:8001", "network address for api to listen on")
tlscert := flag.String("tlscert", "", "tls certificate path")
tlskey := flag.String("tlskey", "", "tls key path")
dbuser := flag.String("dbuser", "pokerogue", "database username") dbuser := flag.String("dbuser", "pokerogue", "database username")
dbpass := flag.String("dbpass", "", "database password") dbpass := flag.String("dbpass", "pokerogue", "database password")
dbproto := flag.String("dbproto", "tcp", "protocol for database connection") dbproto := flag.String("dbproto", "tcp", "protocol for database connection")
dbaddr := flag.String("dbaddr", "localhost", "database address") dbaddr := flag.String("dbaddr", "localhost", "database address")
dbname := flag.String("dbname", "pokeroguedb", "database name") dbname := flag.String("dbname", "pokeroguedb", "database name")
@ -66,10 +68,15 @@ func main() {
api.Init(mux) api.Init(mux)
// start web server // start web server
handler := prodHandler(mux)
if *debug { if *debug {
err = http.Serve(listener, debugHandler(mux)) handler = debugHandler(mux)
}
if *tlscert == "" {
err = http.Serve(listener, handler)
} else { } else {
err = http.Serve(listener, mux) err = http.ServeTLS(listener, handler, *tlscert, *tlskey)
} }
if err != nil { if err != nil {
log.Fatalf("failed to create http server or server errored: %s", err) log.Fatalf("failed to create http server or server errored: %s", err)
@ -93,6 +100,21 @@ func createListener(proto, addr string) (net.Listener, error) {
return listener, nil return listener, nil
} }
func prodHandler(router *http.ServeMux) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST")
w.Header().Set("Access-Control-Allow-Origin", "https://pokerogue.net")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
router.ServeHTTP(w, r)
})
}
func debugHandler(router *http.ServeMux) http.Handler { func debugHandler(router *http.ServeMux) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Access-Control-Allow-Headers", "*")