class_name ServerControl extends Node # Small loopback HTTP control surface for lifecycle-managed servers. The Go # supervisor uses GET /ready as the explicit process-ready probe and POST # /drain during a controlled termination. Direct/community servers outside # Agones do not start this node. signal drain_requested signal initial_connect_ready var _listener := TCPServer.new() var _peers: Array = [] var _ready_for_connections := false var _draining := false var _drain_token := "" func start(port: int, drain_token: String = "") -> Error: if port < 1 or port > 65535: return ERR_INVALID_PARAMETER _drain_token = drain_token return _listener.listen(port, "127.0.0.1") func stop() -> void: _listener.stop() for peer in _peers: if is_instance_valid(peer): peer.disconnect_from_host() _peers.clear() func set_process_ready(value: bool) -> void: _ready_for_connections = value and not _draining func is_draining() -> bool: return _draining func _exit_tree() -> void: stop() func _process(_delta: float) -> void: while _listener.is_connection_available(): _peers.append(_listener.take_connection()) for i in range(_peers.size() - 1, -1, -1): var peer: StreamPeerTCP = _peers[i] if peer.get_status() != StreamPeerTCP.STATUS_CONNECTED: _peers.remove_at(i) continue var available := peer.get_available_bytes() if available <= 0: continue var request := peer.get_utf8_string(available) if "\r\n\r\n" not in request: continue _respond(peer, request) _peers.remove_at(i) func _respond(peer: StreamPeerTCP, request: String) -> void: var lines := request.split("\r\n") var first := lines[0].split(" ") if not lines.is_empty() else PackedStringArray() var method := String(first[0]) if first.size() > 0 else "" var path := String(first[1]) if first.size() > 1 else "" var status := 404 var reason := "Not Found" var body := "" if method in ["GET", "POST"] and path == "/ready": status = 200 if _ready_for_connections else 503 reason = "OK" if status == 200 else "Service Unavailable" elif method in ["GET", "POST"] and path == "/health": status = 200 reason = "OK" elif method == "POST" and path == "/drain": var supplied := "" for line in lines: if line.begins_with("Authorization: Bearer "): supplied = line.substr("Authorization: Bearer ".length()) if _drain_token.is_empty() or not _constant_time_equal(supplied, _drain_token): status = 401 reason = "Unauthorized" else: _draining = true _ready_for_connections = false drain_requested.emit() status = 202 reason = "Accepted" elif method == "POST" and path == "/initial-connect-ready": var supplied := "" for line in lines: if line.begins_with("Authorization: Bearer "): supplied = line.substr("Authorization: Bearer ".length()) if _drain_token.is_empty() or not _constant_time_equal(supplied, _drain_token): status = 401 reason = "Unauthorized" else: initial_connect_ready.emit() status = 202 reason = "Accepted" else: status = 405 if method in ["GET", "POST"] else 400 reason = "Method Not Allowed" if status == 405 else "Bad Request" body = "{\"status\":\"%s\"}" % ("ready" if status == 200 else "not_ready") var response := "HTTP/1.1 %d %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % [status, reason, body.to_utf8_buffer().size(), body] peer.put_data(response.to_utf8_buffer()) peer.disconnect_from_host() func _constant_time_equal(a: String, b: String) -> bool: var left := a.to_utf8_buffer() var right := b.to_utf8_buffer() var difference := left.size() ^ right.size() var length := mini(left.size(), right.size()) for i in length: difference |= left[i] ^ right[i] return difference == 0