Skip to content

Instantly share code, notes, and snippets.

@nisovin
Last active April 20, 2022 22:24
Show Gist options
  • Save nisovin/a65141875334c9586fb477801246eec0 to your computer and use it in GitHub Desktop.
Save nisovin/a65141875334c9586fb477801246eec0 to your computer and use it in GitHub Desktop.
GDScript Web Server
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"
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