└── addons ├── .gitkeep └── HTTPManager ├── plugin.cfg ├── classes ├── decoders │ ├── application_json.gd │ ├── image_texture.gd │ ├── text.gd │ ├── application_octet-stream.gd │ └── image.gd ├── HTTPManagerCookie.gd ├── HTTPManagerCacher.gd ├── HTTPPipe.gd ├── HTTPManagerJob.gd └── HTTPManager.gd ├── http-manager-plugin.gd ├── progress ├── progress.gd └── progress.tscn ├── LICENSE └── example ├── HTTPManager-Example.gd └── HTTPManager-Example.tscn /addons/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addons/HTTPManager/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Godot 4 HTTP Manager" 4 | description="Manages Downloads and request in a pipeline with multiple requests" 5 | author="Klaas Janneck" 6 | version="0.3" 7 | script="http-manager-plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/decoders/application_json.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/HTTPManager/classes/decoders/text.gd" 2 | 3 | 4 | func fetch(): 5 | var charset = response_charset 6 | if forced_charset != "": 7 | charset = forced_charset 8 | var text = as_text( charset ) 9 | return JSON.parse_string( text ) 10 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/decoders/image_texture.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/HTTPManager/classes/decoders/image.gd" 2 | 3 | 4 | func fetch(): 5 | return as_texture() 6 | 7 | 8 | func as_texture(): 9 | var img = as_image( response_mime ) 10 | if img: 11 | return ImageTexture.create_from_image(img) 12 | 13 | return null 14 | 15 | -------------------------------------------------------------------------------- /addons/HTTPManager/http-manager-plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | add_custom_type("HTTPManager","Node",load("res://addons/HTTPManager/classes/HTTPManager.gd"), get_editor_interface().get_base_control().get_theme_icon("HTTPRequest","EditorIcons")) 7 | 8 | 9 | func _exit_tree(): 10 | remove_custom_type("HTTPManager") 11 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/decoders/text.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/HTTPManager/classes/decoders/application_octet-stream.gd" 2 | 3 | 4 | func fetch(): 5 | var charset = response_charset 6 | if forced_charset != "": 7 | charset = forced_charset 8 | return as_text( charset ) 9 | 10 | 11 | func as_text( charset ): 12 | match charset: 13 | "utf-8": 14 | return response_body.get_string_from_utf8() 15 | "utf-16": 16 | return response_body.get_string_from_utf16() 17 | "utf-32": 18 | return response_body.get_string_from_utf32() 19 | _: 20 | return response_body.get_string_from_ascii() 21 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/decoders/application_octet-stream.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | var request_url:String 4 | var request_query:String 5 | var request_headers:Dictionary 6 | var request_get:Dictionary 7 | var request_post:Dictionary 8 | var request_files:Array[Dictionary] 9 | 10 | var result:int 11 | var from_cache:bool = false 12 | 13 | var response_code:int 14 | var response_headers:Dictionary 15 | var response_body:PackedByteArray 16 | var response_mime:Array 17 | var response_charset:String 18 | var forced_mime:Array[String] 19 | var forced_charset:String 20 | 21 | func fetch(): 22 | return response_body 23 | -------------------------------------------------------------------------------- /addons/HTTPManager/progress/progress.gd: -------------------------------------------------------------------------------- 1 | extends Window 2 | 3 | 4 | static func format_bytes( bytes:int ): 5 | var unit:String = "b" 6 | if bytes > 1048576: 7 | return str(snapped(float(bytes) / 1048576, 0.01)) + " mb" 8 | elif bytes > 1024: 9 | return str(snapped(float(bytes) / 1024, 1)) + " kb" 10 | else: 11 | return str(bytes) + " b" 12 | 13 | 14 | func httpmanager_progress_update( total_files:int, current_files:int, total_bytes:int, current_bytes:int ): 15 | %files.text = str(total_files)+" / "+str(current_files) 16 | %progress_bytes.value = round((1.0 - current_files/(0.00001+total_files)) * 100) 17 | %bytes.text = format_bytes(total_bytes)+" / "+format_bytes(current_bytes) 18 | %progress_jobs.value = round(current_bytes/(0.00001+total_bytes) * 100) 19 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/decoders/image.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/HTTPManager/classes/decoders/application_octet-stream.gd" 2 | 3 | 4 | func fetch(): 5 | var mime = response_mime 6 | if forced_mime.size() == 3: 7 | mime = forced_mime 8 | return as_image( mime ) 9 | 10 | 11 | func as_image( mime ): 12 | var img:Image = Image.new() 13 | match mime[2].to_lower(): 14 | "png": 15 | OK 16 | if img.load_png_from_buffer( response_body ) == OK: 17 | return img 18 | "jpg", "jpeg": 19 | if img.load_jpg_from_buffer( response_body ) == OK: 20 | return img 21 | "tga": 22 | if img.load_tga_from_buffer( response_body ) == OK: 23 | return img 24 | "webp": 25 | if img.load_webp_from_buffer( response_body ) == OK: 26 | return img 27 | "bmp": 28 | if img.load_bmp_from_buffer( response_body ) == OK: 29 | return img 30 | 31 | return null 32 | 33 | -------------------------------------------------------------------------------- /addons/HTTPManager/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 D2klaas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/HTTPManager/example/HTTPManager-Example.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | 4 | func _on_button_pressed(): 5 | $HTTPManager.connect("completed",func(): print("all completed")) 6 | 7 | $HTTPManager.job("https://cdn2.thecatapi.com/images/ld.jpg").mime("image/*").on_success(func(response): print("all completed")).fetch() 8 | 9 | $HTTPManager.job( 10 | "https://de.wiktionary.org/wiki/Hilfe:Sonderzeichen/Tabelle" 11 | ).charset( 12 | "utf-8" 13 | ).on_success_set( 14 | $TextEdit, "text" 15 | ).fetch() 16 | 17 | $HTTPManager.job( 18 | "https://support.oneskyapp.com/hc/en-us/article_attachments/202761727/example_2.json" 19 | ).on_success( 20 | func(response): print("This JSON is what i got:"); print(response.fetch()) 21 | ).fetch() 22 | 23 | $HTTPManager.job( 24 | "https://godotengine.org/storage/blog/covers/maintenance-release-godot-4-0-2.jpg" 25 | ).on_success_set( 26 | $TextureRect, "texture" 27 | ).mime("image/texture").fetch() 28 | 29 | $HTTPManager.job( 30 | "https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg" 31 | ).on_success_set( 32 | $TextureRect2, "texture" 33 | ).mime("image/texture").cache(false).on_success( 34 | func( _response ): print("download finished, not from cache") 35 | ).fetch() 36 | 37 | $HTTPManager.job( 38 | "https://this.url.is.a.failure/" 39 | ).on_success( 40 | func( _response ): print("realy?") 41 | ).on_failure( 42 | func( _response ): print("i told this wont work!") 43 | ).fetch() 44 | 45 | #--------------- use your own server here 46 | var server = "https://www.foo.bar" 47 | $HTTPManager.job( 48 | server 49 | ).add_post_file( 50 | "uploadfile_1", "res://icon.svg" 51 | ).add_post_buffer( 52 | "uploadfile_2", PackedByteArray([1,1,1]), "auto", "filename.ext" 53 | ).on_success( 54 | func( _response ): print("uploaded") 55 | ).on_failure( 56 | func( _response ): print("something bad happend") 57 | ).fetch() 58 | 59 | # ------ A download example 60 | # 61 | # $HTTPManager.job( 62 | # "https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg" 63 | # ).download("C:/Users/Klaas/Downloads/video.mp4") 64 | 65 | 66 | -------------------------------------------------------------------------------- /addons/HTTPManager/progress/progress.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bv5iovcvbaon5"] 2 | 3 | [ext_resource type="Script" path="res://addons/HTTPManager/progress/progress.gd" id="1_cngh1"] 4 | 5 | [node name="Progress" type="Window"] 6 | initial_position = 2 7 | size = Vector2i(500, 150) 8 | exclusive = true 9 | unresizable = true 10 | borderless = true 11 | always_on_top = true 12 | popup_window = true 13 | extend_to_title = true 14 | min_size = Vector2i(500, 100) 15 | script = ExtResource("1_cngh1") 16 | 17 | [node name="Control" type="Control" parent="."] 18 | layout_mode = 3 19 | anchors_preset = 15 20 | anchor_right = 1.0 21 | anchor_bottom = 1.0 22 | grow_horizontal = 2 23 | grow_vertical = 2 24 | size_flags_horizontal = 3 25 | size_flags_vertical = 3 26 | 27 | [node name="Panel" type="Panel" parent="Control"] 28 | layout_mode = 1 29 | anchors_preset = 15 30 | anchor_right = 1.0 31 | anchor_bottom = 1.0 32 | grow_horizontal = 2 33 | grow_vertical = 2 34 | 35 | [node name="MarginContainer" type="MarginContainer" parent="Control/Panel"] 36 | layout_mode = 1 37 | anchors_preset = 15 38 | anchor_right = 1.0 39 | anchor_bottom = 1.0 40 | grow_horizontal = 2 41 | grow_vertical = 2 42 | theme_override_constants/margin_left = 10 43 | theme_override_constants/margin_top = 10 44 | theme_override_constants/margin_right = 10 45 | theme_override_constants/margin_bottom = 10 46 | 47 | [node name="VBoxContainer" type="VBoxContainer" parent="Control/Panel/MarginContainer"] 48 | layout_mode = 2 49 | 50 | [node name="files" type="Label" parent="Control/Panel/MarginContainer/VBoxContainer"] 51 | unique_name_in_owner = true 52 | layout_mode = 2 53 | text = "0" 54 | horizontal_alignment = 1 55 | 56 | [node name="progress_bytes" type="ProgressBar" parent="Control/Panel/MarginContainer/VBoxContainer"] 57 | unique_name_in_owner = true 58 | layout_mode = 2 59 | step = 1.0 60 | value = 100.0 61 | 62 | [node name="bytes" type="Label" parent="Control/Panel/MarginContainer/VBoxContainer"] 63 | unique_name_in_owner = true 64 | layout_mode = 2 65 | text = "0" 66 | horizontal_alignment = 1 67 | 68 | [node name="progress_jobs" type="ProgressBar" parent="Control/Panel/MarginContainer/VBoxContainer"] 69 | unique_name_in_owner = true 70 | layout_mode = 2 71 | step = 1.0 72 | value = 100.0 73 | -------------------------------------------------------------------------------- /addons/HTTPManager/example/HTTPManager-Example.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://dq0l3g1vrbr0w"] 2 | 3 | [ext_resource type="Script" path="res://addons/HTTPManager/example/HTTPManager-Example.gd" id="1_2hlte"] 4 | [ext_resource type="Script" path="res://addons/HTTPManager/classes/HTTPManager.gd" id="2_o1fp1"] 5 | [ext_resource type="Texture2D" uid="uid://bead4mo500aiv" path="res://icon.svg" id="3_ryo4l"] 6 | 7 | [node name="HTTPManager-Example" type="Control"] 8 | layout_mode = 3 9 | anchors_preset = 15 10 | anchor_right = 1.0 11 | anchor_bottom = 1.0 12 | grow_horizontal = 2 13 | grow_vertical = 2 14 | script = ExtResource("1_2hlte") 15 | 16 | [node name="HTTPManager" type="Node" parent="."] 17 | script = ExtResource("2_o1fp1") 18 | use_cache = true 19 | display_progress = true 20 | accept_cookies = true 21 | print_debug = true 22 | 23 | [node name="Button" type="Button" parent="."] 24 | layout_mode = 0 25 | offset_left = 48.0 26 | offset_top = 53.0 27 | offset_right = 146.0 28 | offset_bottom = 131.0 29 | text = "go" 30 | 31 | [node name="Label" type="Label" parent="."] 32 | layout_mode = 0 33 | offset_left = 41.0 34 | offset_top = 240.0 35 | offset_right = 158.0 36 | offset_bottom = 266.0 37 | text = "Load a texture " 38 | 39 | [node name="TextureRect" type="TextureRect" parent="."] 40 | layout_mode = 0 41 | offset_left = 44.0 42 | offset_top = 279.0 43 | offset_right = 344.0 44 | offset_bottom = 579.0 45 | texture = ExtResource("3_ryo4l") 46 | expand_mode = 2 47 | 48 | [node name="Label3" type="Label" parent="."] 49 | layout_mode = 0 50 | offset_left = 414.0 51 | offset_top = 240.0 52 | offset_right = 604.0 53 | offset_bottom = 266.0 54 | text = "Load a texture, no cache" 55 | 56 | [node name="TextureRect2" type="TextureRect" parent="."] 57 | layout_mode = 0 58 | offset_left = 417.0 59 | offset_top = 279.0 60 | offset_right = 717.0 61 | offset_bottom = 579.0 62 | texture = ExtResource("3_ryo4l") 63 | expand_mode = 2 64 | 65 | [node name="Label2" type="Label" parent="."] 66 | layout_mode = 0 67 | offset_left = 802.0 68 | offset_top = 240.0 69 | offset_right = 919.0 70 | offset_bottom = 266.0 71 | text = "Load a text" 72 | 73 | [node name="TextEdit" type="TextEdit" parent="."] 74 | layout_mode = 0 75 | offset_left = 801.0 76 | offset_top = 278.0 77 | offset_right = 1101.0 78 | offset_bottom = 578.0 79 | 80 | [connection signal="pressed" from="Button" to="." method="_on_button_pressed"] 81 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/HTTPManagerCookie.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name HTTPManagerCookie 3 | 4 | var manager:HTTPManager 5 | 6 | var name:String = "--unnamed--" 7 | var request_url:String 8 | var value:String 9 | var expires:int = -1 10 | var max_age:int = -1 11 | var path:String 12 | var secure:bool 13 | var domain:String 14 | var http_only:bool #this can be irgnored completely 15 | var same_site:String = "lax" #dont know how to use this by now ... maybe support is added later 16 | 17 | var _is_host_set:bool 18 | var _is_secure_set:bool 19 | 20 | func parse( _value:String, request_url:String ): 21 | var regex = RegEx.new() 22 | regex.compile("http[s]?:\\/\\/([^\\/]+)") 23 | var res = regex.search(request_url) 24 | if not res: 25 | return false 26 | domain = res.strings[1] 27 | 28 | var parts = _value.split(";") 29 | for part in parts: 30 | part = part.strip_edges() 31 | var sp = part.split("=") 32 | if sp.size() == 2: 33 | set_value(sp[0],sp[1]) 34 | else: 35 | set_value(sp[0]) 36 | 37 | if _is_host_set: 38 | if not secure: 39 | return false 40 | 41 | if not domain == "": 42 | return false 43 | 44 | if not path == "/": 45 | return false 46 | 47 | if not request_url.to_lower().begins_with("https"): 48 | return false 49 | 50 | if _is_secure_set: 51 | if not secure: 52 | return false 53 | 54 | if not request_url.to_lower().begins_with("https"): 55 | return false 56 | 57 | manager.d("Cookie set "+name+"="+str(value)+" for "+domain) 58 | if not manager._cookies.has(domain): 59 | manager._cookies[domain] = {} 60 | manager._cookies[domain][name] = self 61 | 62 | 63 | func set_value( _name:String, _value=null ): 64 | _value = str(_value) 65 | match _name.to_lower(): 66 | "max-age": 67 | max_age = _value.to_int() 68 | expires = Time.get_unix_time_from_system() + max_age 69 | "path": 70 | path = _value 71 | "secure": 72 | secure = true 73 | "domain": 74 | domain = _value 75 | "httponly": 76 | http_only = true 77 | "expires": 78 | #this is wrong and must be corrected 79 | #expires = Time.get_unix_time_from_datetime_string(_value) 80 | #not implemented yet 81 | pass 82 | "samesite": 83 | same_site = _value 84 | _: 85 | if _name.substr(0,7) == "__Host-": 86 | _is_host_set = true 87 | _name = _name.substr(7) 88 | if _name.substr(0,9) == "__Secure-": 89 | _is_secure_set = true 90 | _name = _name.substr(9) 91 | name = _name 92 | value = _value 93 | 94 | 95 | func apply_if_valid( request_protocol, request_domain, request_path): 96 | #check path 97 | if not request_path.begins_with( path ): 98 | return "" 99 | 100 | #check secure protocol 101 | if secure and request_protocol != "https": 102 | return "" 103 | 104 | return name+"="+value+";" 105 | 106 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/HTTPManagerCacher.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | var manager:HTTPManager 4 | 5 | func cache_request( job:HTTPManagerJob ): 6 | #look in the cache 7 | var url = job.get_url() 8 | var filepath = get_cache_name(url) 9 | if FileAccess.file_exists ( filepath ): 10 | var file = FileAccess.open( filepath, FileAccess.READ ) 11 | var cache_infos = file.get_pascal_string() 12 | var headers = extract_cache_info(cache_infos) 13 | 14 | #add cache headers 15 | for i in headers: 16 | match i: 17 | "if-none-match","etag": 18 | job.add_header( i, headers[i] ) 19 | 20 | 21 | func cache_response( job:HTTPManagerJob, result:int, response_code:int, headers:PackedStringArray, body:PackedByteArray ): 22 | var cache:Dictionary 23 | cache.headers = headers 24 | var filepath:String = get_cache_name( job.get_url() ) 25 | if response_code == 304: 26 | #not modified 27 | var file = FileAccess.open( filepath, FileAccess.READ ) 28 | if file: 29 | var cache_infos = file.get_pascal_string() 30 | var cache_headers = extract_cache_info(cache_infos) 31 | if cache_headers.has("content-type"): 32 | cache.headers.append("content-type: "+cache_headers["content-type"]) 33 | cache.body = file.get_buffer(file.get_length() - file.get_position()) 34 | cache.from_cache = true 35 | return cache 36 | else: 37 | manager.e("got code 304 but cachefile not found") 38 | else: 39 | cache.from_cache = false 40 | cache.body = body 41 | 42 | #parse headers for extra infos for caching 43 | var cache_headers:Dictionary 44 | var cachable:bool = false 45 | for header in headers: 46 | var h = job._string_to_header( header ) 47 | match h[0].to_lower(): 48 | "etag": 49 | cache_headers["if-none-match"] = h[1] 50 | cachable = true 51 | "last-modified": 52 | cache_headers["if-modified-since"] = h[1] 53 | cachable = true 54 | "content-type": 55 | cache_headers["content-type"] = h[1] 56 | 57 | if not cachable: 58 | return cache 59 | 60 | #save cache with extra headers 61 | DirAccess.make_dir_recursive_absolute(filepath.get_base_dir()) 62 | var file = FileAccess.open( filepath, FileAccess.WRITE ) 63 | if file: 64 | file.store_pascal_string( encode_cache_info(cache_headers)) 65 | file.store_buffer( body ) 66 | else: 67 | manager.e("cachefile could not be written in \""+filepath+"\"") 68 | 69 | return cache 70 | 71 | 72 | func get_cache_name( url:String ): 73 | var pi = HTTPManager.parse_url(url) 74 | var cache_dir = pi.host.replace(":","_") 75 | var cache_name = pi.query.uri_encode() 76 | var filename = manager.cache_directory+"/"+cache_dir+"/"+cache_name 77 | 78 | return filename 79 | 80 | 81 | func extract_cache_info( str:String ) -> Dictionary: 82 | var result = {} 83 | var hs = str.split("\r\n",false) 84 | for ns in hs: 85 | var n = ns.split(":",true,1) 86 | result[n[0]] = n[1] 87 | 88 | return result 89 | 90 | 91 | func encode_cache_info( cache_info:Dictionary ) -> String: 92 | var result = "" 93 | for i in cache_info: 94 | result += i+": "+cache_info[i]+"\r\n" 95 | 96 | return result 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/HTTPPipe.gd: -------------------------------------------------------------------------------- 1 | extends HTTPRequest 2 | 3 | var manager 4 | var is_busy:bool = false 5 | var job:HTTPManagerJob 6 | 7 | 8 | 9 | func _ready(): 10 | connect("request_completed",self._on_pipe_request_completed) 11 | 12 | 13 | func reset(): 14 | cancel_request() 15 | job = null 16 | is_busy = false 17 | 18 | 19 | func dispatch( _job:HTTPManagerJob ): 20 | is_busy = true 21 | job = _job 22 | 23 | if manager.use_cache and job.use_cache: 24 | manager.cacher.cache_request( job ) 25 | var url = job.get_url() 26 | 27 | manager.d("starting request "+url) 28 | 29 | 30 | 31 | var method:int = HTTPClient.METHOD_GET 32 | if job.request_method == -1: 33 | if job.request_post.size() > 0 or job.request_files.size() > 0 or job.request_post.size() > 0: 34 | method = HTTPClient.METHOD_POST 35 | else: 36 | method = HTTPClient.METHOD_GET 37 | else: 38 | method = job.request_method 39 | 40 | var body:PackedByteArray 41 | if job.request_files.size() > 0: 42 | 43 | job.add_header("Content-Type", 'multipart/form-data;boundary="' + manager.content_boundary + '"') 44 | 45 | for file in job.request_files: 46 | var file_content:PackedByteArray 47 | if file.has("path"): 48 | if FileAccess.file_exists( file.path ): 49 | file_content = FileAccess.get_file_as_bytes( file.path ) 50 | if file_content.size() == 0: 51 | manager.e("POST file size zero") 52 | else: 53 | manager.e("POST file not found \""+file.path+"\"") 54 | 55 | if file.has("buffer"): 56 | file_content = file.buffer 57 | if file_content.size() == 0: 58 | manager.e("POST buffer size zero") 59 | 60 | body.append_array(("--"+manager.content_boundary).to_utf8_buffer()) 61 | body.append_array("\r\n".to_utf8_buffer()) 62 | body.append_array(('Content-Disposition: form-data; name="' + file.name +'"; filename="'+file.basename+'"').to_utf8_buffer()) 63 | body.append_array("\r\n".to_utf8_buffer()) 64 | body.append_array(("Content-Type: " + file.mime).to_utf8_buffer()) 65 | body.append_array("\r\n".to_utf8_buffer()) 66 | body.append_array("\r\n".to_utf8_buffer()) 67 | body.append_array(file_content) 68 | body.append_array("\r\n".to_utf8_buffer()) 69 | 70 | for name in job.request_post: 71 | body.append_array(("--"+manager.content_boundary).to_utf8_buffer()) 72 | body.append_array("\r\n".to_utf8_buffer()) 73 | body.append_array(('Content-Disposition: form-data; name="' + name +'"').to_utf8_buffer()) 74 | body.append_array("\r\n".to_utf8_buffer()) 75 | body.append_array("\r\n".to_utf8_buffer()) 76 | body.append_array(job.request_post[name].to_utf8_buffer()) 77 | body.append_array("\r\n".to_utf8_buffer()) 78 | 79 | body.append_array(("--"+manager.content_boundary+"--").to_utf8_buffer()) 80 | body.append_array("\r\n".to_utf8_buffer()) 81 | 82 | elif job.request_post.size() > 0: 83 | var query = manager.query_string_from_dict(job.request_post) 84 | job.add_header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") 85 | job.add_header("Content-Length", str(query.length()) ) 86 | body = query.to_utf8_buffer() 87 | 88 | var headers:PackedStringArray = PackedStringArray() 89 | if job.request_headers.size() > 0: 90 | for h in job.request_headers: 91 | headers.append( h + ": " + job.request_headers[h] ) 92 | 93 | #cookie headers 94 | if manager.accept_cookies: 95 | var cookie_data:String 96 | var regex = RegEx.new() 97 | #this regex could be better 98 | regex.compile("(http[s]?):\\/\\/([^\\/]+)[:\\d*]?(\\/.*)") 99 | var res = regex.search(url) 100 | if res: 101 | var request_protocol = res.strings[1] 102 | var request_domain = res.strings[2] 103 | var request_path = res.strings[3] 104 | 105 | #check cookie domain 106 | for domain in manager._cookies: 107 | if request_domain.ends_with(domain): 108 | #expire cookies befor beign used 109 | for cookie_name in manager._cookies[domain]: 110 | var cookie = manager._cookies[domain][cookie_name] 111 | if cookie.expires > -1 and cookie.expires < Time.get_unix_time_from_system(): 112 | manager._cookies[domain].erase(cookie_name) 113 | 114 | #check path 115 | for cookie_name in manager._cookies[domain]: 116 | var cookie = manager._cookies[domain][cookie_name] 117 | cookie_data += cookie.apply_if_valid(request_protocol,request_domain,request_path) 118 | 119 | if cookie_data.length() > 0: 120 | headers.append("Cookie: "+cookie_data) 121 | 122 | if job.use_proxy and manager.use_proxy: 123 | if manager.http_proxy and manager.http_port: 124 | set_http_proxy(manager.http_proxy, manager.http_port) 125 | if manager.https_proxy and manager.https_port: 126 | set_https_proxy(manager.https_proxy, manager.https_port) 127 | 128 | if job.unsafe_ssl: 129 | set_tls_options ( TLSOptions.client_unsafe() ) 130 | else: 131 | set_tls_options ( TLSOptions.client() ) 132 | job.error = request_raw( url, headers, method, body ) 133 | 134 | if job.error != OK: 135 | _on_pipe_request_completed( HTTPRequest.RESULT_REQUEST_FAILED, 0, [], []) 136 | 137 | 138 | func _on_pipe_request_completed( result:int, response_code:int, headers:PackedStringArray, body:PackedByteArray ): 139 | #analyse result 140 | if result != RESULT_SUCCESS: 141 | if manager._retry_on_result.find(result) != -1: 142 | manager.d("request failed with result "+manager._result_error_string[result]) 143 | if retry_job(): 144 | return 145 | else: 146 | manager.d("job failed because result is "+manager._result_error_string[result]+" and will not retry") 147 | 148 | job.dispatch( result, response_code, headers, body ) 149 | set_completed() 150 | manager._on_pipe_request_completed( ) 151 | 152 | 153 | func retry_job(): 154 | if job.retries >= manager.max_retries: 155 | #managed failed here 156 | manager.d("job could not complete after "+str(manager.max_retries)+" attempts.") 157 | else: 158 | job.retries += 1 159 | manager.d("retry "+str(job.retries)+"/"+str(manager.max_retries)+" job") 160 | manager.add_job( job ) 161 | set_completed() 162 | return true 163 | 164 | 165 | 166 | func set_completed(): 167 | reset() 168 | manager.dispatch() 169 | 170 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/HTTPManagerJob.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name HTTPManagerJob 3 | 4 | var _manager 5 | 6 | var url:String 7 | var request_method:int = -1 8 | 9 | var request_headers:Dictionary 10 | var request_files:Array[Dictionary] 11 | var request_get:Dictionary 12 | var request_post:Dictionary 13 | 14 | var request_get_query:String 15 | var unsafe_ssl:bool =false 16 | var force_mime:String 17 | var force_charset:String 18 | var use_cache:bool = true 19 | var use_proxy:bool = false 20 | 21 | var retries:int = 0 22 | var success:bool = false 23 | var download_filepath:String 24 | var callbacks:Array[Dictionary] 25 | var error:int 26 | 27 | 28 | ##add GET variable with ( name:String, value:Varying ) 29 | ##or add variables with ( variables:Dictionary ) 30 | ##returns self for function-chaining 31 | func add_get( name, value=null ) -> HTTPManagerJob: 32 | if name is String and value != null: 33 | request_get[name] = str(value) 34 | elif name is Dictionary: 35 | request_get.merge( name ) 36 | 37 | return self 38 | 39 | 40 | ##add POST variable with ( name:String, value:Varying ) 41 | ##or add variables with ( variables:Dictionary ) 42 | ##returns self for function-chaining 43 | func add_post( name, value=null ) -> HTTPManagerJob: 44 | if name is String and value != null: 45 | request_post[name] = str(value) 46 | elif name is Dictionary: 47 | request_post.merge( name ) 48 | 49 | return self 50 | 51 | 52 | ##add a file to the POST request 53 | ## name: name of the POST field 54 | ## path: path to the file to add 55 | ## mime: mime-type of the file 56 | func add_post_file( name:String, filepath:String, mime:String="auto" ) -> HTTPManagerJob: 57 | if mime == "auto": 58 | mime = HTTPManager.auto_mime(filepath) 59 | else: 60 | mime = "application/octet-stream" 61 | 62 | request_files.append({ 63 | 'name': name, 64 | 'path': filepath, 65 | 'basename': filepath.get_file(), 66 | 'mime': mime 67 | }) 68 | 69 | return self 70 | 71 | 72 | ##add a buffer to the POST request 73 | ## name: name of the POST field 74 | ## path: path to the file to add 75 | ## mime: mime-type of the file, use "auto" for mime guessing 76 | ## filename: the filename submitted in the request 77 | func add_post_buffer( name:String, buffer:PackedByteArray, mime:String="application/octet-stream", filename:String="buffer.bin" ) -> HTTPManagerJob: 78 | request_files.append({ 79 | 'name': name, 80 | 'buffer': buffer, 81 | 'basename': filename, 82 | 'mime': mime 83 | }) 84 | 85 | return self 86 | 87 | 88 | ##add HEADER with ( name:String, value:Varying ) 89 | ##or HEADERS with ( headers:Dictionary ) 90 | ##returns self for function-chaining 91 | func add_header( name, value=null ) -> HTTPManagerJob: 92 | if name is String and value != null: 93 | request_headers[name] = str(value) 94 | elif name is Dictionary: 95 | request_headers.merge( name ) 96 | 97 | return self 98 | 99 | 100 | ##adds auth-basic header to the request 101 | ##returns self for function-chaining 102 | func auth_basic( name:String, password:String ): 103 | add_header("Authorization","Basic "+Marshalls.utf8_to_base64(str(name, ":", password))) 104 | return self 105 | 106 | 107 | ##turns caching on or off 108 | ##caching must be enabled in HTTPManager to work 109 | ##returns self for function-chaining 110 | func cache( _use_cache:bool=true ) -> HTTPManagerJob: 111 | use_cache = _use_cache 112 | return self 113 | 114 | 115 | ##force a specific mime-type in response 116 | ##returns self for function-chaining 117 | func mime( mime:String ) -> HTTPManagerJob: 118 | force_mime = mime 119 | return self 120 | 121 | 122 | ##specifies the request method to use 123 | ##returns self for function-chaining 124 | func method( method:int ) -> HTTPManagerJob: 125 | request_method = method 126 | return self 127 | 128 | 129 | ##force a specific charset in response 130 | ##only when response is of mime "text" 131 | ##returns self for function-chaining 132 | func charset( charset:String ) -> HTTPManagerJob: 133 | force_charset = charset 134 | return self 135 | 136 | 137 | ##do not validate TLS 138 | ##returns self for function-chaining 139 | func unsafe() -> HTTPManagerJob: 140 | unsafe_ssl = true 141 | return self 142 | 143 | 144 | ##sends this job to the queue 145 | ##the response will be saved to a file when successfull 146 | ## filepath: filepath to where to store the file 147 | ## callback: a callable that will be called when job completes 148 | func download( filepath:String, callback = null ): 149 | download_filepath = filepath 150 | if callback is Callable: 151 | callbacks.append({ 152 | "callback": callback 153 | }) 154 | _manager.add_job( self ) 155 | 156 | 157 | ##sends this job to the queue 158 | ## callback: a callable that will be called when job completes 159 | func fetch( callback = null ): 160 | if callback is Callable: 161 | callbacks.append({ 162 | "callback": callback 163 | }) 164 | 165 | _manager.add_job( self ) 166 | 167 | 168 | ##add a callback that will be called no matter what 169 | func add_callback( callback = null ) -> HTTPManagerJob: 170 | if callback is Callable: 171 | callbacks.append( callback ) 172 | if callback is Array: 173 | for cb in callback: 174 | assert( cb is Callable ) 175 | callbacks.append( cb ) 176 | 177 | return self 178 | 179 | 180 | ##add a callback that will be called when request finished successfull with code 200 or 304 181 | func on_success( callback:Callable ) -> HTTPManagerJob: 182 | callbacks.append({ 183 | "success": true, 184 | "callback": callback 185 | }) 186 | 187 | return self 188 | 189 | 190 | ##set a object property with the decoded request result when successful with code 200 or 304 191 | func on_success_set( object:Object, property:String ) -> HTTPManagerJob: 192 | callbacks.append({ 193 | "success": true, 194 | "do": "set", 195 | "object": object, 196 | "property": property, 197 | }) 198 | 199 | return self 200 | 201 | 202 | ##add a callback that will be called when request fails in any way 203 | func on_failure( callback:Callable ) -> HTTPManagerJob: 204 | callbacks.append({ 205 | "success": false, 206 | "callback": callback 207 | }) 208 | 209 | return self 210 | 211 | 212 | ##add a callback that will be called when a specific HTTP response-code happend 213 | func on_code( code:int, callback:Callable ) -> HTTPManagerJob: 214 | callbacks.append({ 215 | "code": code, 216 | "callback": callback 217 | }) 218 | 219 | return self 220 | 221 | 222 | ##add a callback that will be called when a specific connection result-code happend 223 | func on_result( result:int, callback:Callable ) -> HTTPManagerJob: 224 | callbacks.append({ 225 | "result": result, 226 | "callback": callback 227 | }) 228 | 229 | return self 230 | 231 | 232 | func get_url(): 233 | var _url = url 234 | if request_get.size() > 0: 235 | _url += "?" + _manager.query_string_from_dict(request_get) 236 | return _url 237 | 238 | 239 | func dispatch( result:int, response_code:int, headers:PackedStringArray, body:PackedByteArray ): 240 | _manager.d("job "+url+" done") 241 | 242 | var response_headers:Dictionary 243 | var response_body:PackedByteArray 244 | var response_mime:Array 245 | var response_charset:String 246 | var forced_mime:Array 247 | var forced_charset:String 248 | var response_from_cache:bool 249 | 250 | #modify response by cacher 251 | if _manager.use_cache and use_cache: 252 | var cache_result = _manager.cacher.cache_response( self, result, response_code, headers, body ) 253 | headers = cache_result.headers 254 | body = cache_result.body 255 | response_from_cache = cache_result.from_cache 256 | 257 | if response_code == 200: 258 | success = true 259 | elif response_code == 304 and response_from_cache: 260 | success = true 261 | 262 | for header in headers: 263 | var h = _string_to_header( header ) 264 | if h: 265 | var header_name:String = h[0].to_lower() 266 | match header_name: 267 | "set-cookie": 268 | _manager.set_cookie(h[1],url) 269 | response_headers[header_name] = h[1] 270 | 271 | if response_headers.has("content-type"): 272 | var regex = RegEx.new() 273 | regex.compile("(\\w+)\\/(\\w+)") 274 | var r = regex.search(response_headers["content-type"]) 275 | response_mime = Array() 276 | response_mime.resize(3) 277 | if r and r.strings.size() == 3: 278 | response_mime = Array(r.strings) 279 | regex.compile("charset\\=([-\\d\\w]+)") 280 | r = regex.search(response_headers["content-type"]) 281 | if r and r.strings.size() == 2: 282 | response_charset = r.strings[1].to_lower() 283 | 284 | if force_mime != "": 285 | var regex = RegEx.new() 286 | regex.compile("(\\w+)\\/(\\w+)") 287 | var r = regex.search(force_mime) 288 | forced_mime = Array() 289 | if r and r.strings.size() == 3: 290 | forced_mime = Array(r.strings) 291 | else: 292 | printerr("HTTPManager: '",force_mime,"' is not a valid mime-type and will be ignored") 293 | forced_mime = Array() 294 | 295 | if force_charset != "": 296 | forced_charset = force_charset 297 | 298 | var response = null 299 | var mime = ["","",""] 300 | if response_mime.size() == 3: 301 | mime = response_mime 302 | if forced_mime.size() == 3: 303 | mime = forced_mime 304 | 305 | if _manager._mime_decoders.has(mime[1]+"_"+mime[2]): 306 | response = load(_manager._mime_decoders[mime[1]+"_"+mime[2]]).new() 307 | elif _manager._mime_decoders.has(mime[1]): 308 | response = load(_manager._mime_decoders[mime[1]]).new() 309 | else: 310 | response = load(_manager._mime_decoders["application_octet-stream"]).new() 311 | 312 | response_body = body 313 | 314 | response.request_url = url 315 | response.request_query = get_url() 316 | response.request_headers = request_headers 317 | response.request_get = request_get 318 | response.request_post = request_post 319 | response.request_files = request_files 320 | 321 | response.result = result 322 | response.from_cache = response_from_cache 323 | 324 | response.response_code = response_code 325 | response.response_headers = response_headers 326 | response.response_body = response_body 327 | response.response_mime = response_mime 328 | response.response_charset = response_charset 329 | 330 | #save to download file 331 | if download_filepath != "": 332 | DirAccess.make_dir_recursive_absolute(download_filepath.get_base_dir()) 333 | var file = FileAccess.open(download_filepath, FileAccess.WRITE) 334 | if file: 335 | file.store_buffer( response.response_body ) 336 | 337 | for cb in callbacks: 338 | if cb.has("success") and cb.success != success: 339 | continue 340 | if cb.has("result") and cb.result != result: 341 | continue 342 | if cb.has("code") and cb.code != response_code: 343 | continue 344 | if cb.has("not_result") and cb.not_result == result: 345 | continue 346 | if cb.has("not_code") and cb.not_code == response_code: 347 | continue 348 | if cb.has("callback"): 349 | cb.callback.call( response ) 350 | continue 351 | 352 | if cb.has("do") and cb.do == "set": 353 | if is_instance_valid( cb.object ): 354 | cb.object.set(cb.property, response.fetch()) 355 | 356 | if result == HTTPRequest.RESULT_SUCCESS and (response_code == 200 or response_code == 304): 357 | _manager.emit_signal("job_succeded",self) 358 | else: 359 | if _manager.pause_on_failure: 360 | _manager.pause() 361 | _manager.emit_signal("job_failed",self) 362 | 363 | _manager.emit_signal("job_completed",self) 364 | 365 | 366 | func _string_to_header( header:String ): 367 | var p = header.split(":", false, 1 ) 368 | if p.size() == 2: 369 | p[0] = p[0].to_lower() 370 | 371 | if p.size() < 2: 372 | p.append("") 373 | return p 374 | 375 | 376 | -------------------------------------------------------------------------------- /addons/HTTPManager/classes/HTTPManager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name HTTPManager 3 | 4 | ## a feature rich HTTP-Request-Manager 5 | ## 6 | ## [b]Features:[/b][br] 7 | ## 8 | ## - multiply parrallel request queue managment[br] 9 | ## - add GET and POST variables[br] 10 | ## - add upload files via POST request[br] 11 | ## - decodes response on mime-type[br] 12 | ## - custom decoders for mime-type[br] 13 | ## - automatic progress display[br] 14 | ## 15 | 16 | 17 | ##number of parallel http connections 18 | @export var parallel_connections_count:int = 5 19 | ##The size of the buffer used and maximum bytes to read per iteration. See HTTPClient.read_chunk_size. 20 | ##Set this to a lower value (e.g. 4096 for 4 KiB) when downloading small files to decrease memory usage at the cost of download speeds. 21 | @export var download_chunk_size:int = 65536 : 22 | set( value ): 23 | download_chunk_size = value 24 | for pipe in _pipes: 25 | pipe.download_chunk_size = value 26 | ##If true, multithreading is used to improve performance. 27 | @export var use_threads:bool = false : 28 | set( value ): 29 | use_threads = value 30 | for pipe in _pipes: 31 | pipe.use_threads = value 32 | ##If true, this header will be added to each request: Accept-Encoding: gzip, deflate telling servers that it's okay to compress response bodies. 33 | @export var accept_gzip:bool = true : 34 | set( value ): 35 | accept_gzip = value 36 | for pipe in _pipes: 37 | pipe.accept_gzip = value 38 | ##Maximum allowed size for response bodies. If the response body is compressed, this will be used as the maximum allowed size for the decompressed body. 39 | @export var body_size_limit:int = -1 : 40 | set( value ): 41 | body_size_limit = value 42 | for pipe in _pipes: 43 | pipe.body_size_limit = value 44 | 45 | ##Maximum number of allowed redirects. 46 | @export var max_redirects:int = 8 : 47 | set( value ): 48 | max_redirects = value 49 | for pipe in _pipes: 50 | pipe.max_redirects = value 51 | 52 | ##If set to a value greater than 0.0 before the request starts, the HTTP request will time out after timeout seconds have passed and the request is not completed yet. For small HTTP requests such as REST API usage, set timeout to a value between 10.0 and 30.0 to prevent the application from getting stuck if the request fails to get a response in a timely manner. For file downloads, leave this to 0.0 to prevent the download from failing if it takes too much time. 53 | @export var timeout:float = 0 : 54 | set( value ): 55 | timeout = value 56 | for pipe in _pipes: 57 | pipe.timeout = value 58 | ##maximal times the manager retries to request the job after failed connection 59 | @export var max_retries:int = 3 60 | 61 | @export_group("proxy") 62 | ##use proxy 63 | @export var use_proxy:bool = false 64 | @export var http_proxy:String = "127.0.0.1" 65 | @export var http_port:int = 8080 66 | @export var https_proxy:String = "127.0.0.1" 67 | @export var https_port:int = 8080 68 | 69 | @export_group("cache") 70 | ##use caching 71 | @export var use_cache:bool = false 72 | ## cache directory 73 | @export var cache_directory:String = "user://http-manager-cache" 74 | 75 | @export_group("progress scene") 76 | ##the interval delay to update progress scene and fire progress signal 77 | @export var signal_progress_interval:float = 0.5 78 | ##automatically display the progress scene when the queue is progressed 79 | @export var display_progress:bool = false 80 | ##custom scene to display when the queue is progressed 81 | @export var progress_scene:PackedScene = null 82 | 83 | @export_group("") 84 | ##accept cookies 85 | @export var accept_cookies:bool = false 86 | ##automatically go into pause mode when a job failed 87 | @export var pause_on_failure:bool = false 88 | ##print debug messages 89 | @export var print_debug:bool = false 90 | 91 | ##cache control module 92 | var cacher = null 93 | 94 | var _HTTPPipe = preload("res://addons/HTTPManager/classes/HTTPPipe.gd") 95 | var _pipes:Array[HTTPRequest] = [] 96 | var _client:HTTPClient 97 | var _jobs:Array[HTTPManagerJob] = [] 98 | ##queue processing is currently paused when true 99 | var is_paused:bool = false 100 | ##multipart post--data boundary 101 | var content_boundary:String = "GodotHTTPManagerContentBoundaryString" 102 | var _progress_timer:Timer 103 | var _max_assigned_files:int = 0 104 | var _progress_scene 105 | var _cookies:Dictionary 106 | 107 | var _mime_decoders:Dictionary 108 | 109 | # retry _jobs on this request results 110 | var _retry_on_result:Array = [ 111 | HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH, 112 | HTTPRequest.RESULT_CANT_CONNECT, 113 | #Request failed while connecting. 114 | HTTPRequest.RESULT_CONNECTION_ERROR, 115 | #Request failed due to connection (read/write) error. 116 | HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR, 117 | #Request failed on SSL handshake. 118 | HTTPRequest.RESULT_NO_RESPONSE, 119 | #Request does not have a response (yet). 120 | HTTPRequest.RESULT_TIMEOUT, 121 | #------------ 122 | #HTTPRequest.RESULT_CANT_RESOLVE, 123 | #Request failed while resolving. 124 | #HTTPRequest.RESULT_REQUEST_FAILED, 125 | #Request failed (currently unused). 126 | #HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED, 127 | #Request reached its maximum redirect limit, see max_redirects. 128 | #HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED, 129 | #Request exceeded its maximum size limit, see body_size_limit. 130 | ] 131 | 132 | const _result_error_string = { 133 | 0: "SUCCESS", 134 | 1: "CHUNKED BODY SIZE MISMATCH", 135 | 2: "CANT CONNECT", 136 | 3: "CANT RESOLVE", 137 | 4: "CONNECTION ERROR", 138 | 5: "TLS HANDSHAKE ERROR", 139 | 6: "NO RESPONSE", 140 | 7: "BODY SIZE LIMIT EXCEEDED", 141 | 8: "BODY DECOMPRESS FAILED", 142 | 9: "REQUEST FAILED", # Godot 4.1 docs say this is unused but I got it somehow once while testing 143 | 10: "CANT OPEN DOWNLOAD FILE", 144 | 11: "DOWNLOAD FILE WRITE ERROR", 145 | 12: "REDIRECT LIMIT REACHED", 146 | 13: "TIMEOUT" 147 | } 148 | 149 | const common_mime_types = { 150 | "aac": "audio/aac", 151 | "abw": "application/x-abiword", 152 | "arc": "application/x-freearc", 153 | "avif": "image/avif", 154 | "avi": "video/x-msvideo", 155 | "azw": "application/vnd.amazon.ebook", 156 | "bin": "application/octet-stream", 157 | "bmp": "image/bmp", 158 | "bz": "application/x-bzip", 159 | "bz2": "application/x-bzip2", 160 | "cda": "application/x-cdf", 161 | "csh": "application/x-csh", 162 | "css": "text/css", 163 | "csv": "text/csv", 164 | "doc": "application/msword", 165 | "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 166 | "eot": "application/vnd.ms-fontobject", 167 | "epub": "application/epub+zip", 168 | "gz": "application/gzip", 169 | "gif": "image/gif", 170 | "htm": "text/html", 171 | "html": "text/html", 172 | "ico": "image/vnd.microsoft.icon", 173 | "ics": "text/calendar", 174 | "jar": "application/java-archive", 175 | "jpeg": "image/jpeg", 176 | "jpg": "image/jpeg", 177 | "js": "text/javascript", 178 | "json": "application/json", 179 | "jsonld": "application/ld+json", 180 | "midi": "audio/midi, audio/x-midi", 181 | "mid": "audio/midi, audio/x-midi", 182 | "mjs": "text/javascript", 183 | "mp3": "audio/mpeg", 184 | "mp4": "video/mp4", 185 | "mpeg": "video/mpeg", 186 | "mpkg": "application/vnd.apple.installer+xml", 187 | "odp": "application/vnd.oasis.opendocument.presentation", 188 | "ods": "application/vnd.oasis.opendocument.spreadsheet", 189 | "odt": "application/vnd.oasis.opendocument.text", 190 | "oga": "audio/ogg", 191 | "ogv": "video/ogg", 192 | "ogx": "application/ogg", 193 | "opus": "audio/opus", 194 | "otf": "font/otf", 195 | "png": "image/png", 196 | "pdf": "application/pdf", 197 | "php": "application/x-httpd-php", 198 | "ppt": "application/vnd.ms-powerpoint", 199 | "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", 200 | "rar": "application/vnd.rar", 201 | "rtf": "application/rtf", 202 | "sh": "application/x-sh", 203 | "svg": "image/svg+xml", 204 | "tar": "application/x-tar", 205 | "tif, .tiff": "image/tiff", 206 | "ts": "video/mp2t", 207 | "ttf": "font/ttf", 208 | "txt": "text/plain", 209 | "vsd": "application/vnd.visio", 210 | "wav": "audio/wav", 211 | "weba": "audio/webm", 212 | "webm": "video/webm", 213 | "webp": "image/webp", 214 | "woff": "font/woff", 215 | "woff2": "font/woff2", 216 | "xhtml": "application/xhtml+xml", 217 | "xls": "application/vnd.ms-excel", 218 | "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 219 | "xml": "application/xml", 220 | "xul": "application/vnd.mozilla.xul+xml", 221 | "zip": "application/zip", 222 | "3gp": "video/3gpp", 223 | "3g2": "video/3gpp2", 224 | "7z": "application/x-7z-compressed" 225 | } 226 | 227 | ##emited when the manager goes into pause mode 228 | signal paused 229 | ##emited when the manager unpause and resumes the queue 230 | signal unpaused 231 | ##emited when the all _jobs in the queue has finished 232 | signal completed 233 | ##emited when progressed interval fires 234 | signal progress 235 | ##emited when a job failed 236 | signal job_failed 237 | ##emited when a job succeeded 238 | signal job_succeded 239 | ##emited when a job is completed 240 | signal job_completed 241 | 242 | 243 | func _ready(): 244 | #load all mime decoders in the addon decoders directory 245 | var dir_name = "res://addons/HTTPManager/classes/decoders/" 246 | var dir = DirAccess.open( dir_name ) 247 | if dir: 248 | var files = dir.get_files() 249 | for file_name in files: 250 | _mime_decoders[file_name.get_basename()] = dir_name+file_name 251 | 252 | #make a HTTPClient for utillity use 253 | _client = HTTPClient.new() 254 | 255 | #create the progress timer 256 | _progress_timer = Timer.new() 257 | _progress_timer.wait_time = signal_progress_interval 258 | _progress_timer.connect("timeout", self._on_progress_interval ) 259 | add_child(_progress_timer) 260 | _progress_timer.start() 261 | _progress_timer.paused = true 262 | 263 | #load the custom progress scene 264 | if not progress_scene: 265 | progress_scene = load("res://addons/HTTPManager/progress/progress.tscn") 266 | 267 | #add the progress scene 268 | _progress_scene = progress_scene.instantiate() 269 | add_child( _progress_scene ) 270 | _progress_scene.hide() 271 | 272 | #add the progress scene 273 | cacher = load("res://addons/HTTPManager/classes/HTTPManagerCacher.gd").new() 274 | cacher.manager = self 275 | 276 | #create the http _pipes 277 | for i in parallel_connections_count: 278 | var pipe = _HTTPPipe.new() 279 | _pipes.append( pipe ) 280 | pipe.manager = self 281 | pipe.accept_gzip = accept_gzip 282 | pipe.body_size_limit = body_size_limit 283 | pipe.download_chunk_size = download_chunk_size 284 | pipe.max_redirects = max_redirects 285 | pipe.use_threads = use_threads 286 | pipe.timeout = timeout 287 | add_child( pipe ) 288 | 289 | 290 | ##creates a http job object with given request url 291 | ##returns HTTPManagerJob 292 | func job( url:String ) -> HTTPManagerJob: 293 | var job = HTTPManagerJob.new() 294 | job._manager = self 295 | job.url = url 296 | #set defaults for jobs 297 | job.use_cache = use_cache 298 | #set proxy 299 | job.use_proxy = use_proxy 300 | return job 301 | 302 | 303 | ##add the job to the queue and starts processing the queue 304 | func add_job( job:HTTPManagerJob ): 305 | _max_assigned_files += 1 306 | d("job added "+job.url) 307 | _jobs.append( job ) 308 | dispatch() 309 | 310 | 311 | func dispatch(): 312 | if is_paused: 313 | #if paused dont dispacth a new job 314 | return 315 | 316 | #if _jobs in queue 317 | if _jobs.size() > 0: 318 | #check every pipe if not busy 319 | for pipe in _pipes: 320 | if not pipe.is_busy: 321 | #pop a job from the queue and pdispatch it 322 | var job = _jobs.pop_front() 323 | _progress_timer.paused = false 324 | if display_progress: 325 | _progress_scene.popup_centered() 326 | pipe.dispatch( job ) 327 | 328 | if _jobs.size() <= 0: 329 | #when this was the last job in queue, return 330 | return 331 | 332 | 333 | func query_string_from_dict( dict:Dictionary ): 334 | return _client.query_string_from_dict( dict ) 335 | 336 | 337 | func _on_pipe_request_completed(): 338 | if _jobs.size() > 0: 339 | dispatch() 340 | return 341 | 342 | for pipe in _pipes: 343 | if pipe.is_busy: 344 | return 345 | 346 | #no more _jobs in queue or still in pipe, end execution 347 | _progress_timer.paused = true 348 | if display_progress: 349 | _progress_scene.hide() 350 | _max_assigned_files = 0 351 | emit_signal("completed") 352 | 353 | 354 | func _on_progress_interval(): 355 | var current_files = _jobs.size() 356 | var total_bytes:float = 0.00001 357 | var current_bytes:float = 0 358 | for pipe in _pipes: 359 | if pipe.is_busy: 360 | total_bytes += pipe.get_body_size ( ) 361 | current_files += 1 362 | current_bytes += pipe.get_downloaded_bytes() 363 | 364 | if _progress_scene: 365 | emit_signal("progress", _max_assigned_files, current_files, total_bytes, current_bytes ) 366 | _progress_scene.httpmanager_progress_update( _max_assigned_files, current_files, total_bytes, current_bytes ) 367 | 368 | 369 | ##stops all _jobs and clears the queue 370 | func clear(): 371 | d("clear") 372 | _jobs.clear() 373 | for pipe in _pipes: 374 | pipe.reset() 375 | 376 | 377 | ##pauses queue execution 378 | ##running requests will complete but no further requests will be started 379 | func pause(): 380 | d("paused") 381 | is_paused = true 382 | emit_signal("paused") 383 | 384 | 385 | ##resumes queue processing 386 | func unpause(): 387 | d("unpaused") 388 | is_paused = false 389 | emit_signal("unpaused") 390 | dispatch() 391 | 392 | 393 | ##cleares all cookies for domains ending with "clear_domain" 394 | func clear_cookies( clear_domain:String="" ): 395 | for domain in _cookies: 396 | if domain.ends_with(clear_domain): 397 | d(_cookies[domain].size()+" cookies for "+domain+" cleared") 398 | _cookies.erase(domain) 399 | 400 | 401 | func set_cookie( value:String, request_url:String ): 402 | if not accept_cookies: 403 | return 404 | var cookie = HTTPManagerCookie.new() 405 | cookie.manager = self 406 | cookie.parse( value, request_url ) 407 | 408 | 409 | static func parse_url( url:String ): 410 | var result = { 411 | "scheme": "__empty__", 412 | "host": "__empty__", 413 | "query": "__empty__" 414 | } 415 | var reg = RegEx.new() 416 | reg.compile("^(.*)\\:\\/\\/([^\\/]*)\\/(.*)") 417 | var res = reg.search( url ) 418 | if res and res.strings.size() == 4: 419 | result.scheme = res.strings[1] 420 | result.host = res.strings[2] 421 | result.query = res.strings[3] 422 | elif res and res.strings.size() == 3: 423 | result.scheme = res.strings[1] 424 | result.host = res.strings[2] 425 | 426 | return result 427 | 428 | 429 | static func auto_mime( filename:String ): 430 | var ext = filename.get_extension() 431 | if common_mime_types.has(ext): 432 | return common_mime_types[ext] 433 | else: 434 | return "application/octet-stream" 435 | 436 | 437 | func d( msg ): 438 | if print_debug: 439 | print( "HTTPManager: "+str(msg) ) 440 | 441 | 442 | static func e( msg ): 443 | printerr( "HTTPManager: "+str(msg) ) 444 | 445 | --------------------------------------------------------------------------------