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 gProcesses = {};
|
||||||
let gStatsTimer = false;
|
let gStatsTimer = false;
|
||||||
|
|
||||||
const k_content_security_policy =
|
|
||||||
'sandbox allow-downloads allow-top-navigation-by-user-activation';
|
|
||||||
|
|
||||||
const k_global_settings = {
|
const k_global_settings = {
|
||||||
index: {
|
index: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -812,206 +809,6 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
return process;
|
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 () {
|
ssb.addEventListener('message', function () {
|
||||||
broadcastEvent('onMessage', [...arguments]);
|
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
|
* TODOC
|
||||||
*/
|
*/
|
||||||
@ -1072,16 +936,6 @@ loadSettings()
|
|||||||
httpd.set_http_redirect(settings.http_redirect);
|
httpd.set_http_redirect(settings.http_redirect);
|
||||||
}
|
}
|
||||||
httpd.all('/app/socket', app.socket);
|
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);
|
let port = httpd.start(tildefriends.http_port);
|
||||||
if (tildefriends.args.out_http_port_file) {
|
if (tildefriends.args.out_http_port_file) {
|
||||||
print('Writing the 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)
|
bool tf_http_pattern_matches(const char* pattern, const char* path)
|
||||||
{
|
{
|
||||||
if (!pattern || !*pattern)
|
if (!*pattern && !*path)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -156,12 +156,14 @@ bool tf_http_pattern_matches(const char* pattern, const char* path)
|
|||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (((path[j] >= 'a' && path[j] <= 'z') || (path[j] >= 'A' && path[j] <= 'Z') || (path[j] >= '0' && path[j] <= '9')) &&
|
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))
|
|
||||||
{
|
{
|
||||||
return true;
|
if (tf_http_pattern_matches(pattern + i + k_word_len, path + j + 1))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!path[j])
|
else
|
||||||
{
|
{
|
||||||
break;
|
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);
|
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)
|
static void _httpd_callback_internal(tf_http_request_t* request, bool is_websocket)
|
||||||
{
|
{
|
||||||
http_handler_data_t* data = request->user_data;
|
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, client, "tls", request->is_tls ? JS_TRUE : JS_FALSE);
|
||||||
JS_SetPropertyStr(context, request_object, "client", client);
|
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. */
|
/* The ref is owned by the JS object and will be released by the finalizer. */
|
||||||
tf_http_request_ref(request);
|
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[] = {
|
JSValue args[] = {
|
||||||
request_object,
|
request_object,
|
||||||
response_object,
|
response_object,
|
||||||
@ -560,7 +566,7 @@ static bool _magic_bytes_match(const magic_bytes_t* magic, const uint8_t* actual
|
|||||||
return true;
|
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";
|
const char* type = "application/binary";
|
||||||
if (bytes)
|
if (bytes)
|
||||||
@ -637,13 +643,6 @@ static const char* _httpd_mime_type_from_magic_bytes_internal(const uint8_t* byt
|
|||||||
return type;
|
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)
|
static const char* _ext_to_content_type(const char* ext, bool use_fallback)
|
||||||
{
|
{
|
||||||
if (ext)
|
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;
|
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)
|
static void _httpd_finalizer(JSRuntime* runtime, JSValue value)
|
||||||
{
|
{
|
||||||
tf_http_t* http = JS_GetOpaque(value, _httpd_class_id);
|
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);
|
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
|
typedef struct _user_app_t
|
||||||
{
|
{
|
||||||
const char* user;
|
const char* user;
|
||||||
@ -1018,6 +1025,186 @@ static user_app_t* _parse_user_app_from_path(const char* path, const char* expec
|
|||||||
return result;
|
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
|
typedef struct _view_t
|
||||||
{
|
{
|
||||||
tf_http_request_t* request;
|
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",
|
"Content-Security-Policy",
|
||||||
"sandbox allow-downloads allow-top-navigation-by-user-activation",
|
"sandbox allow-downloads allow-top-navigation-by-user-activation",
|
||||||
"Content-Type",
|
"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,
|
||||||
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, "/speedscope/*", _httpd_endpoint_static, NULL, task);
|
||||||
tf_http_add_handler(http, "/static/*", _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, "/.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, "/&*.sha256/", _httpd_endpoint_static, NULL, task);
|
||||||
tf_http_add_handler(http, "/*/view", _httpd_endpoint_view, NULL, task);
|
tf_http_add_handler(http, "/&*.sha256/view", _httpd_endpoint_view, NULL, task);
|
||||||
tf_http_add_handler(http, "/~*/*/save", _httpd_endpoint_save, NULL, task);
|
tf_http_add_handler(http, "/&*.sha256/*", _httpd_endpoint_app_blob, NULL, task);
|
||||||
tf_http_add_handler(http, "/~*/*/delete", _httpd_endpoint_delete, 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, "/save", _httpd_endpoint_save, NULL, task);
|
||||||
|
|
||||||
tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL);
|
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/logout", _httpd_endpoint_logout, NULL, task);
|
||||||
tf_http_add_handler(http, "/login", _httpd_endpoint_login, 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, "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, "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, "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, "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_SetPropertyStr(context, global, "httpd", httpd);
|
||||||
JS_FreeValue(context, global);
|
JS_FreeValue(context, global);
|
||||||
}
|
}
|
||||||
|
@ -1869,7 +1869,6 @@ void tf_task_destroy(tf_task_t* task)
|
|||||||
{
|
{
|
||||||
JSValue global = JS_GetGlobalObject(task->_context);
|
JSValue global = JS_GetGlobalObject(task->_context);
|
||||||
JS_SetPropertyStr(task->_context, global, "httpd", JS_UNDEFINED);
|
JS_SetPropertyStr(task->_context, global, "httpd", JS_UNDEFINED);
|
||||||
JS_SetPropertyStr(task->_context, global, "gProcesses", JS_NewObject(task->_context));
|
|
||||||
JS_FreeValue(task->_context, global);
|
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}/*", "/~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}", "/~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)
|
static void _test_auto_process_exit(uv_process_t* process, int64_t status, int termination_signal)
|
||||||
|
Loading…
Reference in New Issue
Block a user