From 56457400997d61ce16ebeb2392af04a9be20485c Mon Sep 17 00:00:00 2001 From: AnyUnion Date: Mon, 4 May 2026 21:14:27 +0700 Subject: [PATCH] Add --no-register flag and register-account CLI Author: https://github.com/REUSS-dev --- README.md | 33 ++++++- server/Makefile | 5 +- server/cmd/auth-server/handlers.go | 23 +++-- server/cmd/auth-server/main.go | 9 +- server/cmd/dev/main.go | 20 +++- server/cmd/lunar-tear/grpc.go | 10 +- server/cmd/lunar-tear/main.go | 3 +- server/cmd/register-account/main.go | 99 +++++++++++++++++++ .../auth-server => internal/auth}/store.go | 2 +- .../auth-server => internal/auth}/token.go | 6 +- server/internal/service/user.go | 27 +++-- server/internal/store/sqlite/user.go | 6 -- 12 files changed, 205 insertions(+), 38 deletions(-) create mode 100644 server/cmd/register-account/main.go rename server/{cmd/auth-server => internal/auth}/store.go (99%) rename server/{cmd/auth-server => internal/auth}/token.go (95%) diff --git a/README.md b/README.md index c4eaf73..2ed0934 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000" | `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr | | `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear | | `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear | +| `--no-register` | `false` | disable new user registrations (only already registered users can connect). | | `--admin.listen` | *(empty)* | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. | | `--no-color` | `false` | disable colored output | @@ -195,6 +196,7 @@ make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000" | `--db` | `db/game.db` | SQLite database path | | `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | | `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. | +| `--no-register` | `false` | Disable new user registrations (only already registered users can connect). | ### Live Master Data Reload @@ -270,6 +272,7 @@ All targets run from the `server/` directory. | `make build-all` | Build all service binaries to `bin/` | | `make build-import` | Build the import-snapshot tool | | `make build-claim-account` | Build the claim-account tool | +| `make build-register-account` | Build the register-account tool | | `make clean` | Remove the `bin/` directory | | `make dev` | Run all three services with one command | | `make migrate` | Run goose migrations on `db/game.db` | @@ -308,11 +311,31 @@ The `--secret` flag accepts a hex-encoded HMAC key. If omitted, a random key is ### Flags -| Flag | Default | Description | -| ---------- | --------------- | -------------------------------------------- | -| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) | -| `--db` | `db/auth.db` | SQLite database path for auth users | -| `--secret` | *(generated)* | Hex-encoded HMAC secret for token signing | +| Flag | Default | Description | +| ---------------- | --------------- | -------------------------------------------- | +| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) | +| `--db` | `db/auth.db` | SQLite database path for auth users | +| `--secret` | *(generated)* | Hex-encoded HMAC secret for token signing | +| `--no-register` | `false` | Disable new user registrations (only already registered users can log in). | + +## Create account + +This tool creates a fresh account in main db and new account in Auth Server store with given name & password and automatically binds them together. +A primary mean of registering new accounts when `--no-register` flag is passed to lunar-tear for controlled server access. + +```bash +go run ./cmd/register-account --name "AccountName" --password "AccountPassword" --platform "android" +``` + +| Flag | Default | Description | +| ------------ | ------------ | ------------------------------------------------------------ | +| `--name` | *(required)* | Auth Server account nickname to be registered | +| `--password` | *(required)* | Auth Server account password to be registered | +| `--platform` | `android` | Platform of new user account (`android` or `ios`) | +| `--db` | `db/game.db` | SQLite main database path | +| `--auth-db` | `db/auth.db` | SQLite Auth Server database path | + +This only sets the nickname of Auth Server account, a player can choose their in-game nickname upon first login! ## ⚠️ Legal Disclaimer diff --git a/server/Makefile b/server/Makefile index dcfb7da..309ed53 100644 --- a/server/Makefile +++ b/server/Makefile @@ -30,6 +30,9 @@ build-auth: build-claim-account: go build -o claim-account$(EXE) ./cmd/claim-account +build-register-account: + go build -o register-account$(EXE) ./cmd/register-account + build-dev: go build -o bin/dev$(EXE) ./cmd/dev @@ -57,4 +60,4 @@ ifndef UUID endif go run ./cmd/import-snapshot --snapshot $(SNAPSHOT) --uuid $(UUID) -.PHONY: proto build build-cdn build-auth build-import build-claim-account build-dev build-all clean dev migrate import +.PHONY: proto build build-cdn build-auth build-import build-claim-account build-register-account build-dev build-all clean dev migrate import diff --git a/server/cmd/auth-server/handlers.go b/server/cmd/auth-server/handlers.go index e866d7b..4235b74 100644 --- a/server/cmd/auth-server/handlers.go +++ b/server/cmd/auth-server/handlers.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "log" + "lunar-tear/server/internal/auth" "net/http" "net/url" "strconv" @@ -39,12 +40,13 @@ var oauthRedirectTmpl = template.Must(template.New("oauthRedirect").Parse( `)) type Handlers struct { - store *AuthStore - tok *TokenService + store *auth.AuthStore + tok *auth.TokenService + noRegister bool } -func NewHandlers(store *AuthStore, tok *TokenService) *Handlers { - return &Handlers{store: store, tok: tok} +func NewHandlers(store *auth.AuthStore, tok *auth.TokenService, noRegister bool) *Handlers { + return &Handlers{store: store, tok: tok, noRegister: noRegister} } type loginPageData struct { @@ -139,13 +141,18 @@ func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) { return } - var user AuthUser + var user auth.AuthUser var err error switch action { case "register": + if h.noRegister { + renderErr("This server does not accept user registrations.") + return + } + user, err = h.store.CreateUser(username, password) - if err == ErrUserExists { + if err == auth.ErrUserExists { renderErr("Username is already taken.") return } @@ -158,7 +165,7 @@ func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) { case "login": user, err = h.store.VerifyUser(username, password) - if err == ErrInvalidCreds { + if err == auth.ErrInvalidCreds { renderErr("Invalid username or password.") return } @@ -187,7 +194,7 @@ func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) { fragment := url.Values{} fragment.Set("access_token", token) fragment.Set("token_type", "bearer") - fragment.Set("expires_in", strconv.FormatInt(int64(tokenTTL.Seconds()), 10)) + fragment.Set("expires_in", strconv.FormatInt(int64(auth.TokenTTL.Seconds()), 10)) fragment.Set("signed_request", "0."+b64) // iOS FBSDKLoginManager treats an empty granted_scopes set as a cancelled login // (LoginManager.swift -> getSuccessResult -> getCancelledResult). Echo back the diff --git a/server/cmd/auth-server/main.go b/server/cmd/auth-server/main.go index feeaca3..fa89074 100644 --- a/server/cmd/auth-server/main.go +++ b/server/cmd/auth-server/main.go @@ -8,6 +8,8 @@ import ( "log" "net/http" + "lunar-tear/server/internal/auth" + _ "modernc.org/sqlite" ) @@ -15,6 +17,7 @@ func main() { listen := flag.String("listen", "0.0.0.0:3000", "HTTP listen address (host:port)") dbPath := flag.String("db", "db/auth.db", "SQLite database path for auth users") secret := flag.String("secret", "", "HMAC secret for tokens (auto-generated if empty)") + noRegister := flag.Bool("no-register", false, "Disallow new account registrations for clients, when present. Default = false") flag.Parse() hmacSecret := []byte(*secret) @@ -33,13 +36,13 @@ func main() { } defer db.Close() - store, err := NewAuthStore(db) + store, err := auth.NewAuthStore(db) if err != nil { log.Fatalf("init auth store: %v", err) } - tok := NewTokenService(hmacSecret) - h := NewHandlers(store, tok) + tok := auth.NewTokenService(hmacSecret) + h := NewHandlers(store, tok, *noRegister) mux := http.NewServeMux() mux.HandleFunc("/", h.HandleOAuth) diff --git a/server/cmd/dev/main.go b/server/cmd/dev/main.go index e928cd4..e2bc2d4 100644 --- a/server/cmd/dev/main.go +++ b/server/cmd/dev/main.go @@ -97,7 +97,12 @@ func main() { // (the listener still only binds if LUNAR_ADMIN_TOKEN is set in the env). adminListen := flag.String("admin.listen", "", "lunar-tear admin webhook listen address (host:port). Empty = leave default; webhook only binds when LUNAR_ADMIN_TOKEN is set in the env.") + // Controlled server access + noRegister := flag.Bool("no-register", false, "Disallow new account registrations for clients, when present. Default = false") + + // dev utility output config noColor := flag.Bool("no-color", false, "disable colored output") + flag.Parse() if *grpcOctoURL == "" { @@ -122,6 +127,11 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() + noreg_s := "" + if *noRegister { + noreg_s = "--no-register" + } + services := []service{ { label: "auth", @@ -129,6 +139,7 @@ func main() { cmd: exec.CommandContext(ctx, filepath.Join("bin", "auth-server"+ext), "--listen", *authListen, "--db", *authDB, + noreg_s, ), }, { @@ -143,7 +154,7 @@ func main() { label: "grpc", color: colorYellow, cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext), - grpcArgs(*grpcListen, *grpcPublicAddr, *grpcDB, *grpcOctoURL, *grpcAuthURL, *adminListen)..., + grpcArgs(*grpcListen, *grpcPublicAddr, *grpcDB, *grpcOctoURL, *grpcAuthURL, *adminListen, *noRegister)..., ), }, } @@ -204,7 +215,7 @@ func prefixLines(wg *sync.WaitGroup, prefix string, r io.Reader) { // grpcArgs assembles the argv for the lunar-tear subprocess. The admin flag // is appended only when --admin.listen was supplied so we don't override // lunar-tear's own default when the operator hasn't opted in. -func grpcArgs(listen, publicAddr, db, octoURL, authURL, adminListen string) []string { +func grpcArgs(listen, publicAddr, db, octoURL, authURL, adminListen string, noRegister bool) []string { args := []string{ "--listen", listen, "--public-addr", publicAddr, @@ -212,8 +223,13 @@ func grpcArgs(listen, publicAddr, db, octoURL, authURL, adminListen string) []st "--octo-url", octoURL, "--auth-url", authURL, } + if adminListen != "" { args = append(args, "--admin-listen", adminListen) } + + if noRegister { + args = append(args, "--no-register") + } return args } diff --git a/server/cmd/lunar-tear/grpc.go b/server/cmd/lunar-tear/grpc.go index 890f005..ed9843e 100644 --- a/server/cmd/lunar-tear/grpc.go +++ b/server/cmd/lunar-tear/grpc.go @@ -39,6 +39,7 @@ func startGRPC( store.SessionRepository }, holder *runtime.Holder, + noRegister bool, ) *grpc.Server { lis, err := net.Listen("tcp", listenAddr) if err != nil { @@ -52,13 +53,17 @@ func startGRPC( grpc.UnknownServiceHandler(interceptor.UnknownService), ) - registerServices(grpcServer, publicAddr, octoURL, authURL, userStore, holder) + registerServices(grpcServer, publicAddr, octoURL, authURL, userStore, holder, noRegister) reflection.Register(grpcServer) log.Printf("gRPC server listening on %s", lis.Addr()) log.Printf("public address: %s", publicAddr) + if noRegister { + log.Print("[!!WARNING!!] The gRPC server is running in NO-REGISTER mode. All new user registrations are denied, only existing accounts and auth-server logins are permitted.") + } + go func() { if err := grpcServer.Serve(lis); err != nil { log.Printf("gRPC server stopped: %v", err) @@ -77,12 +82,13 @@ func registerServices( store.SessionRepository }, holder *runtime.Holder, + noRegister bool, ) { pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr) pubPort, _ := strconv.Atoi(pubPortStr) pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(holder)) - pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL)) + pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL, noRegister)) pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore)) pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL)) pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore)) diff --git a/server/cmd/lunar-tear/main.go b/server/cmd/lunar-tear/main.go index 14c19f8..edf09e2 100644 --- a/server/cmd/lunar-tear/main.go +++ b/server/cmd/lunar-tear/main.go @@ -23,6 +23,7 @@ func main() { octoURL := flag.String("octo-url", "", "Octo CDN base URL the client will use for assets (e.g. http://10.0.2.2:8080)") authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)") adminListen := flag.String("admin-listen", "127.0.0.1:8082", "admin webhook listen address (host:port). Loopback by default; only binds when LUNAR_ADMIN_TOKEN is set.") + noRegister := flag.Bool("no-register", false, "Disallow new account registrations for clients, when present. Default = false") flag.Parse() if *octoURL == "" { @@ -46,7 +47,7 @@ func main() { userStore := sqlite.New(db, gametime.Now) - grpcServer := startGRPC(*listen, *publicAddr, *octoURL, *authURL, userStore, holder) + grpcServer := startGRPC(*listen, *publicAddr, *octoURL, *authURL, userStore, holder, *noRegister) startAdmin(*adminListen, holder) diff --git a/server/cmd/register-account/main.go b/server/cmd/register-account/main.go new file mode 100644 index 0000000..1c8774d --- /dev/null +++ b/server/cmd/register-account/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "flag" + "log" + + "github.com/google/uuid" + + "lunar-tear/server/internal/auth" + "lunar-tear/server/internal/database" + "lunar-tear/server/internal/model" + "lunar-tear/server/internal/store/sqlite" +) + +func main() { + dbPath := flag.String("db", "db/game.db", "SQLite database path") + authdbPath := flag.String("auth-db", "db/auth.db", "SQLite auth server database path") + + name := flag.String("name", "", "Nickname of the new account to-be") + password := flag.String("password", "", "Password of the new account to-be") + platform := flag.String("platform", "android", "Platform of the user. Can be: \"android\", \"ios\"") + + flag.Parse() + + if *name == "" { + log.Fatal("--name flag is required") + } + + if *password == "" { + log.Fatal("--password flag is required") + } + + if (*platform != "android") && (*platform != "ios") { + log.Fatal("--platform can be either \"android\" or \"ios\"") + } + + db, err := database.Open(*dbPath) + if err != nil { + log.Fatalf("open database: %v", err) + } + defer db.Close() + + userStore := sqlite.New(db, nil) + + authdb, err := database.Open(*authdbPath) + if err != nil { + log.Fatalf("open auth database: %v", err) + } + defer db.Close() + + authStore, err := auth.NewAuthStore(authdb) + if err != nil { + log.Fatalf("init auth store: %v", err) + } + + // Auth user check + + userExists := authStore.UserExists(*name) + if userExists { + log.Fatal("Username is already taken") + } + + // lunar-tear user + + var userPlatform model.ClientPlatform + + if *platform == "android" { + userPlatform.OsType = 2 + userPlatform.PlatformType = 2 + } else { + userPlatform.OsType = 1 + userPlatform.PlatformType = 1 + } + + userUuid := uuid.New().String() + id, err := userStore.CreateUser(userUuid, userPlatform) + + if err == nil { + log.Printf("Registered user %d in database successfully", id) + } else { + log.Fatalf("Register user in database: %v", err) + } + + // Bind + + authUser, err := authStore.CreateUser(*name, *password) + if err != nil { + log.Fatalf("Register auth account: %v", err) + } + + err = userStore.SetFacebookId(id, authUser.ID) + if err == nil { + log.Printf("Bound user %d with facebook account %v", id, authUser.Username) + } else { + log.Fatalf("failed to bind user with facebook account: %v", err) + } + + log.Printf("Account %v created successfully.", *name) +} diff --git a/server/cmd/auth-server/store.go b/server/internal/auth/store.go similarity index 99% rename from server/cmd/auth-server/store.go rename to server/internal/auth/store.go index 960255c..7960ce5 100644 --- a/server/cmd/auth-server/store.go +++ b/server/internal/auth/store.go @@ -1,4 +1,4 @@ -package main +package auth import ( "database/sql" diff --git a/server/cmd/auth-server/token.go b/server/internal/auth/token.go similarity index 95% rename from server/cmd/auth-server/token.go rename to server/internal/auth/token.go index 30ded2a..9c66bc6 100644 --- a/server/cmd/auth-server/token.go +++ b/server/internal/auth/token.go @@ -1,4 +1,4 @@ -package main +package auth import ( "crypto/hmac" @@ -10,7 +10,7 @@ import ( "time" ) -const tokenTTL = 24 * time.Hour +const TokenTTL = 24 * time.Hour var ( ErrTokenInvalid = errors.New("invalid token") @@ -38,7 +38,7 @@ func (t *TokenService) Generate(user AuthUser) (string, error) { Sub: user.ID, Name: user.Username, Iat: now, - Exp: now + int64(tokenTTL.Seconds()), + Exp: now + int64(TokenTTL.Seconds()), } payload, err := json.Marshal(claims) diff --git a/server/internal/service/user.go b/server/internal/service/user.go index d41a3b5..f49366b 100644 --- a/server/internal/service/user.go +++ b/server/internal/service/user.go @@ -12,6 +12,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" @@ -23,19 +24,30 @@ import ( type UserServiceServer struct { pb.UnimplementedUserServiceServer - users store.UserRepository - sessions store.SessionRepository - authURL string + users store.UserRepository + sessions store.SessionRepository + authURL string + noRegister bool } -func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository, authURL string) *UserServiceServer { +func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository, authURL string, noRegister bool) *UserServiceServer { if authURL != "" && !strings.Contains(authURL, "://") { authURL = "http://" + authURL } - return &UserServiceServer{users: users, sessions: sessions, authURL: authURL} + return &UserServiceServer{users: users, sessions: sessions, authURL: authURL, noRegister: noRegister} } func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) { + if s.noRegister { + ip := "invalid" + + if p, ok := peer.FromContext(ctx); ok { + ip = p.Addr.String() + } + + return nil, fmt.Errorf("Denied user registration: ip=%s uuid=%s", ip, req.Uuid) + } + platform := model.ClientPlatformFromContext(ctx) userId, err := s.users.CreateUser(req.Uuid, platform) if err != nil { @@ -89,11 +101,14 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p func (s *UserServiceServer) TransferUser(ctx context.Context, req *pb.TransferUserRequest) (*pb.TransferUserResponse, error) { platform := model.ClientPlatformFromContext(ctx) + log.Printf("[UserService] TransferUser: platform=%s", platform) - userId, err := s.users.CreateUser(req.Uuid, platform) + + userId, err := s.users.GetUserByUUID(req.Uuid) if err != nil { return nil, fmt.Errorf("create user: %w", err) } + return &pb.TransferUserResponse{ UserId: userId, Signature: "transferred-sig", diff --git a/server/internal/store/sqlite/user.go b/server/internal/store/sqlite/user.go index 8441392..b57cb27 100644 --- a/server/internal/store/sqlite/user.go +++ b/server/internal/store/sqlite/user.go @@ -15,12 +15,6 @@ func (s *SQLiteStore) CreateUser(uuid string, platform model.ClientPlatform) (in } defer tx.Rollback() - var existingId int64 - err = tx.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&existingId) - if err == nil { - return existingId, nil - } - nowMillis := s.clock().UnixMilli() res, err := tx.Exec(`INSERT INTO users (uuid, player_id, os_type, platform_type, user_restriction_type,