Compare commits
	
		
			5 Commits
		
	
	
		
			a09fefab5e
			...
			a84f850e91
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a84f850e91 | |||
| 5a765e6f07 | |||
| 791889c659 | |||
| 5da63faf1f | |||
| 30d108fc35 | 
| @@ -1 +1,3 @@ | ||||
| app.setDocument('<p style="color: #fff">Maybe one day this app will run tests, but for now there is nothing to see here.</p>'); | ||||
| app.setDocument( | ||||
| 	'<p style="color: #fff">Maybe one day this app will run tests, but for now there is nothing to see here.</p>' | ||||
| ); | ||||
|   | ||||
							
								
								
									
										280
									
								
								core/core.js
									
									
									
									
									
								
							
							
						
						
									
										280
									
								
								core/core.js
									
									
									
									
									
								
							| @@ -5,9 +5,6 @@ import * as http from './http.js'; | ||||
| let gProcesses = {}; | ||||
| let gStatsTimer = false; | ||||
|  | ||||
| const k_content_security_policy = | ||||
| 	'sandbox allow-downloads allow-top-navigation-by-user-activation'; | ||||
|  | ||||
| const k_global_settings = { | ||||
| 	index: { | ||||
| 		type: 'string', | ||||
| @@ -812,206 +809,6 @@ async function getProcessBlob(blobId, key, options) { | ||||
| 	return process; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * TODOC | ||||
|  * @param {*} response | ||||
|  * @param {*} data | ||||
|  * @param {*} type | ||||
|  * @param {*} headers | ||||
|  * @param {*} status_code | ||||
|  */ | ||||
| function sendData(response, data, type, headers, status_code) { | ||||
| 	if (data) { | ||||
| 		response.writeHead( | ||||
| 			status_code ?? 200, | ||||
| 			Object.assign( | ||||
| 				{ | ||||
| 					'Content-Type': | ||||
| 						type || | ||||
| 						httpd.mime_type_from_magic_bytes(data) || | ||||
| 						'application/binary', | ||||
| 					'Content-Length': data.byteLength, | ||||
| 				}, | ||||
| 				headers || {} | ||||
| 			) | ||||
| 		); | ||||
| 		response.end(data); | ||||
| 	} else { | ||||
| 		response.writeHead( | ||||
| 			status_code ?? 404, | ||||
| 			Object.assign( | ||||
| 				{ | ||||
| 					'Content-Type': 'text/plain; charset=utf-8', | ||||
| 					'Content-Length': 'File not found'.length, | ||||
| 				}, | ||||
| 				headers || {} | ||||
| 			) | ||||
| 		); | ||||
| 		response.end('File not found'); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| let g_handler_index = 0; | ||||
|  | ||||
| /** | ||||
|  * TODOC | ||||
|  * @param {*} response | ||||
|  * @param {*} handler_blob_id | ||||
|  * @param {*} path | ||||
|  * @param {*} query | ||||
|  * @param {*} headers | ||||
|  * @param {*} packageOwner | ||||
|  * @param {*} packageName | ||||
|  * @returns | ||||
|  */ | ||||
| async function useAppHandler( | ||||
| 	response, | ||||
| 	handler_blob_id, | ||||
| 	path, | ||||
| 	query, | ||||
| 	headers, | ||||
| 	packageOwner, | ||||
| 	packageName | ||||
| ) { | ||||
| 	print('useAppHandler', packageOwner, packageName); | ||||
| 	let do_resolve; | ||||
| 	let promise = new Promise(async function (resolve, reject) { | ||||
| 		do_resolve = resolve; | ||||
| 	}); | ||||
| 	let process; | ||||
| 	let result; | ||||
| 	try { | ||||
| 		process = await getProcessBlob( | ||||
| 			handler_blob_id, | ||||
| 			'handler_' + g_handler_index++, | ||||
| 			{ | ||||
| 				script: 'handler.js', | ||||
| 				imports: { | ||||
| 					request: { | ||||
| 						path: path, | ||||
| 						query: query, | ||||
| 					}, | ||||
| 					respond: do_resolve, | ||||
| 				}, | ||||
| 				credentials: await httpd.auth_query(headers), | ||||
| 				packageOwner: packageOwner, | ||||
| 				packageName: packageName, | ||||
| 			} | ||||
| 		); | ||||
| 		await process.ready; | ||||
|  | ||||
| 		result = await promise; | ||||
| 	} finally { | ||||
| 		if (process?.task) { | ||||
| 			await process.task.kill(); | ||||
| 		} | ||||
| 	} | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * TODOC | ||||
|  * @param {*} request | ||||
|  * @param {*} response | ||||
|  * @param {*} blobId | ||||
|  * @param {*} uri | ||||
|  * @returns | ||||
|  */ | ||||
| async function blobHandler(request, response, blobId, uri) { | ||||
| 	if (!uri) { | ||||
| 		response.writeHead(303, { | ||||
| 			Location: | ||||
| 				(request.client.tls ? 'https://' : 'http://') + | ||||
| 				(request.headers['x-forwarded-host'] ?? request.headers.host) + | ||||
| 				blobId + | ||||
| 				'/', | ||||
| 			'Content-Length': '0', | ||||
| 		}); | ||||
| 		response.end(); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	let process; | ||||
| 	let data; | ||||
| 	let match; | ||||
| 	let id; | ||||
| 	let app_id = blobId; | ||||
| 	let packageOwner; | ||||
| 	let packageName; | ||||
| 	if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) { | ||||
| 		packageOwner = match[1]; | ||||
| 		packageName = match[2]; | ||||
| 		let db = new Database(match[1]); | ||||
| 		app_id = await db.get('path:' + match[2]); | ||||
| 	} | ||||
|  | ||||
| 	let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id))); | ||||
| 	id = app_object?.files[uri.substring(1)]; | ||||
| 	if (!id && app_object?.files['handler.js']) { | ||||
| 		let answer; | ||||
| 		try { | ||||
| 			answer = await useAppHandler( | ||||
| 				response, | ||||
| 				app_id, | ||||
| 				uri.substring(1), | ||||
| 				request.query ? form.decodeForm(request.query) : undefined, | ||||
| 				request.headers, | ||||
| 				packageOwner, | ||||
| 				packageName | ||||
| 			); | ||||
| 		} catch (error) { | ||||
| 			data = utf8Encode( | ||||
| 				`Internal Server Error\n\n${error?.message}\n${error?.stack}` | ||||
| 			); | ||||
| 			response.writeHead(500, { | ||||
| 				'Content-Type': 'text/plain; charset=utf-8', | ||||
| 				'Content-Length': data.length, | ||||
| 			}); | ||||
| 			response.end(data); | ||||
| 			return; | ||||
| 		} | ||||
| 		if (answer && typeof answer.data == 'string') { | ||||
| 			answer.data = utf8Encode(answer.data); | ||||
| 		} | ||||
| 		sendData( | ||||
| 			response, | ||||
| 			answer?.data, | ||||
| 			answer?.content_type, | ||||
| 			Object.assign(answer?.headers ?? {}, { | ||||
| 				'Access-Control-Allow-Origin': '*', | ||||
| 				'Content-Security-Policy': k_content_security_policy, | ||||
| 			}), | ||||
| 			answer.status_code | ||||
| 		); | ||||
| 	} else if (id) { | ||||
| 		if ( | ||||
| 			request.headers['if-none-match'] && | ||||
| 			request.headers['if-none-match'] == '"' + id + '"' | ||||
| 		) { | ||||
| 			let headers = { | ||||
| 				'Access-Control-Allow-Origin': '*', | ||||
| 				'Content-Security-Policy': k_content_security_policy, | ||||
| 				'Content-Length': '0', | ||||
| 			}; | ||||
| 			response.writeHead(304, headers); | ||||
| 			response.end(); | ||||
| 		} else { | ||||
| 			let headers = { | ||||
| 				ETag: '"' + id + '"', | ||||
| 				'Access-Control-Allow-Origin': '*', | ||||
| 				'Content-Security-Policy': k_content_security_policy, | ||||
| 			}; | ||||
| 			data = await ssb.blobGet(id); | ||||
| 			let type = | ||||
| 				httpd.mime_type_from_extension(uri) || | ||||
| 				httpd.mime_type_from_magic_bytes(data); | ||||
| 			sendData(response, data, type, headers); | ||||
| 		} | ||||
| 	} else { | ||||
| 		sendData(response, data, undefined, {}); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| ssb.addEventListener('message', function () { | ||||
| 	broadcastEvent('onMessage', [...arguments]); | ||||
| }); | ||||
| @@ -1063,6 +860,73 @@ function sendStats() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| let g_handler_index = 0; | ||||
|  | ||||
| exports.callAppHandler = async function callAppHandler( | ||||
| 	response, | ||||
| 	app_blob_id, | ||||
| 	path, | ||||
| 	query, | ||||
| 	headers, | ||||
| 	package_owner, | ||||
| 	package_name | ||||
| ) { | ||||
| 	let answer; | ||||
| 	try { | ||||
| 		let do_resolve; | ||||
| 		let promise = new Promise(async function (resolve, reject) { | ||||
| 			do_resolve = resolve; | ||||
| 		}); | ||||
| 		let process; | ||||
| 		try { | ||||
| 			process = await getProcessBlob( | ||||
| 				app_blob_id, | ||||
| 				'handler_' + g_handler_index++, | ||||
| 				{ | ||||
| 					script: 'handler.js', | ||||
| 					imports: { | ||||
| 						request: { | ||||
| 							path: path, | ||||
| 							query: query, | ||||
| 						}, | ||||
| 						respond: do_resolve, | ||||
| 					}, | ||||
| 					credentials: await httpd.auth_query(headers), | ||||
| 					packageOwner: package_owner, | ||||
| 					packageName: package_name, | ||||
| 				} | ||||
| 			); | ||||
| 			await process.ready; | ||||
| 			answer = await promise; | ||||
| 		} finally { | ||||
| 			if (process?.task) { | ||||
| 				await process.task.kill(); | ||||
| 			} | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		let data = utf8Encode( | ||||
| 			`Internal Server Error\n\n${error?.message}\n${error?.stack}` | ||||
| 		); | ||||
| 		response.writeHead(500, { | ||||
| 			'Content-Type': 'text/plain; charset=utf-8', | ||||
| 			'Content-Length': data.length, | ||||
| 		}); | ||||
| 		response.end(data); | ||||
| 		return; | ||||
| 	} | ||||
| 	if (typeof answer?.data == 'string') { | ||||
| 		answer.data = utf8Encode(answer.data); | ||||
| 	} | ||||
| 	response.writeHead(answer?.status_code, { | ||||
| 		'Content-Type': answer?.content_type, | ||||
| 		'Content-Length': answer?.data?.length, | ||||
| 		'Access-Control-Allow-Origin': '*', | ||||
| 		'Content-Security-Policy': | ||||
| 			'sandbox allow-downloads allow-top-navigation-by-user-activation', | ||||
| 	}); | ||||
| 	response.end(answer?.data); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * TODOC | ||||
|  */ | ||||
| @@ -1072,16 +936,6 @@ loadSettings() | ||||
| 			httpd.set_http_redirect(settings.http_redirect); | ||||
| 		} | ||||
| 		httpd.all('/app/socket', app.socket); | ||||
| 		httpd.all('', function default_http_handler(request, response) { | ||||
| 			let match; | ||||
| 			if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) { | ||||
| 				return blobHandler(request, response, match[1], match[2]); | ||||
| 			} else if ( | ||||
| 				(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) | ||||
| 			) { | ||||
| 				return blobHandler(request, response, match[1], match[2]); | ||||
| 			} | ||||
| 		}); | ||||
| 		let port = httpd.start(tildefriends.http_port); | ||||
| 		if (tildefriends.args.out_http_port_file) { | ||||
| 			print('Writing the port file.'); | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/http.c
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/http.c
									
									
									
									
									
								
							| @@ -130,7 +130,7 @@ static void _http_allocate_buffer(uv_handle_t* handle, size_t suggested_size, uv | ||||
|  | ||||
| bool tf_http_pattern_matches(const char* pattern, const char* path) | ||||
| { | ||||
| 	if (!pattern || !*pattern) | ||||
| 	if (!*pattern && !*path) | ||||
| 	{ | ||||
| 		return true; | ||||
| 	} | ||||
| @@ -156,12 +156,14 @@ bool tf_http_pattern_matches(const char* pattern, const char* path) | ||||
| 		{ | ||||
| 			while (true) | ||||
| 			{ | ||||
| 				if (((path[j] >= 'a' && path[j] <= 'z') || (path[j] >= 'A' && path[j] <= 'Z') || (path[j] >= '0' && path[j] <= '9')) && | ||||
| 					tf_http_pattern_matches(pattern + i + k_word_len, path + j + 1)) | ||||
| 				if ((path[j] >= 'a' && path[j] <= 'z') || (path[j] >= 'A' && path[j] <= 'Z') || (path[j] >= '0' && path[j] <= '9')) | ||||
| 				{ | ||||
| 					return true; | ||||
| 					if (tf_http_pattern_matches(pattern + i + k_word_len, path + j + 1)) | ||||
| 					{ | ||||
| 						return true; | ||||
| 					} | ||||
| 				} | ||||
| 				if (!path[j]) | ||||
| 				else | ||||
| 				{ | ||||
| 					break; | ||||
| 				} | ||||
|   | ||||
							
								
								
									
										249
									
								
								src/httpd.js.c
									
									
									
									
									
								
							
							
						
						
									
										249
									
								
								src/httpd.js.c
									
									
									
									
									
								
							| @@ -224,6 +224,17 @@ static void _httpd_message_callback(tf_http_request_t* request, int op_code, con | ||||
| 	JS_FreeValue(context, on_message); | ||||
| } | ||||
|  | ||||
| static JSValue _httpd_make_response_object(JSContext* context, tf_http_request_t* request) | ||||
| { | ||||
| 	JSValue response_object = JS_NewObjectClass(context, _httpd_request_class_id); | ||||
| 	JS_SetOpaque(response_object, request); | ||||
| 	JS_SetPropertyStr(context, response_object, "writeHead", JS_NewCFunction(context, _httpd_response_write_head, "writeHead", 2)); | ||||
| 	JS_SetPropertyStr(context, response_object, "end", JS_NewCFunction(context, _httpd_response_end, "end", 1)); | ||||
| 	JS_SetPropertyStr(context, response_object, "send", JS_NewCFunction(context, _httpd_response_send, "send", 2)); | ||||
| 	JS_SetPropertyStr(context, response_object, "upgrade", JS_NewCFunction(context, _httpd_websocket_upgrade, "upgrade", 2)); | ||||
| 	return response_object; | ||||
| } | ||||
|  | ||||
| static void _httpd_callback_internal(tf_http_request_t* request, bool is_websocket) | ||||
| { | ||||
| 	http_handler_data_t* data = request->user_data; | ||||
| @@ -250,14 +261,9 @@ static void _httpd_callback_internal(tf_http_request_t* request, bool is_websock | ||||
| 	JS_SetPropertyStr(context, client, "tls", request->is_tls ? JS_TRUE : JS_FALSE); | ||||
| 	JS_SetPropertyStr(context, request_object, "client", client); | ||||
|  | ||||
| 	JSValue response_object = JS_NewObjectClass(context, _httpd_request_class_id); | ||||
| 	JSValue response_object = _httpd_make_response_object(context, request); | ||||
| 	/* The ref is owned by the JS object and will be released by the finalizer. */ | ||||
| 	tf_http_request_ref(request); | ||||
| 	JS_SetOpaque(response_object, request); | ||||
| 	JS_SetPropertyStr(context, response_object, "writeHead", JS_NewCFunction(context, _httpd_response_write_head, "writeHead", 2)); | ||||
| 	JS_SetPropertyStr(context, response_object, "end", JS_NewCFunction(context, _httpd_response_end, "end", 1)); | ||||
| 	JS_SetPropertyStr(context, response_object, "send", JS_NewCFunction(context, _httpd_response_send, "send", 2)); | ||||
| 	JS_SetPropertyStr(context, response_object, "upgrade", JS_NewCFunction(context, _httpd_websocket_upgrade, "upgrade", 2)); | ||||
| 	JSValue args[] = { | ||||
| 		request_object, | ||||
| 		response_object, | ||||
| @@ -560,7 +566,7 @@ static bool _magic_bytes_match(const magic_bytes_t* magic, const uint8_t* actual | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| static const char* _httpd_mime_type_from_magic_bytes_internal(const uint8_t* bytes, size_t size) | ||||
| static const char* _httpd_mime_type_from_magic_bytes(const uint8_t* bytes, size_t size) | ||||
| { | ||||
| 	const char* type = "application/binary"; | ||||
| 	if (bytes) | ||||
| @@ -637,13 +643,6 @@ static const char* _httpd_mime_type_from_magic_bytes_internal(const uint8_t* byt | ||||
| 	return type; | ||||
| } | ||||
|  | ||||
| static JSValue _httpd_mime_type_from_magic_bytes(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) | ||||
| { | ||||
| 	size_t size = 0; | ||||
| 	uint8_t* bytes = tf_util_try_get_array_buffer(context, &size, argv[0]); | ||||
| 	return JS_NewString(context, _httpd_mime_type_from_magic_bytes_internal(bytes, size)); | ||||
| } | ||||
|  | ||||
| static const char* _ext_to_content_type(const char* ext, bool use_fallback) | ||||
| { | ||||
| 	if (ext) | ||||
| @@ -676,14 +675,6 @@ static const char* _ext_to_content_type(const char* ext, bool use_fallback) | ||||
| 	return use_fallback ? "application/binary" : NULL; | ||||
| } | ||||
|  | ||||
| static JSValue _httpd_mime_type_from_extension(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) | ||||
| { | ||||
| 	const char* name = JS_ToCString(context, argv[0]); | ||||
| 	const char* type = _ext_to_content_type(strrchr(name, '.'), false); | ||||
| 	JS_FreeCString(context, name); | ||||
| 	return type ? JS_NewString(context, type) : JS_UNDEFINED; | ||||
| } | ||||
|  | ||||
| static void _httpd_finalizer(JSRuntime* runtime, JSValue value) | ||||
| { | ||||
| 	tf_http_t* http = JS_GetOpaque(value, _httpd_class_id); | ||||
| @@ -968,6 +959,22 @@ static void _httpd_endpoint_static(tf_http_request_t* request) | ||||
| 	tf_file_stat(task, path, _httpd_endpoint_static_stat, request); | ||||
| } | ||||
|  | ||||
| static void _httpd_endpoint_add_slash(tf_http_request_t* request) | ||||
| { | ||||
| 	const char* host = tf_http_request_get_header(request, "x-forwarded-host"); | ||||
| 	if (!host) | ||||
| 	{ | ||||
| 		host = tf_http_request_get_header(request, "host"); | ||||
| 	} | ||||
| 	char url[1024]; | ||||
| 	snprintf(url, sizeof(url), "%s%s%s/", request->is_tls ? "https://" : "http://", host, request->path); | ||||
| 	const char* headers[] = { | ||||
| 		"Location", | ||||
| 		url, | ||||
| 	}; | ||||
| 	tf_http_respond(request, 303, headers, tf_countof(headers) / 2, "", 0); | ||||
| } | ||||
|  | ||||
| typedef struct _user_app_t | ||||
| { | ||||
| 	const char* user; | ||||
| @@ -1018,6 +1025,186 @@ static user_app_t* _parse_user_app_from_path(const char* path, const char* expec | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| typedef struct _app_blob_t | ||||
| { | ||||
| 	tf_http_request_t* request; | ||||
| 	bool found; | ||||
| 	bool not_modified; | ||||
| 	bool use_handler; | ||||
| 	void* data; | ||||
| 	size_t size; | ||||
| 	char app_blob_id[k_blob_id_len]; | ||||
| 	const char* file; | ||||
| 	user_app_t* user_app; | ||||
| 	char etag[256]; | ||||
| } app_blob_t; | ||||
|  | ||||
| static void _httpd_endpoint_app_blob_work(tf_ssb_t* ssb, void* user_data) | ||||
| { | ||||
| 	app_blob_t* data = user_data; | ||||
| 	tf_http_request_t* request = data->request; | ||||
| 	if (request->path[0] == '/' && request->path[1] == '~') | ||||
| 	{ | ||||
| 		const char* last_slash = strchr(request->path + 1, '/'); | ||||
| 		if (last_slash) | ||||
| 		{ | ||||
| 			last_slash = strchr(last_slash + 1, '/'); | ||||
| 		} | ||||
| 		data->user_app = last_slash ? _parse_user_app_from_path(request->path, last_slash) : NULL; | ||||
| 		if (data->user_app) | ||||
| 		{ | ||||
| 			size_t path_length = strlen("path:") + strlen(data->user_app->app) + 1; | ||||
| 			char* app_path = tf_malloc(path_length); | ||||
| 			snprintf(app_path, path_length, "path:%s", data->user_app->app); | ||||
| 			const char* value = tf_ssb_db_get_property(ssb, data->user_app->user, app_path); | ||||
| 			snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%s", value); | ||||
| 			tf_free(app_path); | ||||
| 			tf_free((void*)value); | ||||
| 			data->file = last_slash + 1; | ||||
| 		} | ||||
| 	} | ||||
| 	else if (request->path[0] == '/' && request->path[1] == '&') | ||||
| 	{ | ||||
| 		const char* end = strstr(request->path, ".sha256/"); | ||||
| 		if (end) | ||||
| 		{ | ||||
| 			snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%.*s", (int)(end + strlen(".sha256") - request->path - 1), request->path + 1); | ||||
| 			data->file = end + strlen(".sha256/"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	char* app_blob = NULL; | ||||
| 	size_t app_blob_size = 0; | ||||
| 	if (*data->app_blob_id && tf_ssb_db_blob_get(ssb, data->app_blob_id, (uint8_t**)&app_blob, &app_blob_size)) | ||||
| 	{ | ||||
| 		JSMallocFunctions funcs = { 0 }; | ||||
| 		tf_get_js_malloc_functions(&funcs); | ||||
| 		JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL); | ||||
| 		JSContext* context = JS_NewContext(runtime); | ||||
|  | ||||
| 		JSValue app_object = JS_ParseJSON(context, app_blob, app_blob_size, NULL); | ||||
| 		JSValue files = JS_GetPropertyStr(context, app_object, "files"); | ||||
| 		JSValue blob_id = JS_GetPropertyStr(context, files, data->file); | ||||
| 		if (JS_IsUndefined(blob_id)) | ||||
| 		{ | ||||
| 			blob_id = JS_GetPropertyStr(context, files, "handler.js"); | ||||
| 			if (!JS_IsUndefined(blob_id)) | ||||
| 			{ | ||||
| 				data->use_handler = true; | ||||
| 			} | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			const char* blob_id_str = JS_ToCString(context, blob_id); | ||||
| 			if (blob_id_str) | ||||
| 			{ | ||||
| 				snprintf(data->etag, sizeof(data->etag), "\"%s\"", blob_id_str); | ||||
| 				const char* match = tf_http_request_get_header(data->request, "if-none-match"); | ||||
| 				if (match && strcmp(match, data->etag) == 0) | ||||
| 				{ | ||||
| 					data->not_modified = true; | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					data->found = tf_ssb_db_blob_get(ssb, blob_id_str, (uint8_t**)&data->data, &data->size); | ||||
| 				} | ||||
| 			} | ||||
| 			JS_FreeCString(context, blob_id_str); | ||||
| 		} | ||||
| 		JS_FreeValue(context, blob_id); | ||||
| 		JS_FreeValue(context, files); | ||||
| 		JS_FreeValue(context, app_object); | ||||
|  | ||||
| 		JS_FreeContext(context); | ||||
| 		JS_FreeRuntime(runtime); | ||||
| 		tf_free(app_blob); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| static void _httpd_call_app_handler(tf_ssb_t* ssb, tf_http_request_t* request, const char* app_blob_id, const char* path, const char* package_owner, const char* app) | ||||
| { | ||||
| 	JSContext* context = tf_ssb_get_context(ssb); | ||||
| 	JSValue global = JS_GetGlobalObject(context); | ||||
| 	JSValue exports = JS_GetPropertyStr(context, global, "exports"); | ||||
| 	JSValue call_app_handler = JS_GetPropertyStr(context, exports, "callAppHandler"); | ||||
|  | ||||
| 	JSValue response = _httpd_make_response_object(context, request); | ||||
| 	tf_http_request_ref(request); | ||||
| 	JSValue handler_blob_id = JS_NewString(context, app_blob_id); | ||||
| 	JSValue path_value = JS_NewString(context, path); | ||||
| 	JSValue package_owner_value = JS_NewString(context, package_owner); | ||||
| 	JSValue app_value = JS_NewString(context, app); | ||||
|  | ||||
| 	JSValue args[] = { | ||||
| 		response, | ||||
| 		handler_blob_id, | ||||
| 		path_value, | ||||
| 		JS_UNDEFINED, | ||||
| 		JS_UNDEFINED, | ||||
| 		package_owner_value, | ||||
| 		app_value, | ||||
| 	}; | ||||
|  | ||||
| 	JSValue result = JS_Call(context, call_app_handler, JS_NULL, tf_countof(args), args); | ||||
| 	tf_util_report_error(context, result); | ||||
| 	JS_FreeValue(context, result); | ||||
|  | ||||
| 	JS_FreeValue(context, app_value); | ||||
| 	JS_FreeValue(context, package_owner_value); | ||||
| 	JS_FreeValue(context, handler_blob_id); | ||||
| 	JS_FreeValue(context, path_value); | ||||
| 	JS_FreeValue(context, response); | ||||
| 	JS_FreeValue(context, call_app_handler); | ||||
| 	JS_FreeValue(context, exports); | ||||
| 	JS_FreeValue(context, global); | ||||
| } | ||||
|  | ||||
| static void _httpd_endpoint_app_blob_after_work(tf_ssb_t* ssb, int status, void* user_data) | ||||
| { | ||||
| 	app_blob_t* data = user_data; | ||||
| 	if (data->not_modified) | ||||
| 	{ | ||||
| 		tf_http_respond(data->request, 304, NULL, 0, NULL, 0); | ||||
| 	} | ||||
| 	else if (data->use_handler) | ||||
| 	{ | ||||
| 		_httpd_call_app_handler(ssb, data->request, data->app_blob_id, data->file, data->user_app->user, data->user_app->app); | ||||
| 	} | ||||
| 	else if (data->found) | ||||
| 	{ | ||||
| 		const char* mime_type = _ext_to_content_type(strrchr(data->request->path, '.'), false); | ||||
| 		if (!mime_type) | ||||
| 		{ | ||||
| 			mime_type = _httpd_mime_type_from_magic_bytes(data->data, data->size); | ||||
| 		} | ||||
| 		const char* headers[] = { | ||||
| 			"Access-Control-Allow-Origin", | ||||
| 			"*", | ||||
| 			"Content-Security-Policy", | ||||
| 			"sandbox allow-downloads allow-top-navigation-by-user-activation", | ||||
| 			"Content-Type", | ||||
| 			mime_type ? mime_type : "application/binary", | ||||
| 			"etag", | ||||
| 			data->etag, | ||||
| 		}; | ||||
| 		tf_http_respond(data->request, 200, headers, tf_countof(headers) / 2, data->data, data->size); | ||||
| 	} | ||||
| 	tf_free(data->user_app); | ||||
| 	tf_free(data->data); | ||||
| 	tf_http_request_unref(data->request); | ||||
| 	tf_free(data); | ||||
| } | ||||
|  | ||||
| static void _httpd_endpoint_app_blob(tf_http_request_t* request) | ||||
| { | ||||
| 	tf_http_request_ref(request); | ||||
| 	tf_task_t* task = request->user_data; | ||||
| 	tf_ssb_t* ssb = tf_task_get_ssb(task); | ||||
| 	app_blob_t* data = tf_malloc(sizeof(app_blob_t)); | ||||
| 	*data = (app_blob_t) { .request = request }; | ||||
| 	tf_ssb_run_work(ssb, _httpd_endpoint_app_blob_work, _httpd_endpoint_app_blob_after_work, data); | ||||
| } | ||||
|  | ||||
| typedef struct _view_t | ||||
| { | ||||
| 	tf_http_request_t* request; | ||||
| @@ -1099,7 +1286,7 @@ static void _httpd_endpoint_view_after_work(tf_ssb_t* ssb, int status, void* use | ||||
| 		"Content-Security-Policy", | ||||
| 		"sandbox allow-downloads allow-top-navigation-by-user-activation", | ||||
| 		"Content-Type", | ||||
| 		view->data ? _httpd_mime_type_from_magic_bytes_internal(view->data, view->size) : "text/plain", | ||||
| 		view->data ? _httpd_mime_type_from_magic_bytes(view->data, view->size) : "text/plain", | ||||
| 		filename ? "Content-Disposition" : NULL, | ||||
| 		filename ? content_disposition : NULL, | ||||
| 	}; | ||||
| @@ -2101,11 +2288,16 @@ void tf_httpd_register(JSContext* context) | ||||
| 	tf_http_add_handler(http, "/speedscope/*", _httpd_endpoint_static, NULL, task); | ||||
| 	tf_http_add_handler(http, "/static/*", _httpd_endpoint_static, NULL, task); | ||||
| 	tf_http_add_handler(http, "/.well-known/*", _httpd_endpoint_static, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~*/*/", _httpd_endpoint_static, NULL, task); | ||||
| 	tf_http_add_handler(http, "/&*.sha256", _httpd_endpoint_add_slash, NULL, task); | ||||
| 	tf_http_add_handler(http, "/&*.sha256/", _httpd_endpoint_static, NULL, task); | ||||
| 	tf_http_add_handler(http, "/*/view", _httpd_endpoint_view, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~*/*/save", _httpd_endpoint_save, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~*/*/delete", _httpd_endpoint_delete, NULL, task); | ||||
| 	tf_http_add_handler(http, "/&*.sha256/view", _httpd_endpoint_view, NULL, task); | ||||
| 	tf_http_add_handler(http, "/&*.sha256/*", _httpd_endpoint_app_blob, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~{word}/{word}", _httpd_endpoint_add_slash, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~{word}/{word}/", _httpd_endpoint_static, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~{word}/{word}/save", _httpd_endpoint_save, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~{word}/{word}/delete", _httpd_endpoint_delete, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~{word}/{word}/view", _httpd_endpoint_view, NULL, task); | ||||
| 	tf_http_add_handler(http, "/~{word}/{word}/*", _httpd_endpoint_app_blob, NULL, task); | ||||
| 	tf_http_add_handler(http, "/save", _httpd_endpoint_save, NULL, task); | ||||
|  | ||||
| 	tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL); | ||||
| @@ -2118,13 +2310,10 @@ void tf_httpd_register(JSContext* context) | ||||
| 	tf_http_add_handler(http, "/login/logout", _httpd_endpoint_logout, NULL, task); | ||||
| 	tf_http_add_handler(http, "/login", _httpd_endpoint_login, NULL, task); | ||||
|  | ||||
| 	JS_SetPropertyStr(context, httpd, "handlers", JS_NewObject(context)); | ||||
| 	JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_endpoint_all, "all", 2)); | ||||
| 	JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_endpoint_start, "start", 2)); | ||||
| 	JS_SetPropertyStr(context, httpd, "set_http_redirect", JS_NewCFunction(context, _httpd_set_http_redirect, "set_http_redirect", 1)); | ||||
| 	JS_SetPropertyStr(context, httpd, "auth_query", JS_NewCFunction(context, _httpd_auth_query, "auth_query", 1)); | ||||
| 	JS_SetPropertyStr(context, httpd, "mime_type_from_magic_bytes", JS_NewCFunction(context, _httpd_mime_type_from_magic_bytes, "mime_type_from_magic_bytes", 1)); | ||||
| 	JS_SetPropertyStr(context, httpd, "mime_type_from_extension", JS_NewCFunction(context, _httpd_mime_type_from_extension, "mime_type_from_extension", 1)); | ||||
| 	JS_SetPropertyStr(context, global, "httpd", httpd); | ||||
| 	JS_FreeValue(context, global); | ||||
| } | ||||
|   | ||||
| @@ -1869,7 +1869,6 @@ void tf_task_destroy(tf_task_t* task) | ||||
| 	{ | ||||
| 		JSValue global = JS_GetGlobalObject(task->_context); | ||||
| 		JS_SetPropertyStr(task->_context, global, "httpd", JS_UNDEFINED); | ||||
| 		JS_SetPropertyStr(task->_context, global, "gProcesses", JS_NewObject(task->_context)); | ||||
| 		JS_FreeValue(task->_context, global); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -915,6 +915,7 @@ static void _test_pattern(const tf_test_options_t* options) | ||||
| 	assert(tf_http_pattern_matches("/~{word}/*", "/~core/test")); | ||||
| 	assert(tf_http_pattern_matches("/~{word}/{word}/", "/~core/test/")); | ||||
| 	assert(tf_http_pattern_matches("/~{word}/{word}", "/~core/test")); | ||||
| 	assert(!tf_http_pattern_matches("/~{word}/{word}", "/~foo/bar/baz")); | ||||
| } | ||||
|  | ||||
| static void _test_auto_process_exit(uv_process_t* process, int64_t status, int termination_signal) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user