mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
107 lines
3.2 KiB
GDScript
107 lines
3.2 KiB
GDScript
class_name ServerControl
|
|
extends Node
|
|
|
|
# Small loopback HTTP control surface for allocated servers. The Go supervisor
|
|
# uses GET /ready as the explicit process-ready probe and POST /drain during a
|
|
# controlled termination. Direct/community servers do not start this node.
|
|
|
|
signal drain_requested
|
|
|
|
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 == "GET" and path == "/ready":
|
|
status = 200 if _ready_for_connections else 503
|
|
reason = "OK" if status == 200 else "Service Unavailable"
|
|
elif method == "GET" 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"
|
|
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
|