diff --git a/database/get_open_active_matches.go b/database/get_open_active_matches.go new file mode 100644 index 0000000..7a6039f --- /dev/null +++ b/database/get_open_active_matches.go @@ -0,0 +1,62 @@ +package database + +import ( + "database/sql" + "time" + + pb "github.com/PretendoNetwork/grpc/go/nex/matchmaking/v1" + "github.com/PretendoNetwork/splatoon/globals" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func GetOpenActiveMatches() (*[]pb.ActiveMatch, error) { + + rows, err := globals.Postgres.Query(` + SELECT g.id, g.started_time, g.participants,g.owner_pid,s.game_mode,g.flags + FROM matchmaking.gatherings g + JOIN matchmaking.matchmake_sessions s ON (g.id = s.id) + WHERE array_length(g.participants, 1) > 4 AND s.open_participation = true + LIMIT 25`) + + if err != nil { + if err == sql.ErrNoRows { + globals.Logger.Error("Error no matches found") + return nil, status.Errorf(codes.Internal, "internal server error") + } else { + globals.Logger.Error("Error unknown fetching matches") + return nil, status.Errorf(codes.Internal, "internal server error") + } + } + + defer rows.Close() + + var matches []pb.ActiveMatch + + for rows.Next() { + var id uint64 + var startTime time.Time + var participants Participants + var ownerPID uint32 + var gameMode uint64 + var flags uint64 + + err := rows.Scan(&id, &startTime, &participants, &ownerPID, &gameMode, &flags) + if err != nil { + globals.Logger.Error("Error parsing row contents") + globals.Logger.Error(err.Error()) + return nil, status.Errorf(codes.Internal, "internal server error") + } + + matches = append(matches, pb.ActiveMatch{ + Id: uint32(id), + StartTime: uint64(startTime.Unix()), + Participants: participants, + OwnerPid: ownerPID, + HostPid: ownerPID, + GameMode: gameMode, + Flags: flags, + }) + } + return &matches, nil +} diff --git a/database/participants.go b/database/participants.go new file mode 100644 index 0000000..6decb4b --- /dev/null +++ b/database/participants.go @@ -0,0 +1,72 @@ +package database + +import ( + "database/sql/driver" + "encoding/csv" + "fmt" + "strconv" + "strings" +) + +type Participants []uint32 + +func (p Participants) Value() (driver.Value, error) { + return p, nil +} + +func (l *Participants) Scan(value any) error { + if value == nil { + return nil + } + + // * Ensure the value is in the right format + var pgArray string + switch v := value.(type) { + case []byte: + pgArray = string(v) + case string: + pgArray = v + default: + return fmt.Errorf("unsupported Scan type for List: %T", value) + } + + // * Postgres formats arrays in curly braces, + // * such as `{"string1"}` + if len(pgArray) < 2 || pgArray[0] != '{' || pgArray[len(pgArray)-1] != '}' { + return fmt.Errorf("invalid PostgreSQL array format: %s", pgArray) + } + + pgArray = strings.TrimSuffix(pgArray, "}") + pgArray = strings.TrimPrefix(pgArray, "{") + + // * Bail if array is empty, who cares + if pgArray == "" { + return nil + } + + var elements []string + var err error + + reader := csv.NewReader(strings.NewReader(pgArray)) + reader.Comma = ',' + + elements, err = reader.Read() + + if err != nil { + return nil + } + + result := make(Participants, 0, len(elements)) + + for _, element := range elements { + i, err := strconv.ParseUint(element, 10, 32) + if err != nil { + return err + } + + result = append(result, uint32(i)) + } + + *l = result + return nil +} diff --git a/globals/globals.go b/globals/globals.go index ee9ee2a..82128d2 100644 --- a/globals/globals.go +++ b/globals/globals.go @@ -2,10 +2,11 @@ package globals import ( "database/sql" + pbaccount "github.com/PretendoNetwork/grpc/go/account" pbfriends "github.com/PretendoNetwork/grpc/go/friends" "github.com/PretendoNetwork/nex-go/v2" - "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" + common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals" "github.com/PretendoNetwork/plogger-go" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -33,3 +34,6 @@ var MatchmakingManager *common_globals.MatchmakingManager var TokenAESKey []byte var LocalAuthMode bool + +var GRPCServerAPIKey string +var GRPCServerPort int diff --git a/go.mod b/go.mod index 9eea4b4..b4d01c7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.9 require ( - github.com/PretendoNetwork/grpc/go v0.0.0-20251014173731-f51013f00744 + github.com/PretendoNetwork/grpc/go v0.0.0-20260410023215-d68bba931107 github.com/PretendoNetwork/nex-go/v2 v2.1.4 github.com/PretendoNetwork/nex-protocols-common-go/v2 v2.4.1-0.20250809110555-cf55627f0b5a github.com/PretendoNetwork/nex-protocols-go/v2 v2.2.1 @@ -15,6 +15,7 @@ require ( ) require ( + github.com/PretendoNetwork/grpc v0.0.0-20260409015728-fac270c35695 // indirect github.com/PretendoNetwork/pq-extended v1.0.0 // indirect github.com/dolthub/maphash v0.1.0 // indirect github.com/fatih/color v1.18.0 // indirect diff --git a/go.sum b/go.sum index 9eb6753..e966889 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ +github.com/PretendoNetwork/grpc v0.0.0-20260409015728-fac270c35695 h1:2sUhq4NXlJ6BeF7+EZ7UYzSh9MJqFdznUkJ5w2043d8= +github.com/PretendoNetwork/grpc v0.0.0-20260409015728-fac270c35695/go.mod h1:BEYp5gMkRW0jpTbCdosEF+5Rm9jBfNuuHKtm7ldaCd0= github.com/PretendoNetwork/grpc/go v0.0.0-20251014173731-f51013f00744 h1:xJ3bgsUNhavN+57Gph0bgPYnora8igjStosLhGlKo1w= github.com/PretendoNetwork/grpc/go v0.0.0-20251014173731-f51013f00744/go.mod h1:L6We4KkcQeiQVPrF7iu8Zax0B1Bm0v4nssR1JOAiRFQ= +github.com/PretendoNetwork/grpc/go v0.0.0-20260409015728-fac270c35695 h1:Y0LqpoJCZifLqOmmjQObEatiUG5JRxulDtwsPVrvjpM= +github.com/PretendoNetwork/grpc/go v0.0.0-20260409015728-fac270c35695/go.mod h1:L6We4KkcQeiQVPrF7iu8Zax0B1Bm0v4nssR1JOAiRFQ= +github.com/PretendoNetwork/grpc/go v0.0.0-20260409021408-3bd3d3f4d248 h1:pKsOnW2HYc0mVd40B6fS1+Bgkm47YpH6OKyPgc1Vu9w= +github.com/PretendoNetwork/grpc/go v0.0.0-20260409021408-3bd3d3f4d248/go.mod h1:L6We4KkcQeiQVPrF7iu8Zax0B1Bm0v4nssR1JOAiRFQ= +github.com/PretendoNetwork/grpc/go v0.0.0-20260410023215-d68bba931107 h1:HIQBpxkG0DyRvxJGySxQ8yInjCUhThmt++r7HELRvI8= +github.com/PretendoNetwork/grpc/go v0.0.0-20260410023215-d68bba931107/go.mod h1:L6We4KkcQeiQVPrF7iu8Zax0B1Bm0v4nssR1JOAiRFQ= github.com/PretendoNetwork/nex-go/v2 v2.1.4 h1:n7ju/5/sHaY8ZE/4mJT3gu9tSi/4MNXl+xMv7f5i9aI= github.com/PretendoNetwork/nex-go/v2 v2.1.4/go.mod h1:3LyJzsv3AataJW8D0binp15Q8ZH22MWTYly1VNtXi64= github.com/PretendoNetwork/nex-protocols-common-go/v2 v2.4.1-0.20250809110555-cf55627f0b5a h1:mi7Qlysby01rI8o13vqmTtcSdO8blFEHVUbuaxKwEFM= diff --git a/grpc/api_key_interceptor.go b/grpc/api_key_interceptor.go new file mode 100644 index 0000000..4c568e3 --- /dev/null +++ b/grpc/api_key_interceptor.go @@ -0,0 +1,27 @@ +package grpc + +import ( + "context" + "errors" + + "github.com/PretendoNetwork/splatoon/globals" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func apiKeyInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + md, ok := metadata.FromIncomingContext(ctx) + + if ok { + apiKeyHeader := md.Get("X-API-Key") + + if len(apiKeyHeader) == 0 || apiKeyHeader[0] != globals.GRPCServerAPIKey { + globals.Logger.Errorf("API Key \"%v\" did not match \"%v\"", apiKeyHeader, globals.GRPCServerAPIKey) + return nil, errors.New("Missing or invalid API key") + } + } + + h, err := handler(ctx, req) + + return h, err +} diff --git a/grpc/get_active_matches.go b/grpc/get_active_matches.go new file mode 100644 index 0000000..7ad109d --- /dev/null +++ b/grpc/get_active_matches.go @@ -0,0 +1,27 @@ +package grpc + +import ( + "context" + "fmt" + + pb "github.com/PretendoNetwork/grpc/go/nex/matchmaking/v1" + db "github.com/PretendoNetwork/splatoon/database" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (*gRPCNexServer) GetActiveMatches(ctx context.Context, in *pb.GetActiveMatchesRequest) (*pb.GetActiveMatchesResponse, error) { + var result, err = db.GetOpenActiveMatches() + if err != nil { + return nil, status.Errorf(codes.Internal, "internal server error") + } + var matches []*pb.ActiveMatch + for i := range *result { + element := &(*result)[i] + fmt.Printf("ID: %v; Start Time: %v; PIDs: %v\n", element.Id, element.StartTime, element.Participants) + matches = append(matches, element) + } + return &pb.GetActiveMatchesResponse{ + Matches: matches, + }, nil +} diff --git a/grpc/grpc_server.go b/grpc/grpc_server.go new file mode 100644 index 0000000..165c4c9 --- /dev/null +++ b/grpc/grpc_server.go @@ -0,0 +1,33 @@ +package grpc + +import ( + "fmt" + "net" + + pb "github.com/PretendoNetwork/grpc/go/nex/matchmaking/v1" + "github.com/PretendoNetwork/splatoon/globals" + "google.golang.org/grpc" +) + +type gRPCNexServer struct { + pb.UnimplementedNEXMatchmakingServiceV1Server +} + +func StartGRPCServer() { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", 8123)) + if err != nil { + globals.Logger.Errorf("failed to listen: %v", err) + } + + server := grpc.NewServer( + //grpc.UnaryInterceptor(apiKeyInterceptor), + ) + + pb.RegisterNEXMatchmakingServiceV1Server(server, &gRPCNexServer{}) + + globals.Logger.Infof("GRPC server listening at %v", listener.Addr()) + + if err := server.Serve(listener); err != nil { + globals.Logger.Errorf("failed to serve: %v", err) + } +} diff --git a/init.go b/init.go index 0c11354..a8506c2 100644 --- a/init.go +++ b/init.go @@ -41,6 +41,8 @@ func init() { friendsGRPCAPIKey := os.Getenv("PN_SPLATOON_FRIENDS_GRPC_API_KEY") tokenAesKey := os.Getenv("PN_SPLATOON_AES_KEY") localAuthMode := os.Getenv("PN_SPLATOON_LOCAL_AUTH") + GRPCServerAPIKey := os.Getenv("PN_SPLATOON_GRPC_SERVER_API_KEY") + GRPCServerPort := os.Getenv("PN_SPLATOON_GRPC_SERVER_PORT") kerberosPassword := make([]byte, 0x10) _, err = rand.Read(kerberosPassword) @@ -135,6 +137,28 @@ func init() { os.Exit(0) } + // + if strings.TrimSpace(GRPCServerAPIKey) == "" { + globals.Logger.Error("PN_SPLATOON_GRPC_SERVER_API_KEY environment variable not set") + os.Exit(0) + } + globals.GRPCServerAPIKey = GRPCServerAPIKey + if strings.TrimSpace(GRPCServerPort) == "" { + globals.Logger.Error("PN_SPLATOON_GRPC_SERVER_PORT environment variable not set") + os.Exit(0) + } + + if port, err := strconv.Atoi(GRPCServerPort); err != nil { + globals.Logger.Errorf("PN_SPLATOON_GRPC_SERVER_PORT is not a valid port. Expected 0-65535, got %s", accountGRPCPort) + os.Exit(0) + } else if port < 0 || port > 65535 { + globals.Logger.Errorf("PN_SPLATOON_GRPC_SERVER_PORT is not a valid port. Expected 0-65535, got %s", accountGRPCPort) + os.Exit(0) + } else { + globals.GRPCServerPort = port + } + // + if strings.TrimSpace(friendsGRPCAPIKey) == "" { globals.Logger.Warning("Insecure gRPC server detected. PN_SPLATOON_FRIENDS_GRPC_API_KEY environment variable not set") } diff --git a/main.go b/main.go index 5fe0b2f..cda0c0f 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "sync" + "github.com/PretendoNetwork/splatoon/grpc" "github.com/PretendoNetwork/splatoon/nex" ) @@ -14,6 +15,6 @@ func main() { // TODO - Add gRPC server go nex.StartAuthenticationServer() go nex.StartSecureServer() - + go grpc.StartGRPCServer() wg.Wait() }