Last active
April 20, 2022 22:24
-
-
Save nisovin/a65141875334c9586fb477801246eec0 to your computer and use it in GitHub Desktop.
GDScript Web Server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends Node | |
const HTTP_PORT = 8888 | |
var server | |
func _ready(): | |
# Create the HTTP server | |
server = WebServer.new() | |
# Enable Basic HTTP auth if desired | |
server.require_auth("admin", "password123") | |
# Use a string to just send text back | |
server.route("/", "WOW IT'S A WEBSITE") | |
# Use a funcref to handle requests | |
server.route("/test", funcref(self, "test")) | |
# Use a string starting with res:// or user:// to read and send a file's contents | |
server.route("/page", "res://page.html") | |
# Use an Image, Texture, or AudioStream | |
server.route("/test.png", load("res://icon.png")) | |
server.route("/pic", "<img src='test.png'>") | |
# Use a dictionary for custom functionality | |
server.route("/godotsite", {"code": "302 Found", "headers": {"Location": "https://www.godotengine.org"}}) | |
# Set a default route if desired | |
server.default({"code": "404 Not Found"}) | |
# Start server on given port | |
server.start(HTTP_PORT) | |
func test(request): | |
# request var contains these keys: "method", "uri", "path", "query", "vars", "headers", "body", "data" (POST) | |
print(request) | |
if "code" in request.vars: | |
print(request.vars.code) | |
return "YES" | |
return "NO" | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends Reference | |
class_name WebServer | |
var tcp := TCP_Server.new() | |
var default_route = null | |
var routes = {} | |
var need_auth = false | |
var username = "" | |
var password = "" | |
func route(path, response): | |
routes[path] = response | |
func default(response): | |
default_route = response | |
func require_auth(user, passwd): | |
need_auth = true | |
username = user | |
password = passwd | |
func start(port = 80, host = "*"): | |
Engine.get_main_loop().connect("idle_frame", self, "poll") | |
return tcp.listen(port, host) | |
func stop(): | |
tcp.stop() | |
Engine.get_main_loop().disconnect("idle_frame", self, "poll") | |
func reset(): | |
routes = {} | |
func poll(): | |
if not tcp.is_connection_available(): return | |
var conn = tcp.take_connection() | |
var data = conn.get_utf8_string(conn.get_available_bytes()) | |
var request = _parse_http_request(data) | |
var response = _handle_request(request) | |
if response == null: | |
response = {"code": "404 Not Found"} | |
var bytes = _build_http_response(response) | |
conn.put_data(bytes) | |
conn.disconnect_from_host() | |
func _parse_http_request(data): | |
var split = data.split("\r\n\r\n") | |
var lines = Array(split[0].split("\r\n")) | |
var request_line = lines.pop_front() | |
var request_data = request_line.split(" ") | |
var request = {} | |
request.method = request_data[0] | |
request.uri = request_data[1] | |
var question = request.uri.find("?") | |
if question > 0: | |
request.path = request.uri.substr(0, question) | |
request.query = request.uri.substr(question + 1) | |
request.vars = _process_query_string(request.query) | |
else: | |
request.path = request.uri | |
request.query = "" | |
request.vars = {} | |
request.headers = {} | |
for line in lines: | |
var colon = line.find(":") | |
if colon > 0: | |
var header_name = line.substr(0, colon).to_lower() | |
var header_content = line.substr(colon + 1).strip_edges() | |
request.headers[header_name] = header_content | |
request.body = split[1] | |
if request.method.to_upper() == "POST": | |
if "content-type" in request.headers and request.headers["content-type"].to_lower() == "application/x-www-form-urlencoded": | |
request.data = _process_query_string(request.body) | |
else: | |
request.data = {} | |
return request | |
func _process_query_string(q): | |
var vars = q.split("&") | |
var dict = {} | |
for v in vars: | |
var eq = v.find("=") | |
if eq > 0: | |
var key = v.substr(0, eq) | |
var val = v.substr(eq + 1) | |
dict[key] = val | |
else: | |
dict[v] = "" | |
return dict | |
func _build_http_response(response): | |
if typeof(response) == TYPE_STRING: | |
response = {"content": response} | |
var response_text = "HTTP/1.1 " | |
var code = "200 OK" | |
if "code" in response: | |
code = str(response.code) | |
response_text += code + "\r\n" | |
var content_type = false | |
var content_length = false | |
if "headers" in response: | |
if typeof(response.headers) == TYPE_DICTIONARY: | |
for header_name in response.headers: | |
response_text += header_name + ": " + str(response.headers[header_name]) + "\r\n" | |
if header_name.to_lower() == "content-type": | |
content_type = true | |
if header_name.to_lower() == "content-length": | |
content_length = true | |
elif typeof(response.headers) == TYPE_ARRAY: | |
for header in response.headers: | |
response_text += header + "\r\n" | |
if header.to_lower().begins_with("content-type"): | |
content_type = true | |
if header.to_lower().begins_with("content-length"): | |
content_length = true | |
if not content_type: | |
response_text += "Content-Type: text/html\r\n" | |
if not content_length and "content" in response: | |
response_text += "Content-Length: " + str(response.content.to_utf8().size()) + "\r\n" | |
response_text += "\r\n" | |
var response_bytes = response_text.to_utf8() | |
if "content" in response: | |
if typeof(response.content) == TYPE_STRING: | |
response_bytes.append_array(response.content.to_utf8()) | |
elif response.content is PoolByteArray: | |
response_bytes.append_array(response.content) | |
return response_bytes | |
func _handle_request(request): | |
if need_auth: | |
var authed = false | |
if "authorization" in request.headers: | |
var a = request.headers["authorization"] | |
if a.begins_with("Basic "): | |
a = a.substr(6) | |
var login = Marshalls.base64_to_utf8(a).split(":") | |
authed = login.size() == 2 and login[0] == username and login[1] == password | |
if not authed: | |
return {"code": "401 Unauthorized", "headers": ["WWW-Authenticate: Basic realm=\"Login\""]} | |
var response = default_route | |
if request.path in routes: | |
response = routes[request.path] | |
if response == null: | |
return null | |
elif typeof(response) == TYPE_INT: | |
return {"code": response} | |
elif typeof(response) == TYPE_STRING: | |
if response.begins_with("res://") or response.begins_with("user://"): | |
return _load_file(response) | |
else: | |
return response | |
elif typeof(response) == TYPE_DICTIONARY: | |
return response | |
elif response is FuncRef: | |
return response.call_func(request) | |
elif response is Image: | |
return _image_content(response) | |
elif response is Texture: | |
return _image_content(response.get_data()) | |
elif response is AudioStreamMP3 or response is AudioStreamOGGVorbis: | |
return _audio_content(response) | |
return null | |
func _image_content(image: Image): | |
var data = image.save_png_to_buffer() | |
return { | |
"headers": { | |
"Content-Type": "image/png", | |
"Content-Length": data.size() | |
}, | |
"content": data | |
} | |
func _audio_content(stream: AudioStream): | |
return { | |
"headers": { | |
"Content-Type": "audio/" + ("mp3" if stream is AudioStreamMP3 else "ogg"), | |
"Content-Length": stream.data.size() | |
}, | |
"content": stream.data | |
} | |
func _load_file(filename: String): | |
var exten = filename.get_extension() | |
var file = File.new() | |
file.open(filename, File.READ) | |
if exten == "png" or exten == "jpg": | |
var bytes = file.get_buffer(file.get_len()) | |
file.close() | |
return { | |
"headers": { | |
"Content-Type": "image/" + exten, | |
"Content-Length": bytes.size() | |
}, | |
"content": bytes | |
} | |
else: | |
var text = file.get_as_text() | |
file.close() | |
return text | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment