Skip to content

Commit

Permalink
Support for runtime custom HTTP handlers, build with Go 1.23.2, updat…
Browse files Browse the repository at this point in the history
…e changelog.
  • Loading branch information
zyro committed Oct 21, 2024
1 parent 8ea2c95 commit 1bfce49
Show file tree
Hide file tree
Showing 24 changed files with 287 additions and 166 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
## [Unreleased]
### Added
- New runtime function to list user notifications.
- Support for runtime registration of custom HTTP handlers.

### Changed
- Increased limit on runtimes group users list functions.
- Added pagination support to storage index listing.
- Update runtime Satori client for latest API changes.
- Build with Go 1.23.2.

### Fixed
- Ensure matchmaker stats behave correctly if matchmaker becomes fully empty and idle.
Expand Down
2 changes: 1 addition & 1 deletion build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version=v2.1.1 -t heroiclabs/nakama:2.1.1
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version="$(git rev-parse --short HEAD)" -t heroiclabs/nakama-prerelease:"$(git rev-parse --short HEAD)"

FROM golang:1.22.5-bookworm as builder
FROM golang:1.23.2-bookworm as builder

ARG commit
ARG version
Expand Down
2 changes: 1 addition & 1 deletion build/Dockerfile.arm64
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version=v2.1.1 -t heroiclabs/nakama:2.1.1
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version="$(git rev-parse --short HEAD)" -t heroiclabs/nakama-prerelease:"$(git rev-parse --short HEAD)"

FROM arm64v8/golang:1.22.5-bookworm as builder
FROM arm64v8/golang:1.23.2-bookworm as builder

ARG commit
ARG version
Expand Down
2 changes: 1 addition & 1 deletion build/Dockerfile.dsym
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version=v3.19.0 -t heroiclabs/nakama:3.19.0
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version="v3.19.0-$(git rev-parse --short HEAD)" -t heroiclabs/nakama-prerelease:"3.19.0-$(git rev-parse --short HEAD)"

FROM golang:1.22.5-bookworm as builder
FROM golang:1.23.2-bookworm as builder

ARG commit
ARG version
Expand Down
2 changes: 1 addition & 1 deletion build/Dockerfile.dsym.arm64
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version=v3.19.0 -t heroiclabs/nakama:3.19.0
# docker build "$PWD" --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version="v3.19.0-$(git rev-parse --short HEAD)" -t heroiclabs/nakama-prerelease:"3.19.0-$(git rev-parse --short HEAD)"

FROM arm64v8/golang:1.22.5-bookworm as builder
FROM arm64v8/golang:1.23.2-bookworm as builder

ARG commit
ARG version
Expand Down
2 changes: 1 addition & 1 deletion build/Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# docker build .. -f Dockerfile.local -t nakama:dev

FROM golang:1.22.5-bookworm AS builder
FROM golang:1.23.2-bookworm AS builder

ENV GOOS linux
ENV CGO_ENABLED 1
Expand Down
2 changes: 1 addition & 1 deletion build/pluginbuilder/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# docker build . --file ./Dockerfile --build-arg version=v2.1.1 -t heroiclabs/nakama-pluginbuilder:2.1.1
# docker build . --file ./Dockerfile --build-arg version="v2.1.1-$(git rev-parse --short HEAD)" -t heroiclabs/nakama-pluginbuilder:"2.1.1-$(git rev-parse --short HEAD)"

FROM golang:1.22.5-bookworm as builder
FROM golang:1.23.2-bookworm as builder

MAINTAINER Heroic Labs <[email protected]>

Expand Down
2 changes: 1 addition & 1 deletion build/pluginbuilder/Dockerfile.arm64
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# docker build "$PWD" --file ./Dockerfile.pluginbuilder --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version=v2.1.1 -t heroiclabs/nakama-pluginbuilder:2.1.1
# docker build "$PWD" --file ./Dockerfile.pluginbuilder --build-arg commit="$(git rev-parse --short HEAD)" --build-arg version="v2.1.1-$(git rev-parse --short HEAD)" -t heroiclabs/nakama-prerelease:"2.1.1-$(git rev-parse --short HEAD)"

FROM arm64v8/golang:1.22.5-bookworm as builder
FROM arm64v8/golang:1.23.2-bookworm as builder

MAINTAINER Heroic Labs <[email protected]>

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/heroiclabs/nakama/v3

go 1.21
go 1.23.2

require (
github.com/blugelabs/bluge v0.2.2
Expand All @@ -14,7 +14,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0
github.com/heroiclabs/nakama-common v1.33.1-0.20241021170115-c7b5486ef0aa
github.com/heroiclabs/nakama-common v1.34.0
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx/v5 v5.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/heroiclabs/nakama-common v1.33.1-0.20241021170115-c7b5486ef0aa h1:AWkD2AwYSMexj1mPArQvs2xkZTkvXvzqdvHlN4UeHOs=
github.com/heroiclabs/nakama-common v1.33.1-0.20241021170115-c7b5486ef0aa/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
github.com/heroiclabs/nakama-common v1.34.0 h1:7/F5v5yoCFBMTn5Aih/cqR/GW7hbEbup8blq5OmhzjM=
github.com/heroiclabs/nakama-common v1.34.0/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a h1:tuL2ZPaeCbNw8rXmV9ywd00nXRv95V4/FmbIGKLQJAE=
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a/go.mod h1:hzCTPoEi/oml2BllVydJcNP63S7b56e5DzeQeLGvw1U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
Expand Down
7 changes: 7 additions & 0 deletions sample_go_module/sample.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package main
import (
"context"
"database/sql"
"net/http"

"github.com/heroiclabs/nakama-common/api"
"github.com/heroiclabs/nakama-common/rtapi"
Expand Down Expand Up @@ -54,6 +55,12 @@ func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runti
return err
}

if err := initializer.RegisterHttp("/test", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("handling twat"))
}); err != nil {
return err
}

return nil
}

Expand Down
54 changes: 34 additions & 20 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ import (
"crypto/x509"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"github.com/heroiclabs/nakama/v3/internal/satori"
"google.golang.org/grpc/grpclog"
"math"
"net"
"net/http"
Expand All @@ -34,19 +33,21 @@ import (
"time"

"github.com/gofrs/uuid/v5"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
grpcgw "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/heroiclabs/nakama-common/api"
"github.com/heroiclabs/nakama/v3/apigrpc"
"github.com/heroiclabs/nakama/v3/internal/satori"
"github.com/heroiclabs/nakama/v3/social"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
_ "google.golang.org/grpc/encoding/gzip" // enable gzip compression on server for grpc
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -169,7 +170,7 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, p
grpcgw.WithRoutingErrorHandler(handleRoutingError),
grpcgw.WithMetadata(func(ctx context.Context, r *http.Request) metadata.MD {
// For RPC GET operations pass through any custom query parameters.
if r.Method != "GET" || !strings.HasPrefix(r.URL.Path, "/v2/rpc/") {
if r.Method != http.MethodGet || !strings.HasPrefix(r.URL.Path, "/v2/rpc/") {
return metadata.MD{}
}

Expand Down Expand Up @@ -227,12 +228,22 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, p

grpcGatewayRouter := mux.NewRouter()
// Special case routes. Do NOT enable compression on WebSocket route, it results in "http: response.Write on hijacked connection" errors.
grpcGatewayRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }).Methods("GET")
grpcGatewayRouter.HandleFunc("/ws", NewSocketWsAcceptor(logger, config, sessionRegistry, sessionCache, statusRegistry, matchmaker, tracker, metrics, runtime, protojsonMarshaler, protojsonUnmarshaler, pipeline)).Methods("GET")
grpcGatewayRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }).Methods(http.MethodGet)
grpcGatewayRouter.HandleFunc("/ws", NewSocketWsAcceptor(logger, config, sessionRegistry, sessionCache, statusRegistry, matchmaker, tracker, metrics, runtime, protojsonMarshaler, protojsonUnmarshaler, pipeline)).Methods(http.MethodGet)

// Another nested router to hijack RPC requests bound for GRPC Gateway.
grpcGatewayMux := mux.NewRouter()
grpcGatewayMux.HandleFunc("/v2/rpc/{id:.*}", s.RpcFuncHttp).Methods("GET", "POST")
grpcGatewayMux.HandleFunc("/v2/rpc/{id:.*}", s.RpcFuncHttp).Methods(http.MethodGet, http.MethodPost)
for _, handler := range runtime.httpHandlers {
if handler == nil {
continue
}
route := grpcGatewayMux.HandleFunc(handler.PathPattern, handler.Handler)
if len(handler.Methods) > 0 {
route.Methods(handler.Methods...)
}
logger.Info("Registered custom HTTP handler", zap.String("path_pattern", handler.PathPattern))
}
grpcGatewayMux.NewRoute().Handler(grpcGateway)

// Enable stats recording on all request paths except:
Expand Down Expand Up @@ -270,7 +281,7 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, p
// Enable CORS on all requests.
CORSHeaders := handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "User-Agent"})
CORSOrigins := handlers.AllowedOrigins([]string{"*"})
CORSMethods := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE"})
CORSMethods := handlers.AllowedMethods([]string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodDelete})
handlerWithCORS := handlers.CORS(CORSHeaders, CORSOrigins, CORSMethods)(grpcGatewayRouter)

// Enable configured response headers, if any are set. Do not override values that may have been set by server processing.
Expand Down Expand Up @@ -310,11 +321,11 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, p
}

if config.GetSocket().TLSCert != nil {
if err := s.grpcGatewayServer.ServeTLS(listener, "", ""); err != nil && err != http.ErrServerClosed {
if err := s.grpcGatewayServer.ServeTLS(listener, "", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
startupLogger.Fatal("API server gateway listener failed", zap.Error(err))
}
} else {
if err := s.grpcGatewayServer.Serve(listener); err != nil && err != http.ErrServerClosed {
if err := s.grpcGatewayServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
startupLogger.Fatal("API server gateway listener failed", zap.Error(err))
}
}
Expand Down Expand Up @@ -513,7 +524,7 @@ func parseToken(hmacSecretByte []byte, tokenString string) (userID uuid.UUID, us
}

func decompressHandler(logger *zap.Logger, h http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Header.Get("Content-Encoding") {
case "gzip":
gr, err := gzip.NewReader(r.Body)
Expand All @@ -528,7 +539,7 @@ func decompressHandler(logger *zap.Logger, h http.Handler) http.HandlerFunc {
// No request compression.
}
h.ServeHTTP(w, r)
})
}
}

func extractClientAddressFromContext(logger *zap.Logger, ctx context.Context) (string, string) {
Expand Down Expand Up @@ -566,14 +577,17 @@ func extractClientAddress(logger *zap.Logger, clientAddr string, source interfac
if host, port, err := net.SplitHostPort(clientAddr); err == nil {
clientIP = host
clientPort = port
} else if addrErr, ok := err.(*net.AddrError); ok {
switch addrErr.Err {
case "missing port in address":
fallthrough
case "too many colons in address":
clientIP = clientAddr
default:
// Unknown address error, ignore the address.
} else {
var addrErr *net.AddrError
if errors.As(err, &addrErr) {
switch addrErr.Err {
case "missing port in address":
fallthrough
case "too many colons in address":
clientIP = clientAddr
default:
// Unknown address error, ignore the address.
}
}
}
// At this point err may still be a non-nil value that's not a *net.AddrError, ignore the address.
Expand Down
4 changes: 2 additions & 2 deletions server/api_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (s *ApiServer) GetAccount(ctx context.Context, in *emptypb.Empty) (*api.Acc

account, err := GetAccount(ctx, s.logger, s.db, s.statusRegistry, userID)
if err != nil {
if err == ErrAccountNotFound {
if errors.Is(err, ErrAccountNotFound) {
return nil, status.Error(codes.NotFound, "Account not found.")
}
return nil, status.Error(codes.Internal, "Error retrieving user account.")
Expand Down Expand Up @@ -94,7 +94,7 @@ func (s *ApiServer) DeleteAccount(ctx context.Context, in *emptypb.Empty) (*empt
}

if err := DeleteAccount(ctx, s.logger, s.db, s.config, s.leaderboardCache, s.leaderboardRankCache, s.sessionRegistry, s.sessionCache, s.tracker, userID, false); err != nil {
if err == ErrAccountNotFound {
if errors.Is(err, ErrAccountNotFound) {
return nil, status.Error(codes.NotFound, "Account not found.")
}
return nil, status.Error(codes.Internal, "Error deleting user account.")
Expand Down
11 changes: 5 additions & 6 deletions server/api_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"time"

"github.com/gofrs/uuid/v5"
"github.com/gorilla/mux"
grpcgw "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/heroiclabs/nakama-common/api"
"go.uber.org/zap"
Expand Down Expand Up @@ -111,8 +110,8 @@ func (s *ApiServer) RpcFuncHttp(w http.ResponseWriter, r *http.Request) {
}()

// Check the RPC function ID.
maybeID, ok := mux.Vars(r)["id"]
if !ok || maybeID == "" {
maybeID := r.PathValue("id")
if maybeID == "" {
// Missing RPC function ID.
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusBadRequest)
Expand Down Expand Up @@ -144,7 +143,7 @@ func (s *ApiServer) RpcFuncHttp(w http.ResponseWriter, r *http.Request) {

// Prepare input to function.
var payload string
if r.Method == "POST" {
if r.Method == http.MethodPost {
b, err := io.ReadAll(r.Body)
if err != nil {
// Request body too large.
Expand Down Expand Up @@ -272,8 +271,8 @@ func (s *ApiServer) RpcFunc(ctx context.Context, in *api.Rpc) (*api.Rpc, error)
return nil, status.Error(codes.NotFound, "RPC function not found")
}

headers := make(map[string][]string, 0)
queryParams := make(map[string][]string, 0)
headers := make(map[string][]string)
queryParams := make(map[string][]string)
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Internal, "RPC function could not get incoming context")
Expand Down
26 changes: 13 additions & 13 deletions server/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"crypto"
"database/sql"
"errors"
"fmt"
"io"
"math"
Expand Down Expand Up @@ -318,7 +319,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.D
// Enable CORS on all requests.
CORSHeaders := handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "User-Agent"})
CORSOrigins := handlers.AllowedOrigins([]string{"*"})
CORSMethods := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"})
CORSMethods := handlers.AllowedMethods([]string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch})
handlerWithCORS := handlers.CORS(CORSHeaders, CORSOrigins, CORSMethods)(grpcGatewayRouter)

// Set up and start GRPC Gateway server.
Expand All @@ -332,7 +333,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.D

startupLogger.Info("Starting Console server gateway for HTTP requests", zap.Int("port", config.GetConsole().Port))
go func() {
if err := s.grpcGatewayServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := s.grpcGatewayServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
startupLogger.Fatal("Console server gateway listener failed", zap.Error(err))
}
}()
Expand Down Expand Up @@ -508,13 +509,16 @@ func checkAuth(ctx context.Context, logger *zap.Logger, config Config, auth stri
if username == config.GetConsole().Username {
if password != config.GetConsole().Password {
// Admin password does not match.
if lockout, until := loginAttemptCache.Add(config.GetConsole().Username, ip); lockout != LockoutTypeNone {
switch lockout {
case LockoutTypeAccount:
logger.Info(fmt.Sprintf("Console admin account locked until %v.", until))
case LockoutTypeIp:
logger.Info(fmt.Sprintf("Console admin IP locked until %v.", until))
}
lockout, until := loginAttemptCache.Add(config.GetConsole().Username, ip)
switch lockout {
case LockoutTypeAccount:
logger.Info(fmt.Sprintf("Console admin account locked until %v.", until))
case LockoutTypeIp:
logger.Info(fmt.Sprintf("Console admin IP locked until %v.", until))
case LockoutTypeNone:
fallthrough
default:
// No lockout.
}
return ctx, false
}
Expand Down Expand Up @@ -543,10 +547,6 @@ func checkAuth(ctx context.Context, logger *zap.Logger, config Config, auth stri
// The token or its claims are invalid.
return ctx, false
}
if !ok {
// Expiry time claim is invalid.
return ctx, false
}
if exp <= time.Now().UTC().Unix() {
// Token expired.
return ctx, false
Expand Down
Loading

0 comments on commit 1bfce49

Please sign in to comment.