@@ -141,28 +141,20 @@ All connections must use TLS 1.3 for encryption. This means you have to generate
|
||||
|
||||
### Setup for Production
|
||||
|
||||
For production deployments, you'll need to generate your own certificates since traditional Certificate Authorities don't support custom protocols:
|
||||
For production deployments, you'll need to install GurtCA from the Github repository for Gurted, and use it to request certificates for your domain.
|
||||
|
||||
1. **Generate production certificates with OpenSSL:**
|
||||
1. **Generate production certificates with GurtCA:**
|
||||
```bash
|
||||
# Generate private key
|
||||
openssl genpkey -algorithm RSA -out gurt-server.key -pkcs8 -v
|
||||
|
||||
# Generate certificate signing request
|
||||
openssl req -new -key gurt-server.key -out gurt-server.csr
|
||||
|
||||
# Generate self-signed certificate (valid for 365 days)
|
||||
openssl x509 -req -days 365 -in gurt-server.csr -signkey gurt-server.key -out gurt-server.crt
|
||||
|
||||
# Or generate both key and certificate in one step
|
||||
openssl req -x509 -newkey rsa:4096 -keyout gurt-server.key -out gurt-server.crt -days 365 -nodes
|
||||
gurtca request yourdomain.real --output ./certs
|
||||
```
|
||||
|
||||
2. **Deploy with production certificates:**
|
||||
```bash
|
||||
cargo run --release serve --cert gurt-server.crt --key gurt-server.key --host 0.0.0.0 --port 4878
|
||||
cargo run --release serve --cert ./certs/yourdomain.real.crt --key ./certs/yourdomain.real.key --host 0.0.0.0 --port 4878
|
||||
```
|
||||
|
||||
Be careful, your `.key` file is the private key, do not share it with anyone!
|
||||
|
||||
### Development Environment Setup
|
||||
|
||||
To set up a development environment for GURT, follow these steps:
|
||||
|
||||
22
docs/docs/lua/download.md
Normal file
22
docs/docs/lua/download.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Download API
|
||||
|
||||
The download API allows Lua scripts to trigger file downloads from URLs.
|
||||
|
||||
## gurt.download
|
||||
|
||||
Downloads a file from a URL and saves it to the user's default download location.
|
||||
|
||||
### Syntax
|
||||
|
||||
```lua
|
||||
download_id = gurt.download(url filename)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **url** (string): The URL to download from. Supports HTTP, HTTPS, and gurt:// protocols.
|
||||
- **filename** (string, optional): The filename to save as. If not provided, the filename will be extracted from the URL or default to "download".
|
||||
|
||||
### Returns
|
||||
|
||||
- **download_id** (string): A unique identifier for the download operation.
|
||||
@@ -48,6 +48,7 @@ const sidebars: SidebarsConfig = {
|
||||
'lua/crumbs',
|
||||
'lua/audio',
|
||||
'lua/canvas',
|
||||
'lua/download',
|
||||
'lua/network',
|
||||
'lua/regex',
|
||||
'lua/handling',
|
||||
|
||||
BIN
docs/static/img/docs/crt.gif
vendored
BIN
docs/static/img/docs/crt.gif
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 38 MiB After Width: | Height: | Size: 18 MiB |
BIN
docs/static/img/docs/download.gif
vendored
Normal file
BIN
docs/static/img/docs/download.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 960 KiB |
@@ -489,6 +489,11 @@ visible = false
|
||||
layout_mode = 2
|
||||
metadata/_tab_index = 2
|
||||
|
||||
[node name="Messages" type="VBoxContainer" parent="DevTools/TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
metadata/_tab_index = 3
|
||||
|
||||
[node name="Application" type="Label" parent="DevTools/TabContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
@@ -122,6 +122,7 @@ layout_mode = 2
|
||||
[node name="Popup" type="Button" parent="VBoxContainer/TabContainer"]
|
||||
custom_minimum_size = Vector2(50, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 1
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_344ge")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_hptm8")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_6bp64")
|
||||
@@ -138,6 +139,7 @@ layout_mode = 2
|
||||
[node name="NewTabButton" type="Button" parent="VBoxContainer/TabContainer"]
|
||||
custom_minimum_size = Vector2(50, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 1
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_344ge")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_ynf5e")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_6bp64")
|
||||
@@ -165,6 +167,7 @@ layout_mode = 2
|
||||
[node name="BackButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(45, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 1
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_d1ilt")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_fdnlq")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_d1ilt")
|
||||
@@ -175,6 +178,7 @@ icon_alignment = 1
|
||||
[node name="ForwardButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(45, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 1
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_d1ilt")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_fdnlq")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_d1ilt")
|
||||
@@ -185,6 +189,7 @@ icon_alignment = 1
|
||||
[node name="RefreshButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(45, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 1
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_d1ilt")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_fdnlq")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_d1ilt")
|
||||
@@ -216,6 +221,7 @@ stretch_mode = 5
|
||||
[node name="OptionsButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(45, 0)
|
||||
layout_mode = 2
|
||||
focus_mode = 1
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_d1ilt")
|
||||
theme_override_styles/hover = SubResource("StyleBoxFlat_fdnlq")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxFlat_d1ilt")
|
||||
|
||||
@@ -72,13 +72,8 @@ func _start_download(download_id: String, url: String, save_path: String, downlo
|
||||
|
||||
progress_ui.setup_download(download_id, download_data)
|
||||
progress_ui.download_cancelled.connect(_on_download_progress_cancelled)
|
||||
|
||||
var http_request = HTTPRequest.new()
|
||||
http_request.name = "DownloadRequest_" + download_id
|
||||
main_node.add_child(http_request)
|
||||
|
||||
|
||||
active_downloads[download_id] = {
|
||||
"http_request": http_request,
|
||||
"save_path": save_path,
|
||||
"progress_ui": progress_ui,
|
||||
"start_time": Time.get_ticks_msec() / 1000.0,
|
||||
@@ -88,25 +83,43 @@ func _start_download(download_id: String, url: String, save_path: String, downlo
|
||||
"filename": download_data.get("filename", ""),
|
||||
"current_site": download_data.get("current_site", "")
|
||||
}
|
||||
|
||||
|
||||
if url.begins_with("gurt://"):
|
||||
_download_gurt_resource(download_id, url)
|
||||
else:
|
||||
_start_http_download(download_id, url)
|
||||
|
||||
func _start_http_download(download_id: String, url: String):
|
||||
var http_request = HTTPRequest.new()
|
||||
http_request.name = "DownloadRequest_" + download_id
|
||||
main_node.add_child(http_request)
|
||||
|
||||
if not active_downloads.has(download_id):
|
||||
http_request.queue_free()
|
||||
return
|
||||
|
||||
active_downloads[download_id]["http_request"] = http_request
|
||||
|
||||
var save_path = active_downloads[download_id]["save_path"]
|
||||
http_request.set_download_file(save_path)
|
||||
|
||||
|
||||
http_request.request_completed.connect(func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
|
||||
_on_download_completed(download_id, result, response_code, headers, body)
|
||||
)
|
||||
|
||||
|
||||
var headers = ["User-Agent: GURT Browser 1.0"]
|
||||
var request_error = http_request.request(url, headers)
|
||||
|
||||
|
||||
if request_error != OK:
|
||||
var error_msg = "Failed to start download: " + str(request_error)
|
||||
print(error_msg)
|
||||
var progress_ui = active_downloads[download_id]["progress_ui"]
|
||||
if progress_ui:
|
||||
progress_ui.set_error(error_msg)
|
||||
http_request.queue_free()
|
||||
active_downloads.erase(download_id)
|
||||
return
|
||||
|
||||
|
||||
var timer = Timer.new()
|
||||
timer.name = "ProgressTimer_" + download_id
|
||||
timer.wait_time = 0.5
|
||||
@@ -114,12 +127,59 @@ func _start_download(download_id: String, url: String, save_path: String, downlo
|
||||
main_node.add_child(timer)
|
||||
timer.start()
|
||||
|
||||
func _download_gurt_resource(download_id: String, url: String):
|
||||
if not active_downloads.has(download_id):
|
||||
return
|
||||
|
||||
var progress_ui = active_downloads[download_id]["progress_ui"]
|
||||
var save_path = active_downloads[download_id]["save_path"]
|
||||
|
||||
if progress_ui:
|
||||
progress_ui.update_progress(0, 0, -1) # -1 indicates unknown total size
|
||||
|
||||
var resource_data = await Network.fetch_gurt_resource(url, true)
|
||||
|
||||
if not active_downloads.has(download_id):
|
||||
return
|
||||
|
||||
if resource_data.is_empty():
|
||||
var error_msg = "Failed to fetch gurt:// resource"
|
||||
print(error_msg)
|
||||
if progress_ui:
|
||||
progress_ui.set_error(error_msg)
|
||||
active_downloads.erase(download_id)
|
||||
return
|
||||
|
||||
var file = FileAccess.open(save_path, FileAccess.WRITE)
|
||||
if not file:
|
||||
var error_msg = "Failed to create download file: " + save_path
|
||||
print(error_msg)
|
||||
if progress_ui:
|
||||
progress_ui.set_error(error_msg)
|
||||
active_downloads.erase(download_id)
|
||||
return
|
||||
|
||||
file.store_buffer(resource_data)
|
||||
file.close()
|
||||
|
||||
var file_size = resource_data.size()
|
||||
|
||||
active_downloads[download_id]["total_bytes"] = file_size
|
||||
active_downloads[download_id]["downloaded_bytes"] = file_size
|
||||
|
||||
if progress_ui:
|
||||
progress_ui.set_completed(save_path)
|
||||
|
||||
_add_to_download_history(active_downloads[download_id], file_size, save_path)
|
||||
|
||||
active_downloads.erase(download_id)
|
||||
|
||||
func _update_download_progress(download_id: String):
|
||||
if not active_downloads.has(download_id):
|
||||
return
|
||||
|
||||
var download_info = active_downloads[download_id]
|
||||
var http_request = download_info.http_request
|
||||
var http_request = download_info.get("http_request", null)
|
||||
var progress_ui = download_info.progress_ui
|
||||
|
||||
if http_request and progress_ui:
|
||||
@@ -180,11 +240,12 @@ func _on_download_progress_cancelled(download_id: String):
|
||||
return
|
||||
|
||||
var download_info = active_downloads[download_id]
|
||||
|
||||
if download_info.http_request:
|
||||
download_info.http_request.cancel_request()
|
||||
download_info.http_request.queue_free()
|
||||
|
||||
|
||||
var http_request = download_info.get("http_request", null)
|
||||
if http_request:
|
||||
http_request.cancel_request()
|
||||
http_request.queue_free()
|
||||
|
||||
var timer = main_node.get_node_or_null("ProgressTimer_" + download_id)
|
||||
if timer:
|
||||
timer.queue_free()
|
||||
|
||||
@@ -138,4 +138,54 @@ func add_completed_request(url: String, method: String, is_from_lua: bool, statu
|
||||
all_requests.append(request)
|
||||
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.add_network_request(request)
|
||||
dev_tools_network_tab.add_network_request(request)
|
||||
|
||||
func start_websocket_connection(url: String, websocket_id: String) -> NetworkRequest:
|
||||
var request = NetworkRequest.create_websocket_connection(url, websocket_id)
|
||||
active_requests[request.id] = request
|
||||
all_requests.append(request)
|
||||
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.add_network_request(request)
|
||||
|
||||
request_started.emit(request)
|
||||
return request
|
||||
|
||||
func add_websocket_message(url: String, websocket_id: String, direction: String, message: String):
|
||||
var connection_request: NetworkRequest = null
|
||||
|
||||
for request in active_requests.values():
|
||||
if request.websocket_id == websocket_id and request.websocket_event_type == "connection":
|
||||
connection_request = request
|
||||
break
|
||||
|
||||
if not connection_request:
|
||||
for request in all_requests:
|
||||
if request.websocket_id == websocket_id and request.websocket_event_type == "connection":
|
||||
connection_request = request
|
||||
break
|
||||
|
||||
if connection_request:
|
||||
connection_request.add_websocket_message(direction, message)
|
||||
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.update_request_item(connection_request)
|
||||
|
||||
request_completed.emit(connection_request)
|
||||
|
||||
func update_websocket_connection(websocket_id: String, status: String, status_code: int = 200, status_text: String = "OK"):
|
||||
for request in active_requests.values():
|
||||
if request.websocket_id == websocket_id and request.websocket_event_type == "connection":
|
||||
request.update_websocket_status(status, status_code, status_text)
|
||||
|
||||
if status in ["closed", "error"]:
|
||||
active_requests.erase(request.id)
|
||||
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.update_request_item(request)
|
||||
|
||||
if request.status == NetworkRequest.RequestStatus.SUCCESS:
|
||||
request_completed.emit(request)
|
||||
else:
|
||||
request_failed.emit(request)
|
||||
break
|
||||
@@ -42,6 +42,31 @@ var response_body_bytes: PackedByteArray = []
|
||||
var mime_type: String = ""
|
||||
var is_from_lua: bool = false
|
||||
|
||||
var websocket_id: String = ""
|
||||
var websocket_event_type: String = "" # "connection", "close", "error"
|
||||
var connection_status: String = "" # "connecting", "open", "closing", "closed"
|
||||
var websocket_messages: Array[WebSocketMessage] = []
|
||||
|
||||
class WebSocketMessage:
|
||||
var hour: int
|
||||
var minute: int
|
||||
var second: int
|
||||
var direction: String # "sent" or "received"
|
||||
var content: String
|
||||
var size: int
|
||||
|
||||
func _init(dir: String, msg: String):
|
||||
var local_time = Time.get_datetime_dict_from_system(false)
|
||||
hour = local_time.hour
|
||||
minute = local_time.minute
|
||||
second = local_time.second
|
||||
direction = dir
|
||||
content = msg
|
||||
size = msg.length()
|
||||
|
||||
func get_formatted_time() -> String:
|
||||
return "%02d:%02d:%02d" % [hour, minute, second]
|
||||
|
||||
func _init(request_url: String = "", request_method: String = "GET"):
|
||||
id = generate_id()
|
||||
url = request_url
|
||||
@@ -62,6 +87,16 @@ func generate_id() -> String:
|
||||
func extract_name_from_url(request_url: String) -> String:
|
||||
if request_url.is_empty():
|
||||
return "Unknown"
|
||||
|
||||
if request_url.begins_with("ws://") or request_url.begins_with("wss://"):
|
||||
if not websocket_event_type.is_empty():
|
||||
match websocket_event_type:
|
||||
"connection":
|
||||
return "WebSocket"
|
||||
"close":
|
||||
return "WebSocket Close"
|
||||
"error":
|
||||
return "WebSocket Error"
|
||||
|
||||
var parts = request_url.split("/")
|
||||
if parts.size() > 0:
|
||||
@@ -177,7 +212,7 @@ static func format_bytes(given_size: int) -> String:
|
||||
elif given_size < 1024 * 1024:
|
||||
return str(given_size / 1024) + " KB"
|
||||
else:
|
||||
return str(given_size / (1024 * 1024)) + " MB"
|
||||
return str(given_size / (1024.0 * 1024)) + " MB"
|
||||
|
||||
func get_time_display() -> String:
|
||||
if status == RequestStatus.PENDING:
|
||||
@@ -205,3 +240,40 @@ func get_icon_texture() -> Texture2D:
|
||||
return load("res://Assets/Icons/arrow-down-up.svg")
|
||||
_:
|
||||
return load("res://Assets/Icons/search.svg")
|
||||
|
||||
static func create_websocket_connection(ws_url: String, ws_id: String) -> NetworkRequest:
|
||||
var request = NetworkRequest.new(ws_url, "WS")
|
||||
request.type = RequestType.SOCKET
|
||||
request.websocket_id = ws_id
|
||||
request.websocket_event_type = "connection"
|
||||
request.connection_status = "connecting"
|
||||
request.is_from_lua = true
|
||||
return request
|
||||
|
||||
func add_websocket_message(direction: String, message: String):
|
||||
var ws_message = WebSocketMessage.new(direction, message)
|
||||
websocket_messages.append(ws_message)
|
||||
|
||||
var total_message_size = 0
|
||||
for msg in websocket_messages:
|
||||
total_message_size += msg.size
|
||||
size = total_message_size
|
||||
|
||||
func update_websocket_status(new_status: String, status_code: int = 200, status_text: String = "OK"):
|
||||
connection_status = new_status
|
||||
self.status_code = status_code
|
||||
self.status_text = status_text
|
||||
|
||||
match new_status:
|
||||
"open":
|
||||
status = RequestStatus.SUCCESS
|
||||
"closed":
|
||||
if status_code >= 1000 and status_code < 1100:
|
||||
status = RequestStatus.SUCCESS
|
||||
else:
|
||||
status = RequestStatus.ERROR
|
||||
"error":
|
||||
status = RequestStatus.ERROR
|
||||
|
||||
end_time = Time.get_ticks_msec()
|
||||
time_ms = end_time - start_time
|
||||
|
||||
@@ -11,6 +11,7 @@ const NetworkRequestItemScene = preload("res://Scenes/NetworkRequestItem.tscn")
|
||||
@onready var headers_tab: VBoxContainer = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Headers
|
||||
@onready var preview_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Preview
|
||||
@onready var response_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Response
|
||||
@onready var messages_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Messages
|
||||
|
||||
# Header components
|
||||
@onready var status_header: Label = %StatusHeader
|
||||
@@ -31,7 +32,7 @@ const NetworkRequestItemScene = preload("res://Scenes/NetworkRequestItem.tscn")
|
||||
@onready var syntax_highlighter = preload("res://Resources/LuaSyntaxHighlighter.tres")
|
||||
|
||||
var network_requests: Array[NetworkRequest] = []
|
||||
var current_filter: NetworkRequest.RequestType = -1 # -1 means all
|
||||
var current_filter: int = -1 # -1 means all, otherwise NetworkRequest.RequestType
|
||||
var selected_request: NetworkRequest = null
|
||||
var request_items: Dictionary = {}
|
||||
|
||||
@@ -66,7 +67,7 @@ func apply_filter():
|
||||
for request in network_requests:
|
||||
var item = request_items.get(request.id)
|
||||
if item:
|
||||
var should_show = (current_filter == -1) or (request.type == current_filter)
|
||||
var should_show = (current_filter == -1) or (int(request.type) == current_filter)
|
||||
item.visible = should_show
|
||||
|
||||
func update_request_item(request: NetworkRequest):
|
||||
@@ -76,6 +77,9 @@ func update_request_item(request: NetworkRequest):
|
||||
|
||||
request_item.update_display()
|
||||
|
||||
if selected_request == request and details_panel.visible:
|
||||
update_details_panel(request)
|
||||
|
||||
apply_filter()
|
||||
update_status_bar()
|
||||
|
||||
@@ -84,11 +88,19 @@ func update_details_panel(request: NetworkRequest):
|
||||
update_headers_tab(request)
|
||||
update_preview_tab(request)
|
||||
update_response_tab(request)
|
||||
update_messages_tab(request)
|
||||
|
||||
if request.type == NetworkRequest.RequestType.SOCKET:
|
||||
messages_tab.visible = true
|
||||
details_tab_container.set_tab_title(3, "Messages (" + str(request.websocket_messages.size()) + ")")
|
||||
else:
|
||||
messages_tab.visible = false
|
||||
|
||||
func clear_details_panel():
|
||||
for child in headers_tab.get_children(): child.queue_free()
|
||||
for child in preview_tab.get_children(): child.queue_free()
|
||||
for child in response_tab.get_children(): child.queue_free()
|
||||
for child in messages_tab.get_children(): child.queue_free()
|
||||
|
||||
func create_collapsible_section(title: String, expanded: bool = false) -> VBoxContainer:
|
||||
var section = VBoxContainer.new()
|
||||
@@ -154,6 +166,17 @@ func update_headers_tab(request: NetworkRequest):
|
||||
add_header_row(general_content, "Request Method:", request.method)
|
||||
add_header_row(general_content, "Status Code:", str(request.status_code) + " " + request.status_text)
|
||||
|
||||
# WebSocket information
|
||||
if request.type == NetworkRequest.RequestType.SOCKET:
|
||||
var ws_section = create_collapsible_section("WebSocket Information", true)
|
||||
headers_tab.add_child(ws_section)
|
||||
|
||||
var ws_content = ws_section.get_child(1)
|
||||
add_header_row(ws_content, "WebSocket ID:", request.websocket_id)
|
||||
add_header_row(ws_content, "Event Type:", request.websocket_event_type)
|
||||
add_header_row(ws_content, "Connection Status:", request.connection_status)
|
||||
add_header_row(ws_content, "Total Messages:", str(request.websocket_messages.size()))
|
||||
|
||||
# Request Headers section
|
||||
if not request.request_headers.is_empty():
|
||||
var request_headers_section = create_collapsible_section("Request Headers", false)
|
||||
@@ -173,6 +196,32 @@ func update_headers_tab(request: NetworkRequest):
|
||||
add_header_row(response_headers_content, header_name + ":", str(request.response_headers[header_name]))
|
||||
|
||||
func update_preview_tab(request: NetworkRequest):
|
||||
if request.type == NetworkRequest.RequestType.SOCKET:
|
||||
var content_to_show = ""
|
||||
|
||||
match request.websocket_event_type:
|
||||
"connection":
|
||||
content_to_show = "WebSocket connection event\n"
|
||||
content_to_show += "URL: " + request.url + "\n"
|
||||
content_to_show += "Status: " + request.connection_status + "\n"
|
||||
if request.status_code > 0:
|
||||
content_to_show += "Status Code: " + str(request.status_code) + " " + request.status_text + "\n"
|
||||
content_to_show += "Messages exchanged: " + str(request.websocket_messages.size())
|
||||
"close", "error":
|
||||
content_to_show = "WebSocket " + request.websocket_event_type + " event\n"
|
||||
content_to_show += "Status Code: " + str(request.status_code) + "\n"
|
||||
content_to_show += "Reason: " + request.status_text
|
||||
|
||||
if not content_to_show.is_empty():
|
||||
var code_edit = CodeEditUtils.create_code_edit({
|
||||
"text": content_to_show,
|
||||
"editable": false,
|
||||
"show_line_numbers": false,
|
||||
"syntax_highlighter": null
|
||||
})
|
||||
preview_tab.add_child(code_edit)
|
||||
return
|
||||
|
||||
# For images, show the image in the preview tab
|
||||
if request.type == NetworkRequest.RequestType.IMG and request.status == NetworkRequest.RequestStatus.SUCCESS:
|
||||
var image = Image.new()
|
||||
@@ -240,6 +289,42 @@ func update_preview_tab(request: NetworkRequest):
|
||||
preview_tab.add_child(code_edit)
|
||||
|
||||
func update_response_tab(request: NetworkRequest):
|
||||
if request.type == NetworkRequest.RequestType.SOCKET:
|
||||
var content_to_show = ""
|
||||
|
||||
match request.websocket_event_type:
|
||||
"connection":
|
||||
content_to_show = "WebSocket Connection Details\n\n"
|
||||
content_to_show += "This is a WebSocket connection request.\n"
|
||||
content_to_show += "Connection Status: " + request.connection_status + "\n"
|
||||
content_to_show += "WebSocket ID: " + request.websocket_id + "\n"
|
||||
content_to_show += "Total Messages: " + str(request.websocket_messages.size()) + "\n"
|
||||
if request.status_code > 0:
|
||||
content_to_show += "Status Code: " + str(request.status_code) + " " + request.status_text + "\n"
|
||||
content_to_show += "\nNote: Individual messages can be viewed in the 'Messages' tab."
|
||||
"close", "error":
|
||||
content_to_show = "WebSocket " + request.websocket_event_type.capitalize() + " Event\n\n"
|
||||
content_to_show += "Status Code: " + str(request.status_code) + "\n"
|
||||
content_to_show += "Reason: " + request.status_text + "\n"
|
||||
content_to_show += "WebSocket ID: " + request.websocket_id + "\n"
|
||||
content_to_show += "Total Messages Exchanged: " + str(request.websocket_messages.size())
|
||||
|
||||
if not content_to_show.is_empty():
|
||||
var code_edit = CodeEditUtils.create_code_edit({
|
||||
"text": content_to_show,
|
||||
"editable": false,
|
||||
"show_line_numbers": false,
|
||||
"syntax_highlighter": null
|
||||
})
|
||||
response_tab.add_child(code_edit)
|
||||
else:
|
||||
var label = Label.new()
|
||||
label.text = "No WebSocket data to display"
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
response_tab.add_child(label)
|
||||
return
|
||||
|
||||
if request.type == NetworkRequest.RequestType.IMG:
|
||||
var label = Label.new()
|
||||
label.text = "This response contains image data. See the \"Preview\" tab to view the image."
|
||||
@@ -280,6 +365,165 @@ func update_response_tab(request: NetworkRequest):
|
||||
|
||||
response_tab.add_child(code_edit)
|
||||
|
||||
func update_messages_tab(request: NetworkRequest):
|
||||
if request.type != NetworkRequest.RequestType.SOCKET:
|
||||
return
|
||||
|
||||
if request.websocket_messages.is_empty():
|
||||
var label = Label.new()
|
||||
label.text = "No WebSocket messages yet"
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
messages_tab.add_child(label)
|
||||
return
|
||||
|
||||
var scroll_container = ScrollContainer.new()
|
||||
scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var messages_container = VBoxContainer.new()
|
||||
messages_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll_container.add_child(messages_container)
|
||||
|
||||
var header_container = VBoxContainer.new()
|
||||
header_container.add_theme_constant_override("separation", 8)
|
||||
|
||||
var search_container = HBoxContainer.new()
|
||||
search_container.add_theme_constant_override("separation", 8)
|
||||
|
||||
var search_input = LineEdit.new()
|
||||
search_input.placeholder_text = "Filter"
|
||||
search_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var focus_style = StyleBoxFlat.new()
|
||||
focus_style.content_margin_left = 16.0
|
||||
focus_style.content_margin_right = 8.0
|
||||
focus_style.bg_color = Color(0.168627, 0.168627, 0.168627, 1)
|
||||
focus_style.border_width_left = 1
|
||||
focus_style.border_width_top = 1
|
||||
focus_style.border_width_right = 1
|
||||
focus_style.border_width_bottom = 1
|
||||
focus_style.border_color = Color(0.247059, 0.466667, 0.807843, 1)
|
||||
focus_style.corner_radius_top_left = 15
|
||||
focus_style.corner_radius_top_right = 15
|
||||
focus_style.corner_radius_bottom_right = 15
|
||||
focus_style.corner_radius_bottom_left = 15
|
||||
search_input.add_theme_stylebox_override("focus", focus_style)
|
||||
|
||||
var normal_style = StyleBoxFlat.new()
|
||||
normal_style.content_margin_left = 16.0
|
||||
normal_style.content_margin_right = 8.0
|
||||
normal_style.bg_color = Color(0.168627, 0.168627, 0.168627, 1)
|
||||
normal_style.corner_radius_top_left = 15
|
||||
normal_style.corner_radius_top_right = 15
|
||||
normal_style.corner_radius_bottom_right = 15
|
||||
normal_style.corner_radius_bottom_left = 15
|
||||
search_input.add_theme_stylebox_override("normal", normal_style)
|
||||
|
||||
search_container.add_child(search_input)
|
||||
|
||||
header_container.add_child(search_container)
|
||||
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size.y = 8
|
||||
header_container.add_child(spacer)
|
||||
|
||||
messages_container.add_child(header_container)
|
||||
|
||||
var message_rows: Array[Control] = []
|
||||
var search_term = ""
|
||||
|
||||
var update_search = func():
|
||||
var filter_text = search_input.text.to_lower()
|
||||
|
||||
for row_index in range(message_rows.size()):
|
||||
var row = message_rows[row_index]
|
||||
var message = request.websocket_messages[row_index]
|
||||
var should_show = filter_text.is_empty() or message.content.to_lower().contains(filter_text)
|
||||
row.visible = should_show
|
||||
|
||||
search_input.text_changed.connect(func(_text): update_search.call())
|
||||
|
||||
for i in range(request.websocket_messages.size()):
|
||||
var message = request.websocket_messages[i]
|
||||
|
||||
var message_panel = PanelContainer.new()
|
||||
message_panel.custom_minimum_size.y = 32
|
||||
|
||||
var button = Button.new()
|
||||
button.flat = true
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
|
||||
button.anchors_preset = Control.PRESET_FULL_RECT
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var panel_style = StyleBoxFlat.new()
|
||||
if message.direction == "sent":
|
||||
panel_style.bg_color = Color(0.2, 0.3, 0.5, 0.3)
|
||||
else:
|
||||
panel_style.bg_color = Color(0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
panel_style.content_margin_left = 6
|
||||
panel_style.content_margin_right = 6
|
||||
panel_style.content_margin_top = 2
|
||||
panel_style.content_margin_bottom = 2
|
||||
message_panel.add_theme_stylebox_override("panel", panel_style)
|
||||
|
||||
var message_row = HBoxContainer.new()
|
||||
message_row.add_theme_constant_override("separation", 8)
|
||||
|
||||
var direction_label = Label.new()
|
||||
var direction_icon = "↑" if message.direction == "sent" else "↓"
|
||||
var direction_color = Color(0.7, 0.9, 1.0) if message.direction == "sent" else Color(1.0, 0.7, 0.7)
|
||||
|
||||
direction_label.text = direction_icon
|
||||
direction_label.add_theme_font_size_override("font_size", 16)
|
||||
direction_label.add_theme_color_override("font_color", direction_color)
|
||||
direction_label.custom_minimum_size.x = 16
|
||||
direction_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
message_row.add_child(direction_label)
|
||||
|
||||
var timestamp_label = Label.new()
|
||||
timestamp_label.text = message.get_formatted_time()
|
||||
timestamp_label.add_theme_font_size_override("font_size", 14)
|
||||
timestamp_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 1.0))
|
||||
timestamp_label.custom_minimum_size.x = 80
|
||||
message_row.add_child(timestamp_label)
|
||||
|
||||
var content_label = Label.new()
|
||||
var content_text = message.content
|
||||
if content_text.length() > 60:
|
||||
content_text = content_text.substr(0, 57) + "..."
|
||||
content_text = content_text.replace("\n", " ").replace("\r", " ")
|
||||
|
||||
content_label.text = content_text
|
||||
content_label.add_theme_font_size_override("font_size", 16)
|
||||
content_label.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0, 1.0))
|
||||
content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content_label.clip_contents = true
|
||||
content_label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
message_row.add_child(content_label)
|
||||
|
||||
var size_label = Label.new()
|
||||
size_label.text = str(message.size) + "b"
|
||||
size_label.add_theme_font_size_override("font_size", 14)
|
||||
size_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 1.0))
|
||||
size_label.custom_minimum_size.x = 40
|
||||
size_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
message_row.add_child(size_label)
|
||||
|
||||
button.pressed.connect(func(): DisplayServer.clipboard_set(message.content))
|
||||
|
||||
message_panel.add_child(message_row)
|
||||
message_panel.add_child(button)
|
||||
|
||||
messages_container.add_child(message_panel)
|
||||
message_rows.append(message_panel)
|
||||
|
||||
messages_tab.add_child(scroll_container)
|
||||
|
||||
func update_status_bar():
|
||||
var total_requests = network_requests.size()
|
||||
var total_size = 0
|
||||
|
||||
@@ -60,6 +60,25 @@ func _process(_delta):
|
||||
else:
|
||||
close_button.add_theme_stylebox_override("normal", CLOSE_BUTTON_NORMAL)
|
||||
|
||||
func _input(event):
|
||||
# make sure we are hovering over the tab
|
||||
if mouse_over_tab and event.is_action_pressed("CloseTabMouse"):
|
||||
var close_tween = create_tween()
|
||||
close_tween.set_ease(Tween.EASE_IN)
|
||||
close_tween.set_trans(Tween.TRANS_CUBIC)
|
||||
|
||||
animate_tab_close(close_tween)
|
||||
|
||||
await close_tween.finished
|
||||
tab_closed.emit()
|
||||
queue_free()
|
||||
|
||||
func animate_tab_close(tween: Tween) -> void:
|
||||
tween.parallel().tween_property(self, "custom_minimum_size:x", 0.0, 0.15)
|
||||
tween.parallel().tween_property(self, "size:x", 0.0, 0.15)
|
||||
tween.parallel().tween_property(button, "custom_minimum_size:x", 0.0, 0.15)
|
||||
tween.parallel().tween_property(button, "size:x", 0.0, 0.15)
|
||||
|
||||
func set_title(title: String) -> void:
|
||||
button.text = title
|
||||
button.set_meta("original_text", title)
|
||||
@@ -231,10 +250,7 @@ func _on_close_button_pressed() -> void:
|
||||
close_tween.set_ease(Tween.EASE_IN)
|
||||
close_tween.set_trans(Tween.TRANS_CUBIC)
|
||||
|
||||
close_tween.parallel().tween_property(self, "custom_minimum_size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(self, "size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(button, "custom_minimum_size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(button, "size:x", 0.0, 0.15)
|
||||
animate_tab_close(close_tween)
|
||||
|
||||
await close_tween.finished
|
||||
tab_closed.emit()
|
||||
|
||||
48
flumi/Scripts/ClientPool.gd
Normal file
48
flumi/Scripts/ClientPool.gd
Normal file
@@ -0,0 +1,48 @@
|
||||
class_name ClientPool
|
||||
extends RefCounted
|
||||
|
||||
static var gurt_clients: Dictionary = {}
|
||||
static var client_last_used: Dictionary = {}
|
||||
static var client_timeout_ms: int = 30000
|
||||
|
||||
static func _cleanup_idle_clients():
|
||||
var current_time = Time.get_ticks_msec()
|
||||
var to_remove = []
|
||||
|
||||
for domain in client_last_used:
|
||||
if current_time - client_last_used[domain] > client_timeout_ms:
|
||||
to_remove.append(domain)
|
||||
|
||||
for domain in to_remove:
|
||||
if gurt_clients.has(domain):
|
||||
gurt_clients[domain].disconnect()
|
||||
gurt_clients.erase(domain)
|
||||
client_last_used.erase(domain)
|
||||
|
||||
static func get_or_create_gurt_client(domain: String) -> GurtProtocolClient:
|
||||
_cleanup_idle_clients()
|
||||
|
||||
if gurt_clients.has(domain):
|
||||
client_last_used[domain] = Time.get_ticks_msec()
|
||||
return gurt_clients[domain]
|
||||
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT):
|
||||
return null
|
||||
|
||||
gurt_clients[domain] = client
|
||||
client_last_used[domain] = Time.get_ticks_msec()
|
||||
return client
|
||||
|
||||
static func extract_domain_from_url(gurt_url: String) -> String:
|
||||
var host_domain = gurt_url
|
||||
if host_domain.begins_with("gurt://"):
|
||||
host_domain = host_domain.right(-7)
|
||||
var slash_pos = host_domain.find("/")
|
||||
if slash_pos != -1:
|
||||
host_domain = host_domain.left(slash_pos)
|
||||
return host_domain
|
||||
1
flumi/Scripts/ClientPool.gd.uid
Normal file
1
flumi/Scripts/ClientPool.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dkortg18h0wa2
|
||||
@@ -1,5 +1,14 @@
|
||||
extends Node
|
||||
|
||||
const ClientPool = preload("res://Scripts/ClientPool.gd")
|
||||
|
||||
func _ready():
|
||||
var timer = Timer.new()
|
||||
timer.wait_time = 10.0
|
||||
timer.autostart = true
|
||||
timer.timeout.connect(ClientPool._cleanup_idle_clients)
|
||||
add_child(timer)
|
||||
|
||||
func fetch_image(url: String) -> ImageTexture:
|
||||
if url.is_empty():
|
||||
return null
|
||||
@@ -170,27 +179,17 @@ func fetch_gurt_resource(url: String, as_binary: bool = false):
|
||||
|
||||
var network_request = NetworkManager.start_request(gurt_url, "GET", false)
|
||||
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT):
|
||||
var host_domain = ClientPool.extract_domain_from_url(gurt_url)
|
||||
|
||||
var client = ClientPool.get_or_create_gurt_client(host_domain)
|
||||
if client == null:
|
||||
NetworkManager.fail_request(network_request.id, "Failed to create GURT client")
|
||||
return ""
|
||||
|
||||
var host_domain = gurt_url
|
||||
if host_domain.begins_with("gurt://"):
|
||||
host_domain = host_domain.substr(7)
|
||||
var slash_pos = host_domain.find("/")
|
||||
if slash_pos != -1:
|
||||
host_domain = host_domain.substr(0, slash_pos)
|
||||
return PackedByteArray() if as_binary else ""
|
||||
|
||||
var response = client.request(gurt_url, {
|
||||
"method": "GET",
|
||||
"headers": {"Host": host_domain}
|
||||
})
|
||||
client.disconnect()
|
||||
|
||||
if not response or not response.is_success:
|
||||
var error_msg = "Failed to load GURT resource"
|
||||
|
||||
@@ -23,6 +23,8 @@ class WebSocketWrapper:
|
||||
if connection_status:
|
||||
return
|
||||
|
||||
NetworkManager.call_deferred("start_websocket_connection", url, instance_id)
|
||||
|
||||
var error = websocket.connect_to_url(url)
|
||||
|
||||
if error == OK:
|
||||
@@ -42,8 +44,10 @@ class WebSocketWrapper:
|
||||
timer.call_deferred("start")
|
||||
else:
|
||||
trigger_event("error", {"message": "No scene available for WebSocket timer"})
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "error", 0, "No scene available")
|
||||
else:
|
||||
trigger_event("error", {"message": "Failed to connect to " + url + " (error: " + str(error) + ")"})
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "error", 0, "Connection failed: " + str(error))
|
||||
|
||||
func _poll_websocket():
|
||||
if not websocket:
|
||||
@@ -57,17 +61,25 @@ class WebSocketWrapper:
|
||||
if not connection_status:
|
||||
connection_status = true
|
||||
trigger_event("open", {})
|
||||
# Update NetworkManager with successful connection
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "open", 200, "Connected")
|
||||
|
||||
# Check for messages
|
||||
while websocket.get_available_packet_count() > 0:
|
||||
var packet = websocket.get_packet()
|
||||
var message = packet.get_string_from_utf8()
|
||||
trigger_event("message", {"data": message})
|
||||
# Track received message in NetworkManager
|
||||
NetworkManager.call_deferred("add_websocket_message", url, instance_id, "received", message)
|
||||
|
||||
WebSocketPeer.STATE_CLOSED:
|
||||
if connection_status:
|
||||
connection_status = false
|
||||
trigger_event("close", {})
|
||||
# Update NetworkManager with closed connection
|
||||
var close_code = websocket.get_close_code()
|
||||
var close_reason = websocket.get_close_reason()
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "closed", close_code, close_reason)
|
||||
|
||||
# Clean up timer
|
||||
if timer:
|
||||
@@ -82,25 +94,33 @@ class WebSocketWrapper:
|
||||
# Connection is closing
|
||||
if connection_status:
|
||||
connection_status = false
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "closing", 0, "Closing")
|
||||
|
||||
_:
|
||||
# Unknown state or connection failed
|
||||
if connection_status:
|
||||
connection_status = false
|
||||
trigger_event("close", {})
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "closed", 0, "Unexpected disconnection")
|
||||
elif not connection_status:
|
||||
# This might be a connection failure
|
||||
trigger_event("error", {"message": "Connection failed or was rejected by server"})
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "error", 0, "Connection failed or rejected")
|
||||
|
||||
func send_message(message: String):
|
||||
if connection_status and websocket:
|
||||
websocket.send_text(message)
|
||||
# Track sent message in NetworkManager
|
||||
NetworkManager.call_deferred("add_websocket_message", url, instance_id, "sent", message)
|
||||
|
||||
func close_connection():
|
||||
if websocket:
|
||||
websocket.close()
|
||||
connection_status = false
|
||||
|
||||
# Update NetworkManager with manual close
|
||||
NetworkManager.call_deferred("update_websocket_connection", instance_id, "closed", 1000, "Manually closed")
|
||||
|
||||
if timer:
|
||||
timer.queue_free()
|
||||
timer = null
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class_name Main
|
||||
extends Control
|
||||
|
||||
const ClientPool = preload("res://Scripts/ClientPool.gd")
|
||||
|
||||
@onready var website_container: Control = %WebsiteContainer
|
||||
@onready var tab_container: TabManager = $VBoxContainer/TabContainer
|
||||
@onready var search_bar: LineEdit = $VBoxContainer/HBoxContainer/LineEdit
|
||||
@@ -75,6 +77,7 @@ func _ready():
|
||||
call_deferred("update_navigation_buttons")
|
||||
call_deferred("_handle_startup_behavior")
|
||||
|
||||
|
||||
func _input(_event: InputEvent) -> void:
|
||||
if Input.is_action_just_pressed("DevTools"):
|
||||
_toggle_dev_tools()
|
||||
@@ -151,19 +154,16 @@ func fetch_gurt_content_async(gurt_url: String, tab: Tab, original_url: String,
|
||||
|
||||
func _perform_gurt_request_threaded(request_data: Dictionary) -> Dictionary:
|
||||
var gurt_url: String = request_data.gurt_url
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||
client.add_ca_certificate(ca_cert)
|
||||
|
||||
if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT):
|
||||
client.disconnect()
|
||||
var host_domain = ClientPool.extract_domain_from_url(gurt_url)
|
||||
|
||||
var client = ClientPool.get_or_create_gurt_client(host_domain)
|
||||
if client == null:
|
||||
return {"success": false, "error": "Failed to connect to GURT DNS server at " + GurtProtocol.DNS_SERVER_IP + ":" + str(GurtProtocol.DNS_SERVER_PORT)}
|
||||
|
||||
var response = client.request(gurt_url, {
|
||||
"method": "GET"
|
||||
})
|
||||
client.disconnect()
|
||||
|
||||
if not response or not response.is_success:
|
||||
var error_msg = "Connection failed"
|
||||
@@ -290,7 +290,7 @@ func _on_search_focus_exited() -> void:
|
||||
if not current_domain.is_empty():
|
||||
var display_text = current_domain
|
||||
if display_text.begins_with("gurt://"):
|
||||
display_text = display_text.substr(7)
|
||||
display_text = display_text.right(-7)
|
||||
elif display_text.begins_with("file://"):
|
||||
display_text = URLUtils.file_url_to_path(display_text)
|
||||
search_bar.text = display_text
|
||||
@@ -489,7 +489,7 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
||||
var base_url_for_scripts = current_domain
|
||||
var query_pos = base_url_for_scripts.find("?")
|
||||
if query_pos != -1:
|
||||
base_url_for_scripts = base_url_for_scripts.substr(0, query_pos)
|
||||
base_url_for_scripts = base_url_for_scripts.left(query_pos)
|
||||
await parser.process_external_scripts(lua_api, null, base_url_for_scripts)
|
||||
|
||||
var postprocess_element = parser.process_postprocess()
|
||||
@@ -838,7 +838,7 @@ func update_search_bar_from_current_domain() -> void:
|
||||
if not search_bar.has_focus() and not current_domain.is_empty():
|
||||
var display_text = current_domain
|
||||
if display_text.begins_with("gurt://"):
|
||||
display_text = display_text.substr(7)
|
||||
display_text = display_text.right(-7)
|
||||
elif display_text.begins_with("file://"):
|
||||
display_text = URLUtils.file_url_to_path(display_text)
|
||||
search_bar.text = display_text
|
||||
@@ -879,7 +879,7 @@ func add_to_history(url: String, tab: Tab, add_to_navigation: bool = true):
|
||||
|
||||
var clean_url = url
|
||||
if clean_url.begins_with("gurt://"):
|
||||
clean_url = clean_url.substr(7)
|
||||
clean_url = clean_url.right(-7)
|
||||
|
||||
BrowserHistory.add_entry(clean_url, title, icon_url)
|
||||
update_navigation_buttons()
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
[Setup]
|
||||
AppName=Flumi
|
||||
AppVersion=1.0.2
|
||||
AppVersion=1.0.3
|
||||
AppPublisher=Outpoot
|
||||
AppPublisherURL=https://github.com/gurted/flumi
|
||||
AppSupportURL=https://github.com/gurted/flumi/issues
|
||||
|
||||
@@ -11,7 +11,7 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="Flumi"
|
||||
config/version="1.0.1"
|
||||
config/version="1.0.3"
|
||||
run/main_scene="uid://bytm7bt2s4ak8"
|
||||
config/use_custom_user_dir=true
|
||||
config/features=PackedStringArray("4.4", "Forward Plus")
|
||||
@@ -85,3 +85,8 @@ ReloadPage={
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
CloseTabMouse={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":4,"position":Vector2(134, 20),"global_position":Vector2(143, 68),"factor":1.0,"button_index":3,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ crate-type = ["cdylib"]
|
||||
[dependencies]
|
||||
gurtlib = { path = "../library" }
|
||||
|
||||
godot = "0.1"
|
||||
godot = { version = "0.1", features = ["experimental-threads"] }
|
||||
|
||||
tokio = { version = "1.0", features = ["rt"] }
|
||||
url = "2.5"
|
||||
serde_json = "1.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
strip = true
|
||||
@@ -33,7 +33,7 @@ while [[ $# -gt 0 ]]; do
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -t, --target TARGET Build target (debug|release) [default: release]"
|
||||
echo " -p, --platform PLATFORM Target platform (windows|linux|macos|current)"
|
||||
echo " -p, --platform PLATFORM Target platform (windows|linux|macos|macos-intel|current)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
exit 0
|
||||
@@ -82,6 +82,10 @@ case $PLATFORM in
|
||||
LIB_NAME="libgurt_godot.so"
|
||||
;;
|
||||
macos)
|
||||
RUST_TARGET="aarch64-apple-darwin"
|
||||
LIB_NAME="libgurt_godot.dylib"
|
||||
;;
|
||||
macos-intel)
|
||||
RUST_TARGET="x86_64-apple-darwin"
|
||||
LIB_NAME="libgurt_godot.dylib"
|
||||
;;
|
||||
|
||||
48
search-engine/Dockerfile
Normal file
48
search-engine/Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
||||
FROM rustlang/rust:nightly-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache \
|
||||
musl-dev \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
pkgconfig \
|
||||
postgresql-dev
|
||||
|
||||
ENV OPENSSL_STATIC=1
|
||||
ENV OPENSSL_DIR=/usr
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY protocol/library ../protocol/library
|
||||
COPY search-engine/Cargo.toml ./
|
||||
COPY search-engine/src ./src
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
postgresql-client \
|
||||
wget
|
||||
|
||||
RUN addgroup -g 1001 -S appgroup && \
|
||||
adduser -u 1001 -S appuser -G appgroup
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/target/release/gurted-search-engine /app/gurted-search-engine
|
||||
COPY search-engine/frontend ./frontend
|
||||
|
||||
RUN mkdir -p /app/config /app/data /app/search_indexes /app/certs && \
|
||||
chown -R appuser:appgroup /app
|
||||
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8081
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
|
||||
|
||||
# Default command - runs the server subcommand
|
||||
CMD ["./gurted-search-engine", "--config", "/app/config/config.toml", "server"]
|
||||
15
search-engine/build.sh
Normal file
15
search-engine/build.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Building Gurted Search Engine Server..."
|
||||
|
||||
echo "Stopping existing container..."
|
||||
docker compose down
|
||||
|
||||
echo "Building Docker image..."
|
||||
docker compose build --no-cache --parallel
|
||||
|
||||
echo "Starting services..."
|
||||
docker compose up -d
|
||||
|
||||
echo "Container logs (press Ctrl+C to stop following):"
|
||||
docker compose logs -f search-engine
|
||||
14
search-engine/docker-compose.yml
Normal file
14
search-engine/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
search-engine:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: search-engine/Dockerfile
|
||||
container_name: gurted-search-engine
|
||||
network_mode: host
|
||||
environment:
|
||||
RUST_LOG: info
|
||||
volumes:
|
||||
- ./config.toml:/app/config/config.toml:ro
|
||||
- ./search_indexes:/app/search_indexes
|
||||
- ./certs:/app/certs:ro
|
||||
restart: unless-stopped
|
||||
@@ -158,7 +158,14 @@ impl DomainCrawler {
|
||||
}
|
||||
|
||||
fn parse_clanker_txt(&self, content: &str, base_url: &str) -> Result<Vec<String>> {
|
||||
let user_agent = &self.config.search.crawler_user_agent;
|
||||
Self::parse_clanker_rules(
|
||||
content,
|
||||
base_url,
|
||||
&self.config.search.crawler_user_agent,
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_clanker_rules(content: &str, base_url: &str, user_agent: &str) -> Result<Vec<String>> {
|
||||
let mut disallow_all = false;
|
||||
let mut user_agent_matches = false;
|
||||
let mut allowed_urls = Vec::new();
|
||||
@@ -169,26 +176,31 @@ impl DomainCrawler {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(user_agent_value) = line.to_lowercase().strip_prefix("user-agent:") {
|
||||
let current_user_agent = user_agent_value.trim().to_string();
|
||||
user_agent_matches = current_user_agent == "*" || current_user_agent.eq_ignore_ascii_case(user_agent);
|
||||
let (directive, value) = match line.split_once(':') {
|
||||
Some((directive, value)) => (directive.trim().to_lowercase(), value.trim()),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if directive == "user-agent" {
|
||||
user_agent_matches =
|
||||
value == "*" || value.eq_ignore_ascii_case(user_agent);
|
||||
continue;
|
||||
}
|
||||
|
||||
if user_agent_matches {
|
||||
if let Some(path_value) = line.to_lowercase().strip_prefix("disallow:") {
|
||||
let path = path_value.trim();
|
||||
if path == "/" {
|
||||
disallow_all = true;
|
||||
break;
|
||||
}
|
||||
} else if let Some(path_value) = line.to_lowercase().strip_prefix("allow:") {
|
||||
let path = path_value.trim();
|
||||
if !path.is_empty() {
|
||||
let full_url = Self::normalize_url(format!("{}{}", base_url, path));
|
||||
debug!("Added allowed URL from clanker.txt: {}", full_url);
|
||||
allowed_urls.push(full_url);
|
||||
}
|
||||
if !user_agent_matches {
|
||||
continue;
|
||||
}
|
||||
|
||||
if directive == "disallow" {
|
||||
if value == "/" {
|
||||
disallow_all = true;
|
||||
break;
|
||||
}
|
||||
} else if directive == "allow" {
|
||||
if !value.is_empty() {
|
||||
let full_url = Self::normalize_url(format!("{}{}", base_url, value));
|
||||
debug!("Added allowed URL from clanker.txt: {}", full_url);
|
||||
allowed_urls.push(full_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -719,4 +731,41 @@ impl CrawlStats {
|
||||
duration_seconds: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DomainCrawler;
|
||||
|
||||
#[test]
|
||||
fn parse_clanker_rules_preserves_case_in_allowed_urls() {
|
||||
let content = "User-agent: TestBot\nAllow: /getpage?l=Fri,12Sep2025000605_ZzesV.txt\n";
|
||||
let result = DomainCrawler::parse_clanker_rules(content, "gurt://wi.ki", "TestBot")
|
||||
.expect("expected allow list");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec!["gurt://wi.ki/getpage?l=Fri,12Sep2025000605_ZzesV.txt".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_clanker_rules_handles_case_insensitive_directives() {
|
||||
let content = "user-Agent: AnotherBot\nAlLoW: /MiXeD/Path.HTML\n";
|
||||
let result = DomainCrawler::parse_clanker_rules(content, "gurt://example", "AnotherBot")
|
||||
.expect("expected allow list");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec!["gurt://example/MiXeD/Path.HTML".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_clanker_rules_respects_disallow_all() {
|
||||
let content = "User-agent: Bot\nDisallow: /\n";
|
||||
let result = DomainCrawler::parse_clanker_rules(content, "gurt://example", "Bot");
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
1608
site/package-lock.json
generated
1608
site/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/instrument-serif": "^5.2.7",
|
||||
"@sveltejs/adapter-cloudflare": "^7.2.3",
|
||||
"lucide-svelte": "^0.542.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
} from 'lucide-svelte';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
|
||||
const version = '1.0.2';
|
||||
const version = '1.0.3';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 MiB After Width: | Height: | Size: 18 MiB |
@@ -1,4 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-cloudflare';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
||||
Reference in New Issue
Block a user