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 | |