| 1 | /**************************************************************************/ |
| 2 | /* editor_http_server.h */ |
| 3 | /**************************************************************************/ |
| 4 | /* This file is part of: */ |
| 5 | /* GODOT ENGINE */ |
| 6 | /* https://godotengine.org */ |
| 7 | /**************************************************************************/ |
| 8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | /* */ |
| 11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | /* a copy of this software and associated documentation files (the */ |
| 13 | /* "Software"), to deal in the Software without restriction, including */ |
| 14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | /* the following conditions: */ |
| 18 | /* */ |
| 19 | /* The above copyright notice and this permission notice shall be */ |
| 20 | /* included in all copies or substantial portions of the Software. */ |
| 21 | /* */ |
| 22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | /**************************************************************************/ |
| 30 | |
| 31 | #ifndef WEB_EDITOR_HTTP_SERVER_H |
| 32 | #define WEB_EDITOR_HTTP_SERVER_H |
| 33 | |
| 34 | #include "core/io/image_loader.h" |
| 35 | #include "core/io/stream_peer_tls.h" |
| 36 | #include "core/io/tcp_server.h" |
| 37 | #include "core/io/zip_io.h" |
| 38 | #include "editor/editor_paths.h" |
| 39 | |
| 40 | class EditorHTTPServer : public RefCounted { |
| 41 | private: |
| 42 | Ref<TCPServer> server; |
| 43 | HashMap<String, String> mimes; |
| 44 | Ref<StreamPeerTCP> tcp; |
| 45 | Ref<StreamPeerTLS> tls; |
| 46 | Ref<StreamPeer> peer; |
| 47 | Ref<CryptoKey> key; |
| 48 | Ref<X509Certificate> cert; |
| 49 | bool use_tls = false; |
| 50 | uint64_t time = 0; |
| 51 | uint8_t req_buf[4096]; |
| 52 | int req_pos = 0; |
| 53 | |
| 54 | void _clear_client() { |
| 55 | peer = Ref<StreamPeer>(); |
| 56 | tls = Ref<StreamPeerTLS>(); |
| 57 | tcp = Ref<StreamPeerTCP>(); |
| 58 | memset(req_buf, 0, sizeof(req_buf)); |
| 59 | time = 0; |
| 60 | req_pos = 0; |
| 61 | } |
| 62 | |
| 63 | void _set_internal_certs(Ref<Crypto> p_crypto) { |
| 64 | const String cache_path = EditorPaths::get_singleton()->get_cache_dir(); |
| 65 | const String key_path = cache_path.path_join("html5_server.key" ); |
| 66 | const String crt_path = cache_path.path_join("html5_server.crt" ); |
| 67 | bool regen = !FileAccess::exists(key_path) || !FileAccess::exists(crt_path); |
| 68 | if (!regen) { |
| 69 | key = Ref<CryptoKey>(CryptoKey::create()); |
| 70 | cert = Ref<X509Certificate>(X509Certificate::create()); |
| 71 | if (key->load(key_path) != OK || cert->load(crt_path) != OK) { |
| 72 | regen = true; |
| 73 | } |
| 74 | } |
| 75 | if (regen) { |
| 76 | key = p_crypto->generate_rsa(2048); |
| 77 | key->save(key_path); |
| 78 | cert = p_crypto->generate_self_signed_certificate(key, "CN=godot-debug.local,O=A Game Dev,C=XXA" , "20140101000000" , "20340101000000" ); |
| 79 | cert->save(crt_path); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | public: |
| 84 | EditorHTTPServer() { |
| 85 | mimes["html" ] = "text/html" ; |
| 86 | mimes["js" ] = "application/javascript" ; |
| 87 | mimes["json" ] = "application/json" ; |
| 88 | mimes["pck" ] = "application/octet-stream" ; |
| 89 | mimes["png" ] = "image/png" ; |
| 90 | mimes["svg" ] = "image/svg" ; |
| 91 | mimes["wasm" ] = "application/wasm" ; |
| 92 | server.instantiate(); |
| 93 | stop(); |
| 94 | } |
| 95 | |
| 96 | void stop() { |
| 97 | server->stop(); |
| 98 | _clear_client(); |
| 99 | } |
| 100 | |
| 101 | Error listen(int p_port, IPAddress p_address, bool p_use_tls, String p_tls_key, String p_tls_cert) { |
| 102 | use_tls = p_use_tls; |
| 103 | if (use_tls) { |
| 104 | Ref<Crypto> crypto = Crypto::create(); |
| 105 | if (crypto.is_null()) { |
| 106 | return ERR_UNAVAILABLE; |
| 107 | } |
| 108 | if (!p_tls_key.is_empty() && !p_tls_cert.is_empty()) { |
| 109 | key = Ref<CryptoKey>(CryptoKey::create()); |
| 110 | Error err = key->load(p_tls_key); |
| 111 | ERR_FAIL_COND_V(err != OK, err); |
| 112 | cert = Ref<X509Certificate>(X509Certificate::create()); |
| 113 | err = cert->load(p_tls_cert); |
| 114 | ERR_FAIL_COND_V(err != OK, err); |
| 115 | } else { |
| 116 | _set_internal_certs(crypto); |
| 117 | } |
| 118 | } |
| 119 | return server->listen(p_port, p_address); |
| 120 | } |
| 121 | |
| 122 | bool is_listening() const { |
| 123 | return server->is_listening(); |
| 124 | } |
| 125 | |
| 126 | void _send_response() { |
| 127 | Vector<String> psa = String((char *)req_buf).split("\r\n" ); |
| 128 | int len = psa.size(); |
| 129 | ERR_FAIL_COND_MSG(len < 4, "Not enough response headers, got: " + itos(len) + ", expected >= 4." ); |
| 130 | |
| 131 | Vector<String> req = psa[0].split(" " , false); |
| 132 | ERR_FAIL_COND_MSG(req.size() < 2, "Invalid protocol or status code." ); |
| 133 | |
| 134 | // Wrong protocol |
| 135 | ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1" , "Invalid method or HTTP version." ); |
| 136 | |
| 137 | const int query_index = req[1].find_char('?'); |
| 138 | const String path = (query_index == -1) ? req[1] : req[1].substr(0, query_index); |
| 139 | |
| 140 | const String req_file = path.get_file(); |
| 141 | const String req_ext = path.get_extension(); |
| 142 | const String cache_path = EditorPaths::get_singleton()->get_cache_dir().path_join("web" ); |
| 143 | const String filepath = cache_path.path_join(req_file); |
| 144 | |
| 145 | if (!mimes.has(req_ext) || !FileAccess::exists(filepath)) { |
| 146 | String s = "HTTP/1.1 404 Not Found\r\n" ; |
| 147 | s += "Connection: Close\r\n" ; |
| 148 | s += "\r\n" ; |
| 149 | CharString cs = s.utf8(); |
| 150 | peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); |
| 151 | return; |
| 152 | } |
| 153 | const String ctype = mimes[req_ext]; |
| 154 | |
| 155 | Ref<FileAccess> f = FileAccess::open(filepath, FileAccess::READ); |
| 156 | ERR_FAIL_COND(f.is_null()); |
| 157 | String s = "HTTP/1.1 200 OK\r\n" ; |
| 158 | s += "Connection: Close\r\n" ; |
| 159 | s += "Content-Type: " + ctype + "\r\n" ; |
| 160 | s += "Access-Control-Allow-Origin: *\r\n" ; |
| 161 | s += "Cross-Origin-Opener-Policy: same-origin\r\n" ; |
| 162 | s += "Cross-Origin-Embedder-Policy: require-corp\r\n" ; |
| 163 | s += "Cache-Control: no-store, max-age=0\r\n" ; |
| 164 | s += "\r\n" ; |
| 165 | CharString cs = s.utf8(); |
| 166 | Error err = peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); |
| 167 | if (err != OK) { |
| 168 | ERR_FAIL(); |
| 169 | } |
| 170 | |
| 171 | while (true) { |
| 172 | uint8_t bytes[4096]; |
| 173 | uint64_t read = f->get_buffer(bytes, 4096); |
| 174 | if (read == 0) { |
| 175 | break; |
| 176 | } |
| 177 | err = peer->put_data(bytes, read); |
| 178 | if (err != OK) { |
| 179 | ERR_FAIL(); |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | void poll() { |
| 185 | if (!server->is_listening()) { |
| 186 | return; |
| 187 | } |
| 188 | if (tcp.is_null()) { |
| 189 | if (!server->is_connection_available()) { |
| 190 | return; |
| 191 | } |
| 192 | tcp = server->take_connection(); |
| 193 | peer = tcp; |
| 194 | time = OS::get_singleton()->get_ticks_usec(); |
| 195 | } |
| 196 | if (OS::get_singleton()->get_ticks_usec() - time > 1000000) { |
| 197 | _clear_client(); |
| 198 | return; |
| 199 | } |
| 200 | if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) { |
| 201 | return; |
| 202 | } |
| 203 | |
| 204 | if (use_tls) { |
| 205 | if (tls.is_null()) { |
| 206 | tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); |
| 207 | peer = tls; |
| 208 | if (tls->accept_stream(tcp, TLSOptions::server(key, cert)) != OK) { |
| 209 | _clear_client(); |
| 210 | return; |
| 211 | } |
| 212 | } |
| 213 | tls->poll(); |
| 214 | if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { |
| 215 | // Still handshaking, keep waiting. |
| 216 | return; |
| 217 | } |
| 218 | if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { |
| 219 | _clear_client(); |
| 220 | return; |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | while (true) { |
| 225 | char *r = (char *)req_buf; |
| 226 | int l = req_pos - 1; |
| 227 | if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { |
| 228 | _send_response(); |
| 229 | _clear_client(); |
| 230 | return; |
| 231 | } |
| 232 | |
| 233 | int read = 0; |
| 234 | ERR_FAIL_COND(req_pos >= 4096); |
| 235 | Error err = peer->get_partial_data(&req_buf[req_pos], 1, read); |
| 236 | if (err != OK) { |
| 237 | // Got an error |
| 238 | _clear_client(); |
| 239 | return; |
| 240 | } else if (read != 1) { |
| 241 | // Busy, wait next poll |
| 242 | return; |
| 243 | } |
| 244 | req_pos += read; |
| 245 | } |
| 246 | } |
| 247 | }; |
| 248 | |
| 249 | #endif // WEB_EDITOR_HTTP_SERVER_H |
| 250 | |