diff options
| author | pg9182 <96569817+pg9182@users.noreply.github.com> | 2022-10-15 05:44:41 -0400 |
|---|---|---|
| committer | pg9182 <96569817+pg9182@users.noreply.github.com> | 2022-10-15 05:44:41 -0400 |
| commit | 49423ea779a521f8bc131bb862bececdd73dc47e (patch) | |
| tree | c8f604aac6e783175fbafaad0ccedafa57ee3657 /pkg | |
| parent | 05b81c4db902bc9f8acea1400d5e0836a78a565a (diff) | |
| download | Atlas-49423ea779a521f8bc131bb862bececdd73dc47e.tar.gz Atlas-49423ea779a521f8bc131bb862bececdd73dc47e.zip | |
pkg/api/api0: Implement /server/add_server
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/api/api0/api.go | 23 | ||||
| -rw-r--r-- | pkg/api/api0/server.go | 304 |
2 files changed, 325 insertions, 2 deletions
diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go index b376a14..af3505a 100644 --- a/pkg/api/api0/api.go +++ b/pkg/api/api0/api.go @@ -49,6 +49,14 @@ type Handler struct { // NotFound handles requests not handled by this Handler. NotFound http.Handler + // MaxServers limits the number of registered servers. If -1, no limit is + // applied. If 0, a reasonable default is used. + MaxServers int + + // MaxServersPerIP limits the number of registered servers per IP. If -1, no + // limit is applied. If 0, a reasonable default is used. + MaxServersPerIP int + // InsecureDevNoCheckPlayerAuth is an option you shouldn't use since it // makes the server trust that clients are who they say they are. Blame // @BobTheBob9 for this option even existing in the first place. @@ -62,6 +70,9 @@ type Handler struct { // TokenExpiryTime controls the expiry of player masterserver auth tokens. // If zero, a reasonable a default is used. TokenExpiryTime time.Duration + + // AllowGameServerIPv6 controls whether to allow game servers to use IPv6. + AllowGameServerIPv6 bool } // ServeHTTP routes requests to Handler. @@ -77,6 +88,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.handleClientAuthWithSelf(w, r) case "/client/servers": h.handleClientServers(w, r) + case "/server/add_server": + h.handleServerAddServer(w, r) case "/accounts/write_persistence": h.handleAccountsWritePersistence(w, r) case "/accounts/get_username": @@ -199,3 +212,13 @@ func marshalJSONBytesAsArray(b []byte) json.RawMessage { e.WriteByte(']') return json.RawMessage(e.Bytes()) } + +func checkLimit(limit, def, cur int) bool { + if limit == -1 { + return true + } + if limit == 0 && cur < def { + return true + } + return cur < limit +} diff --git a/pkg/api/api0/server.go b/pkg/api/api0/server.go index d1a94bb..4fd3b43 100644 --- a/pkg/api/api0/server.go +++ b/pkg/api/api0/server.go @@ -1,8 +1,22 @@ package api0 +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/netip" + "strconv" + "time" + + "github.com/pg9182/atlas/pkg/a2s" + "github.com/rs/zerolog/hlog" +) + /* - /server/add_server: - POST: /server/heartbeat: POST: /server/update_values: @@ -10,3 +24,289 @@ package api0 /server/remove_server: DELETE: */ + +func (h *Handler) handleServerAddServer(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodOptions && r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Cache-Control", "private, no-cache, no-store") + w.Header().Set("Expires", "0") + w.Header().Set("Pragma", "no-cache") + + if r.Method == http.MethodOptions { + w.Header().Set("Allow", "OPTIONS, POST") + w.WriteHeader(http.StatusNoContent) + return + } + + if !h.checkLauncherVersion(r) { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_UNSUPPORTED_VERSION, + }) + return + } + + raddr, err := netip.ParseAddrPort(r.RemoteAddr) + if err != nil { + hlog.FromRequest(r).Error(). + Err(err). + Msgf("failed to parse remote ip %q", r.RemoteAddr) + respJSON(w, r, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(), + }) + return + } + + if !h.AllowGameServerIPv6 { + if raddr.Addr().Is6() { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_NO_GAMESERVER_RESPONSE, + "msg": ErrorCode_NO_GAMESERVER_RESPONSE.Messagef("ipv6 is not currently supported (ip %s)", raddr.Addr()), + }) + return + } + } + + var s Server + + if v := r.URL.Query().Get("port"); v == "" { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_BAD_REQUEST, + "msg": ErrorCode_BAD_REQUEST.Messagef("port param is required"), + }) + return + } else if n, err := strconv.ParseUint(v, 10, 16); err != nil { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_BAD_REQUEST, + "msg": ErrorCode_BAD_REQUEST.Messagef("port param is invalid: %v", err), + }) + return + } else { + s.Addr = netip.AddrPortFrom(raddr.Addr(), uint16(n)) + } + + if v := r.URL.Query().Get("authPort"); v == "" { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_BAD_REQUEST, + "msg": ErrorCode_BAD_REQUEST.Messagef("authPort param is required"), + }) + return + } else if n, err := strconv.ParseUint(v, 10, 16); err != nil { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_BAD_REQUEST, + "msg": ErrorCode_BAD_REQUEST.Messagef("authPort param is invalid: %v", err), + }) + return + } else { + s.AuthPort = uint16(n) + } + + if v := r.URL.Query().Get("name"); v == "" { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_BAD_REQUEST, + "msg": ErrorCode_BAD_REQUEST.Messagef("name param must not be empty"), + }) + return + } else { + // TODO: bad word censoring + if n := 256; len(v) > n { // NorthstarLauncher@v1.9.7 limits it to 63 + v = v[:n] + } + s.Name = v + } + + if v := r.URL.Query().Get("description"); v != "" { + // TODO: bad word censoring + if n := 1024; len(v) > n { // NorthstarLauncher@v1.9.7 doesn't have a limit + v = v[:n] + } + s.Description = v + } + + if v := r.URL.Query().Get("map"); v != "" { + if n := 64; len(v) > n { // NorthstarLauncher@v1.9.7 limits it to 31 + v = v[:n] + } + s.Map = v + } + + if v := r.URL.Query().Get("playlist"); v != "" { + if n := 64; len(v) > n { // NorthstarLauncher@v1.9.7 limits it to 15 + v = v[:n] + } + s.Playlist = v + } + + if n, err := strconv.ParseUint(r.URL.Query().Get("maxPlayers"), 10, 8); err == nil { + s.MaxPlayers = int(n) + } + + if v := r.URL.Query().Get("password"); len(v) > 128 { + respJSON(w, r, http.StatusBadRequest, map[string]any{ + "success": false, + "error": ErrorCode_BAD_REQUEST, + "msg": ErrorCode_BAD_REQUEST.Messagef("password is too long"), + }) + return + } else { + s.Password = v + } + + var modInfoErr error + if err := r.ParseMultipartForm(1 << 18 /*.25 MB*/); err == nil { + if mf, mfHdr, err := r.FormFile("modinfo"); err == nil { + if mfHdr.Size < 1<<18 { + var obj struct { + Mods []struct { + Name string `json:"Name"` + Version string `json:"Version"` + RequiredOnClient bool `json:"RequiredOnClient"` + } `json:"Mods"` + } + if err := json.NewDecoder(mf).Decode(&obj); err == nil { + for _, m := range obj.Mods { + if m.Name != "" { + if m.Version == "" { + m.Version = "0.0.0" + } + s.ModInfo = append(s.ModInfo, ServerModInfo{ + Name: m.Name, + Version: m.Version, + RequiredOnClient: m.RequiredOnClient, + }) + } + } + } else { + modInfoErr = fmt.Errorf("parse modinfo file: %w", err) + } + } else { + modInfoErr = fmt.Errorf("get modinfo file: too large (size %d)", mfHdr.Size) + } + mf.Close() + } else { + modInfoErr = fmt.Errorf("get modinfo file: %w", err) + } + } else { + modInfoErr = fmt.Errorf("parse multipart form: %w", err) + } + if modInfoErr != nil { + hlog.FromRequest(r).Warn(). + Err(err). + Msgf("failed to parse modinfo") + } + + verifyDeadline := time.Now().Add(time.Second * 10) + if err := func() error { + ctx, cancel := context.WithDeadline(r.Context(), verifyDeadline) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://%s/verify", s.AuthAddr()), nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "Atlas") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + buf, err := io.ReadAll(io.LimitReader(resp.Body, 100)) + if err != nil { + return err + } + if string(bytes.TrimSpace(buf)) != "I am a northstar server!" { + return fmt.Errorf("unexpected response") + } + return nil + }(); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + err = fmt.Errorf("request timed out") + } + respJSON(w, r, http.StatusBadGateway, map[string]any{ + "success": false, + "error": ErrorCode_BAD_GAMESERVER_RESPONSE, + "msg": ErrorCode_BAD_GAMESERVER_RESPONSE.Messagef("failed to connect to auth port: %v", err), + }) + return + } + if err := a2s.Probe(s.Addr, time.Until(verifyDeadline)); err != nil { + respJSON(w, r, http.StatusBadGateway, map[string]any{ + "success": false, + "error": ErrorCode_BAD_GAMESERVER_RESPONSE, + "msg": ErrorCode_BAD_GAMESERVER_RESPONSE.Messagef("failed to connect to game port: %v", err), + }) + return + } + + if tok, err := cryptoRandHex(32); err != nil { + hlog.FromRequest(r).Error(). + Err(err). + Msgf("failed to generate random token") + respJSON(w, r, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(), + }) + return + } else { + s.ServerAuthToken = tok + } + + // these checks are racy, but it's meant to be a safety net, not a hard limit + if !checkLimit(h.MaxServers, 1000, h.ServerList.GetServerCountByIP(netip.Addr{})) { + respJSON(w, r, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": ErrorCode_INTERNAL_SERVER_ERROR.Messagef("game server limit reached"), + }) + return + } + if !checkLimit(h.MaxServers, 50, h.ServerList.GetServerCountByIP(s.Addr.Addr())) { + respJSON(w, r, http.StatusTooManyRequests, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": ErrorCode_INTERNAL_SERVER_ERROR.Messagef("game server per-ip limit reached"), + }) + return + } + + if sid, _, err := h.ServerList.PutServerByAddr(&s); err == nil { + s.ID = sid + } else if errors.Is(err, ErrServerListDuplicateAuthAddr) { + respJSON(w, r, http.StatusForbidden, map[string]any{ + "success": false, + "error": ErrorCode_DUPLICATE_SERVER, + "msg": ErrorCode_DUPLICATE_SERVER.Messagef("%v", err), + }) + return + } else { + hlog.FromRequest(r).Error(). + Err(err). + Msgf("failed to add server to list") + respJSON(w, r, http.StatusInternalServerError, map[string]any{ + "success": false, + "error": ErrorCode_INTERNAL_SERVER_ERROR, + "msg": ErrorCode_INTERNAL_SERVER_ERROR.Message(), + }) + return + } + + respJSON(w, r, http.StatusInternalServerError, map[string]any{ + "success": true, + "id": s.ID, + "serverAuthToken": s.ServerAuthToken, + }) +} |
