forked from cory/tildefriends
		
	Move the auth handler out of JS. #7
This commit is contained in:
		
							
								
								
									
										227
									
								
								core/auth.js
									
									
									
									
									
								
							
							
						
						
									
										227
									
								
								core/auth.js
									
									
									
									
									
								
							@@ -1,8 +1,6 @@
 | 
			
		||||
import * as core from './core.js';
 | 
			
		||||
import * as form from './form.js';
 | 
			
		||||
 | 
			
		||||
let gDatabase = new Database('auth');
 | 
			
		||||
 | 
			
		||||
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -106,60 +104,6 @@ function readSession(session) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check the provided password matches the hash
 | 
			
		||||
 * @param {string} password
 | 
			
		||||
 * @param {string} hash bcrypt hash
 | 
			
		||||
 * @returns true if the password matches the hash
 | 
			
		||||
 */
 | 
			
		||||
function verifyPassword(password, hash) {
 | 
			
		||||
	return bCrypt.hashpw(password, hash) === hash;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hashes a password
 | 
			
		||||
 * @param {string} password
 | 
			
		||||
 * @returns {string} TODOC
 | 
			
		||||
 */
 | 
			
		||||
function hashPassword(password) {
 | 
			
		||||
	let salt = bCrypt.gensalt(12);
 | 
			
		||||
	return bCrypt.hashpw(password, salt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if there is an administrator on the instance
 | 
			
		||||
 * @returns TODOC
 | 
			
		||||
 */
 | 
			
		||||
function noAdministrator() {
 | 
			
		||||
	return (
 | 
			
		||||
		!core.globalSettings ||
 | 
			
		||||
		!core.globalSettings.permissions ||
 | 
			
		||||
		!Object.keys(core.globalSettings.permissions).some(function (name) {
 | 
			
		||||
			return (
 | 
			
		||||
				core.globalSettings.permissions[name].indexOf('administration') != -1
 | 
			
		||||
			);
 | 
			
		||||
		})
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Makes a user an administrator
 | 
			
		||||
 * @param {string} name the user's name
 | 
			
		||||
 */
 | 
			
		||||
function makeAdministrator(name) {
 | 
			
		||||
	if (!core.globalSettings.permissions) {
 | 
			
		||||
		core.globalSettings.permissions = {};
 | 
			
		||||
	}
 | 
			
		||||
	if (!core.globalSettings.permissions[name]) {
 | 
			
		||||
		core.globalSettings.permissions[name] = [];
 | 
			
		||||
	}
 | 
			
		||||
	if (core.globalSettings.permissions[name].indexOf('administration') == -1) {
 | 
			
		||||
		core.globalSettings.permissions[name].push('administration');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	core.setGlobalSettings(core.globalSettings);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} headers most likely an object
 | 
			
		||||
@@ -181,175 +125,6 @@ function getCookies(headers) {
 | 
			
		||||
	return cookies;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates a username
 | 
			
		||||
 * @param {string} name
 | 
			
		||||
 * @returns false | boolean[] ?
 | 
			
		||||
 */
 | 
			
		||||
function isNameValid(name) {
 | 
			
		||||
	// TODO(tasiaiso): convert this into a regex
 | 
			
		||||
	let c = name.charAt(0);
 | 
			
		||||
	return (
 | 
			
		||||
		((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
 | 
			
		||||
		name
 | 
			
		||||
			.split()
 | 
			
		||||
			.map(
 | 
			
		||||
				(x) =>
 | 
			
		||||
					x >= ('a' && x <= 'z') ||
 | 
			
		||||
					x >= ('A' && x <= 'Z') ||
 | 
			
		||||
					x >= ('0' && x <= '9')
 | 
			
		||||
			)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Request handler ?
 | 
			
		||||
 * @param {*} request TODOC
 | 
			
		||||
 * @param {*} response
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function handler(request, response) {
 | 
			
		||||
	// TODO(tasiaiso): split this function
 | 
			
		||||
	let session = getCookies(request.headers).session;
 | 
			
		||||
	if (request.uri == '/login') {
 | 
			
		||||
		let formData = form.decodeForm(request.query);
 | 
			
		||||
		if (query(request.headers)?.permissions?.authenticated) {
 | 
			
		||||
			if (formData.return) {
 | 
			
		||||
				response.writeHead(303, {Location: formData.return});
 | 
			
		||||
			} else {
 | 
			
		||||
				response.writeHead(303, {
 | 
			
		||||
					Location:
 | 
			
		||||
						(request.client.tls ? 'https://' : 'http://') +
 | 
			
		||||
						request.headers.host +
 | 
			
		||||
						'/',
 | 
			
		||||
					'Content-Length': '0',
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			response.end();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let sessionIsNew = false;
 | 
			
		||||
		let loginError;
 | 
			
		||||
 | 
			
		||||
		if (request.method == 'POST' || formData.submit) {
 | 
			
		||||
			sessionIsNew = true;
 | 
			
		||||
			formData = form.decodeForm(utf8Decode(request.body), formData);
 | 
			
		||||
			if (formData.submit == 'Login') {
 | 
			
		||||
				let account = gDatabase.get('user:' + formData.name);
 | 
			
		||||
				account = account ? JSON.parse(account) : account;
 | 
			
		||||
				if (formData.register == '1') {
 | 
			
		||||
					if (
 | 
			
		||||
						!account &&
 | 
			
		||||
						isNameValid(formData.name) &&
 | 
			
		||||
						formData.password == formData.confirm
 | 
			
		||||
					) {
 | 
			
		||||
						let users = new Set();
 | 
			
		||||
						let users_original = gDatabase.get('users');
 | 
			
		||||
						try {
 | 
			
		||||
							users = new Set(JSON.parse(users_original));
 | 
			
		||||
						} catch {}
 | 
			
		||||
						if (!users.has(formData.name)) {
 | 
			
		||||
							users.add(formData.name);
 | 
			
		||||
						}
 | 
			
		||||
						users = JSON.stringify([...users].sort());
 | 
			
		||||
						if (users !== users_original) {
 | 
			
		||||
							gDatabase.set('users', users);
 | 
			
		||||
						}
 | 
			
		||||
						session = makeJwt({name: formData.name});
 | 
			
		||||
						account = {password: hashPassword(formData.password)};
 | 
			
		||||
						gDatabase.set('user:' + formData.name, JSON.stringify(account));
 | 
			
		||||
						if (noAdministrator()) {
 | 
			
		||||
							makeAdministrator(formData.name);
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						loginError = 'Error registering account.';
 | 
			
		||||
					}
 | 
			
		||||
				} else if (formData.change == '1') {
 | 
			
		||||
					if (
 | 
			
		||||
						account &&
 | 
			
		||||
						isNameValid(formData.name) &&
 | 
			
		||||
						formData.new_password == formData.confirm &&
 | 
			
		||||
						verifyPassword(formData.password, account.password)
 | 
			
		||||
					) {
 | 
			
		||||
						session = makeJwt({name: formData.name});
 | 
			
		||||
						account = {password: hashPassword(formData.new_password)};
 | 
			
		||||
						gDatabase.set('user:' + formData.name, JSON.stringify(account));
 | 
			
		||||
					} else {
 | 
			
		||||
						loginError = 'Error changing password.';
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					if (
 | 
			
		||||
						account &&
 | 
			
		||||
						account.password &&
 | 
			
		||||
						verifyPassword(formData.password, account.password)
 | 
			
		||||
					) {
 | 
			
		||||
						session = makeJwt({name: formData.name});
 | 
			
		||||
						if (noAdministrator()) {
 | 
			
		||||
							makeAdministrator(formData.name);
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						loginError = 'Invalid username or password.';
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				// Proceed as Guest
 | 
			
		||||
				session = makeJwt({name: 'guest'});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
 | 
			
		||||
		let entry = readSession(session);
 | 
			
		||||
		if (entry && formData.return) {
 | 
			
		||||
			response.writeHead(303, {
 | 
			
		||||
				Location: formData.return,
 | 
			
		||||
				'Set-Cookie': cookie,
 | 
			
		||||
			});
 | 
			
		||||
			response.end();
 | 
			
		||||
		} else {
 | 
			
		||||
			File.readFile('core/auth.html')
 | 
			
		||||
				.then(function (data) {
 | 
			
		||||
					let html = utf8Decode(data);
 | 
			
		||||
					let auth_data = {
 | 
			
		||||
						session_is_new: sessionIsNew,
 | 
			
		||||
						name: entry?.name,
 | 
			
		||||
						error: loginError,
 | 
			
		||||
						code_of_conduct: core.globalSettings.code_of_conduct,
 | 
			
		||||
						have_administrator: !noAdministrator(),
 | 
			
		||||
					};
 | 
			
		||||
					html = utf8Encode(
 | 
			
		||||
						html.replace('$AUTH_DATA', JSON.stringify(auth_data))
 | 
			
		||||
					);
 | 
			
		||||
					response.writeHead(200, {
 | 
			
		||||
						'Content-Type': 'text/html; charset=utf-8',
 | 
			
		||||
						'Set-Cookie': cookie,
 | 
			
		||||
						'Content-Length': html.length,
 | 
			
		||||
					});
 | 
			
		||||
					response.end(html);
 | 
			
		||||
				})
 | 
			
		||||
				.catch(function (error) {
 | 
			
		||||
					response.writeHead(404, {
 | 
			
		||||
						'Content-Type': 'text/plain; charset=utf-8',
 | 
			
		||||
						Connection: 'close',
 | 
			
		||||
					});
 | 
			
		||||
					response.end('404 File not found');
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	} else if (request.uri == '/login/logout') {
 | 
			
		||||
		response.writeHead(303, {
 | 
			
		||||
			'Set-Cookie': `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`,
 | 
			
		||||
			Location: '/login' + (request.query ? '?' + request.query : ''),
 | 
			
		||||
		});
 | 
			
		||||
		response.end();
 | 
			
		||||
	} else {
 | 
			
		||||
		response.writeHead(200, {
 | 
			
		||||
			'Content-Type': 'text/plain; charset=utf-8',
 | 
			
		||||
			Connection: 'close',
 | 
			
		||||
		});
 | 
			
		||||
		response.end('Hello, ' + request.client.peerName + '.');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets a user's permissions based on it's session ?
 | 
			
		||||
 * @param {*} session TODOC
 | 
			
		||||
@@ -417,4 +192,4 @@ function makeRefresh(credentials) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {handler, query, makeRefresh};
 | 
			
		||||
export {query, makeRefresh};
 | 
			
		||||
 
 | 
			
		||||
@@ -1334,8 +1334,6 @@ loadSettings()
 | 
			
		||||
		if (tildefriends.https_port && gGlobalSettings.http_redirect) {
 | 
			
		||||
			httpd.set_http_redirect(gGlobalSettings.http_redirect);
 | 
			
		||||
		}
 | 
			
		||||
		httpd.all('/login', auth.handler);
 | 
			
		||||
		httpd.all('/login/logout', auth.handler);
 | 
			
		||||
		httpd.all('/app/socket', app.socket);
 | 
			
		||||
		httpd.all('', function default_http_handler(request, response) {
 | 
			
		||||
			let match;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								src/http.c
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								src/http.c
									
									
									
									
									
								
							@@ -1030,3 +1030,48 @@ void* tf_http_get_user_data(tf_http_t* http)
 | 
			
		||||
{
 | 
			
		||||
	return http->user_data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char* tf_http_get_cookie(const char* cookie_header, const char* name)
 | 
			
		||||
{
 | 
			
		||||
	if (!cookie_header)
 | 
			
		||||
	{
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	int name_start = 0;
 | 
			
		||||
	int equals = 0;
 | 
			
		||||
	for (int i = 0; ; i++)
 | 
			
		||||
	{
 | 
			
		||||
		if (cookie_header[i] == '=')
 | 
			
		||||
		{
 | 
			
		||||
			equals = i;
 | 
			
		||||
		}
 | 
			
		||||
		else if (cookie_header[i] == ',' || cookie_header[i] == ';' || cookie_header[i] == '\0')
 | 
			
		||||
		{
 | 
			
		||||
			if (equals > name_start &&
 | 
			
		||||
				strncmp(cookie_header + name_start, name, equals - name_start) == 0 &&
 | 
			
		||||
				(int)strlen(name) == equals - name_start)
 | 
			
		||||
			{
 | 
			
		||||
				int length = i - equals - 1;
 | 
			
		||||
				char* result = tf_malloc(length + 1);
 | 
			
		||||
				memcpy(result, cookie_header + equals + 1, length);
 | 
			
		||||
				result[length] = '\0';
 | 
			
		||||
				return result;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (cookie_header[i] == '\0')
 | 
			
		||||
			{
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				name_start = i + 1;
 | 
			
		||||
				while (cookie_header[name_start] == ' ')
 | 
			
		||||
				{
 | 
			
		||||
					name_start++;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -196,6 +196,15 @@ void tf_http_request_unref(tf_http_request_t* request);
 | 
			
		||||
*/
 | 
			
		||||
const char* tf_http_request_get_header(tf_http_request_t* request, const char* name);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
** Get a cookie value from request headers.
 | 
			
		||||
** @param cookie_header The value of the "Cookie" header of the form
 | 
			
		||||
** "name1=value1; name2=value2".
 | 
			
		||||
** @param name The cookie name.
 | 
			
		||||
** @return The cookie value, if found, or NULL.  Must be freed with tf_free().
 | 
			
		||||
*/
 | 
			
		||||
const char* tf_http_get_cookie(const char* cookie_header, const char* name);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
** Send a websocket message.
 | 
			
		||||
** @param request The HTTP request which was previously updated to a websocket
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										755
									
								
								src/httpd.js.c
									
									
									
									
									
								
							
							
						
						
									
										755
									
								
								src/httpd.js.c
									
									
									
									
									
								
							@@ -4,16 +4,26 @@
 | 
			
		||||
#include "http.h"
 | 
			
		||||
#include "log.h"
 | 
			
		||||
#include "mem.h"
 | 
			
		||||
#include "ssb.h"
 | 
			
		||||
#include "ssb.db.h"
 | 
			
		||||
#include "task.h"
 | 
			
		||||
#include "tlscontext.js.h"
 | 
			
		||||
#include "trace.h"
 | 
			
		||||
#include "util.js.h"
 | 
			
		||||
 | 
			
		||||
#include "ow-crypt.h"
 | 
			
		||||
 | 
			
		||||
#include "picohttpparser.h"
 | 
			
		||||
 | 
			
		||||
#include "sodium/crypto_sign.h"
 | 
			
		||||
#include "sodium/utils.h"
 | 
			
		||||
 | 
			
		||||
#include "sqlite3.h"
 | 
			
		||||
 | 
			
		||||
#include <assert.h>
 | 
			
		||||
#include <stdlib.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
#include <time.h>
 | 
			
		||||
 | 
			
		||||
#include <openssl/sha.h>
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +33,8 @@
 | 
			
		||||
 | 
			
		||||
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
 | 
			
		||||
 | 
			
		||||
const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
 | 
			
		||||
 | 
			
		||||
static JSClassID _httpd_class_id;
 | 
			
		||||
@@ -732,6 +744,746 @@ static void _httpd_endpoint_debug(tf_http_request_t* request)
 | 
			
		||||
	tf_free(response);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char** _form_data_decode(const char* data, int length)
 | 
			
		||||
{
 | 
			
		||||
	int key_max = 1;
 | 
			
		||||
	for (int i = 0; i < length; i++)
 | 
			
		||||
	{
 | 
			
		||||
		if (data[i] == '&')
 | 
			
		||||
		{
 | 
			
		||||
			key_max++;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	int write_length = length + 1;
 | 
			
		||||
	char** result = tf_malloc(sizeof(const char*) * (key_max + 1) * 2 + write_length);
 | 
			
		||||
	char* result_buffer = ((char*)result) + sizeof(const char*) * (key_max + 1) * 2;
 | 
			
		||||
 | 
			
		||||
	char* write_pos = result_buffer;
 | 
			
		||||
	int count = 0;
 | 
			
		||||
	int i = 0;
 | 
			
		||||
	while (i < length)
 | 
			
		||||
	{
 | 
			
		||||
		result[count++] = write_pos;
 | 
			
		||||
		while (i < length)
 | 
			
		||||
		{
 | 
			
		||||
			if (data[i] == '+')
 | 
			
		||||
			{
 | 
			
		||||
				*write_pos++ = ' ';
 | 
			
		||||
				i++;
 | 
			
		||||
			}
 | 
			
		||||
			else if (data[i] == '%' && i + 2 < length)
 | 
			
		||||
			{
 | 
			
		||||
				*write_pos++ = (char)strtoul((const char[]) { data[i + 1], data[i + 2], 0 }, NULL, 16);
 | 
			
		||||
				i += 3;
 | 
			
		||||
			}
 | 
			
		||||
			else if (data[i] == '=')
 | 
			
		||||
			{
 | 
			
		||||
				if (count % 2 == 0)
 | 
			
		||||
				{
 | 
			
		||||
					result[count++] = "";
 | 
			
		||||
				}
 | 
			
		||||
				i++;
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			else if (data[i] == '&')
 | 
			
		||||
			{
 | 
			
		||||
				if (count % 2 != 0)
 | 
			
		||||
				{
 | 
			
		||||
					result[count++] = "";
 | 
			
		||||
				}
 | 
			
		||||
				i++;
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				*write_pos++ = data[i++];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		*write_pos++ = '\0';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result[count++] = NULL;
 | 
			
		||||
	result[count++] = NULL;
 | 
			
		||||
 | 
			
		||||
	return (const char**)result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char* _form_data_get(const char** form_data, const char* key)
 | 
			
		||||
{
 | 
			
		||||
	for (int i = 0; form_data[i]; i += 2)
 | 
			
		||||
	{
 | 
			
		||||
		if (form_data[i] && strcmp(form_data[i], key) == 0)
 | 
			
		||||
		{
 | 
			
		||||
			return form_data[i + 1];
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return NULL;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
typedef struct _login_request_t
 | 
			
		||||
{
 | 
			
		||||
	tf_http_request_t* request;
 | 
			
		||||
	const char* session_cookie;
 | 
			
		||||
	JSValue jwt;
 | 
			
		||||
	const char* name;
 | 
			
		||||
	const char* error;
 | 
			
		||||
	const char* code_of_conduct;
 | 
			
		||||
	bool have_administrator;
 | 
			
		||||
	bool session_is_new;
 | 
			
		||||
} login_request_t;
 | 
			
		||||
 | 
			
		||||
static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
 | 
			
		||||
{
 | 
			
		||||
	login_request_t* login = user_data;
 | 
			
		||||
	tf_http_request_t* request = login->request;
 | 
			
		||||
	if (result >= 0)
 | 
			
		||||
	{
 | 
			
		||||
		const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
 | 
			
		||||
		int length = login->session_cookie ? snprintf(NULL, 0, k_pattern, login->session_cookie, k_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
 | 
			
		||||
		char* cookie = length ? tf_malloc(length + 1) : NULL;
 | 
			
		||||
		if (cookie)
 | 
			
		||||
		{
 | 
			
		||||
			snprintf(cookie, length + 1, k_pattern, login->session_cookie, k_refresh_interval, request->is_tls ? "Secure; " : "");
 | 
			
		||||
		}
 | 
			
		||||
		const char* headers[] =
 | 
			
		||||
		{
 | 
			
		||||
			"Content-Type", "text/html; charset=utf-8",
 | 
			
		||||
			"Set-Cookie", cookie ? cookie : "",
 | 
			
		||||
		};
 | 
			
		||||
		const char* replace_me = "$AUTH_DATA";
 | 
			
		||||
		const char* auth = strstr(data, replace_me);
 | 
			
		||||
		if (auth)
 | 
			
		||||
		{
 | 
			
		||||
			JSContext* context = tf_task_get_context(task);
 | 
			
		||||
			JSValue object = JS_NewObject(context);
 | 
			
		||||
			JS_SetPropertyStr(context, object, "session_is_new", JS_NewBool(context, login->session_is_new));
 | 
			
		||||
			JS_SetPropertyStr(context, object, "name", login->name ? JS_NewString(context, login->name) : JS_UNDEFINED);
 | 
			
		||||
			JS_SetPropertyStr(context, object, "error", login->error ? JS_NewString(context, login->error) : JS_UNDEFINED);
 | 
			
		||||
			JS_SetPropertyStr(context, object, "code_of_conduct", login->code_of_conduct ? JS_NewString(context, login->code_of_conduct) : JS_UNDEFINED);
 | 
			
		||||
			JS_SetPropertyStr(context, object, "have_administrator", JS_NewBool(context, login->have_administrator));
 | 
			
		||||
			JSValue object_json = JS_JSONStringify(context, object, JS_NULL, JS_NULL);
 | 
			
		||||
			size_t json_length = 0;
 | 
			
		||||
			const char* json = JS_ToCStringLen(context, &json_length, object_json);
 | 
			
		||||
 | 
			
		||||
			char* copy = tf_malloc(result + json_length);
 | 
			
		||||
			int replace_start = (auth - (const char*)data);
 | 
			
		||||
			int replace_end = (auth - (const char*)data) + (int)strlen(replace_me);
 | 
			
		||||
			memcpy(copy, data, replace_start);
 | 
			
		||||
			memcpy(copy + replace_start, json, json_length);
 | 
			
		||||
			memcpy(copy + replace_start + json_length, ((const char*)data) + replace_end, result - replace_end);
 | 
			
		||||
			tf_http_respond(request, 200, headers, tf_countof(headers) / 2, copy, replace_start + json_length + (result - replace_end));
 | 
			
		||||
			tf_free(copy);
 | 
			
		||||
 | 
			
		||||
			JS_FreeCString(context, json);
 | 
			
		||||
			JS_FreeValue(context, object_json);
 | 
			
		||||
			JS_FreeValue(context, object);
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
 | 
			
		||||
		}
 | 
			
		||||
		tf_free(cookie);
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		const char* k_payload = tf_http_status_text(404);
 | 
			
		||||
		tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
 | 
			
		||||
	}
 | 
			
		||||
	tf_http_request_unref(request);
 | 
			
		||||
	tf_free((void*)login->name);
 | 
			
		||||
	tf_free((void*)login->code_of_conduct);
 | 
			
		||||
	tf_free((void*)login->session_cookie);
 | 
			
		||||
	tf_free(login);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _string_property_equals(JSContext* context, JSValue object, const char* name, const char* value)
 | 
			
		||||
{
 | 
			
		||||
	JSValue object_value = JS_GetPropertyStr(context, object, name);
 | 
			
		||||
	const char* object_value_string = JS_ToCString(context, object_value);
 | 
			
		||||
	bool equals = object_value_string && strcmp(object_value_string, value) == 0;
 | 
			
		||||
	JS_FreeCString(context, object_value_string);
 | 
			
		||||
	JS_FreeValue(context, object_value);
 | 
			
		||||
	return equals;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void _public_key_visit(const char* identity, void* user_data)
 | 
			
		||||
{
 | 
			
		||||
	snprintf(user_data, k_id_base64_len, "%s", identity);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
 | 
			
		||||
{
 | 
			
		||||
	if (!jwt)
 | 
			
		||||
	{
 | 
			
		||||
		return JS_UNDEFINED;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	int dot[2] = { 0 };
 | 
			
		||||
	int dot_count = 0;
 | 
			
		||||
	for (int i = 0; jwt[i]; i++)
 | 
			
		||||
	{
 | 
			
		||||
		if (jwt[i] == '.')
 | 
			
		||||
		{
 | 
			
		||||
			if (dot_count >= tf_countof(dot))
 | 
			
		||||
			{
 | 
			
		||||
				return JS_UNDEFINED;
 | 
			
		||||
			}
 | 
			
		||||
			dot[dot_count++] = i;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (dot_count != 2)
 | 
			
		||||
	{
 | 
			
		||||
		return JS_UNDEFINED;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uint8_t header[256];
 | 
			
		||||
	size_t actual_length = 0;
 | 
			
		||||
	if (sodium_base642bin(header, sizeof(header), jwt, dot[0], NULL, &actual_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0)
 | 
			
		||||
	{
 | 
			
		||||
		return JS_UNDEFINED;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	JSValue header_value = JS_ParseJSON(context, (const char*)header, actual_length, NULL);
 | 
			
		||||
	bool header_valid =
 | 
			
		||||
		_string_property_equals(context, header_value, "typ", "JWT") &&
 | 
			
		||||
		_string_property_equals(context, header_value, "alg", "HS256");
 | 
			
		||||
	JS_FreeValue(context, header_value);
 | 
			
		||||
	if (!header_valid)
 | 
			
		||||
	{
 | 
			
		||||
		return JS_UNDEFINED;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tf_task_t* task = tf_task_get(context);
 | 
			
		||||
	tf_ssb_t* ssb = tf_task_get_ssb(task);
 | 
			
		||||
	char public_key_b64[k_id_base64_len] = { 0 };
 | 
			
		||||
	tf_ssb_db_identity_visit(ssb, ":auth", _public_key_visit, public_key_b64);
 | 
			
		||||
 | 
			
		||||
	const char* payload = jwt + dot[0] + 1;
 | 
			
		||||
	size_t payload_length = dot[1] - dot[0];
 | 
			
		||||
	if (!tf_ssb_hmacsha256_verify(public_key_b64, payload, payload_length, jwt + dot[1] + 1))
 | 
			
		||||
	{
 | 
			
		||||
		return JS_UNDEFINED;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uint8_t payload_bin[256];
 | 
			
		||||
	size_t actual_payload_length = 0;
 | 
			
		||||
	if (sodium_base642bin(payload_bin, sizeof(payload_bin), payload, payload_length, NULL, &actual_payload_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0)
 | 
			
		||||
	{
 | 
			
		||||
		return JS_UNDEFINED;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	JSValue parsed = JS_ParseJSON(context, (const char*)payload_bin, actual_payload_length, NULL);
 | 
			
		||||
	JSValue exp = JS_GetPropertyStr(context, parsed, "exp");
 | 
			
		||||
	int64_t exp_value = 0;
 | 
			
		||||
	JS_ToInt64(context, &exp_value, exp);
 | 
			
		||||
	if (time(NULL) >= exp_value)
 | 
			
		||||
	{
 | 
			
		||||
		JS_FreeValue(context, parsed);
 | 
			
		||||
		return JS_UNDEFINED;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return parsed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
 | 
			
		||||
{
 | 
			
		||||
	bool result = false;
 | 
			
		||||
	JSValue user = JS_GetPropertyStr(context, session, "user");
 | 
			
		||||
	const char* user_string = JS_ToCString(context, user);
 | 
			
		||||
	result = user_string && strcmp(user_string, "guest") != 0;
 | 
			
		||||
	JS_FreeCString(context, user_string);
 | 
			
		||||
	JS_FreeValue(context, user);
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _is_name_valid(const char* name)
 | 
			
		||||
{
 | 
			
		||||
	if (!name ||
 | 
			
		||||
		!((*name >= 'a' && *name <= 'z') || (*name >= 'A' && *name <= 'Z')))
 | 
			
		||||
	{
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
	for (const char* p = name; *p; p++)
 | 
			
		||||
	{
 | 
			
		||||
		bool in_range =
 | 
			
		||||
			(*p >= 'a' && *p <= 'z') ||
 | 
			
		||||
			(*p >= 'A' && *p <= 'Z') ||
 | 
			
		||||
			(*p >= '0' && *p <= '9');
 | 
			
		||||
		if (!in_range)
 | 
			
		||||
		{
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _read_account(tf_ssb_t* ssb, const char* name, char* out_passwd, size_t passwd_size)
 | 
			
		||||
{
 | 
			
		||||
	bool result = false;
 | 
			
		||||
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
 | 
			
		||||
	sqlite3_stmt* statement = NULL;
 | 
			
		||||
	if (sqlite3_prepare(db, "SELECT value ->> '$.password' FROM properties WHERE id = 'auth' AND key = 'user:' || ?", -1, &statement, NULL) == SQLITE_OK)
 | 
			
		||||
	{
 | 
			
		||||
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
 | 
			
		||||
		{
 | 
			
		||||
			if (sqlite3_step(statement) == SQLITE_ROW)
 | 
			
		||||
			{
 | 
			
		||||
				snprintf(out_passwd, passwd_size, "%s", (const char*)sqlite3_column_text(statement, 0));
 | 
			
		||||
				result = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		sqlite3_finalize(statement);
 | 
			
		||||
	}
 | 
			
		||||
	tf_ssb_release_db_reader(ssb, db);
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _set_account_password(JSContext* context, sqlite3* db, const char* name, const char* password)
 | 
			
		||||
{
 | 
			
		||||
	bool result = false;
 | 
			
		||||
	static const int k_salt_length = 12;
 | 
			
		||||
 | 
			
		||||
	char buffer[16];
 | 
			
		||||
	tf_task_t* task = tf_task_get(context);
 | 
			
		||||
	size_t bytes = uv_random(tf_task_get_loop(task), &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0;
 | 
			
		||||
	char output[7 + 22 + 1];
 | 
			
		||||
	char* salt = crypt_gensalt_rn("$2b$", k_salt_length, buffer, bytes, output, sizeof(output));
 | 
			
		||||
	char hash_output[7 + 22 + 31 + 1];
 | 
			
		||||
	char* hash = crypt_rn(password, salt, hash_output, sizeof(hash_output));
 | 
			
		||||
 | 
			
		||||
	JSValue user_entry = JS_NewObject(context);
 | 
			
		||||
	JS_SetPropertyStr(context, user_entry, "password", JS_NewString(context, hash));
 | 
			
		||||
	JSValue user_json = JS_JSONStringify(context, user_entry, JS_NULL, JS_NULL);
 | 
			
		||||
	size_t user_length = 0;
 | 
			
		||||
	const char* user_string = JS_ToCStringLen(context, &user_length, user_json);
 | 
			
		||||
 | 
			
		||||
	sqlite3_stmt* statement = NULL;
 | 
			
		||||
	if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'user:' || ?, ?)", -1, &statement, NULL) == SQLITE_OK)
 | 
			
		||||
	{
 | 
			
		||||
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK &&
 | 
			
		||||
			sqlite3_bind_text(statement, 2, user_string, user_length, NULL) == SQLITE_OK)
 | 
			
		||||
		{
 | 
			
		||||
			result = sqlite3_step(statement) == SQLITE_DONE;
 | 
			
		||||
		}
 | 
			
		||||
		sqlite3_finalize(statement);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	JS_FreeCString(context, user_string);
 | 
			
		||||
	JS_FreeValue(context, user_json);
 | 
			
		||||
	JS_FreeValue(context, user_entry);
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _register_account(tf_ssb_t* ssb, const char* name, const char* password)
 | 
			
		||||
{
 | 
			
		||||
	bool result = false;
 | 
			
		||||
	JSContext* context = tf_ssb_get_context(ssb);
 | 
			
		||||
	JSValue users_array = JS_UNDEFINED;
 | 
			
		||||
 | 
			
		||||
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
 | 
			
		||||
	sqlite3_stmt* statement = NULL;
 | 
			
		||||
	if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK)
 | 
			
		||||
	{
 | 
			
		||||
		if (sqlite3_step(statement) == SQLITE_ROW)
 | 
			
		||||
		{
 | 
			
		||||
			users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL);
 | 
			
		||||
		}
 | 
			
		||||
		sqlite3_finalize(statement);
 | 
			
		||||
	}
 | 
			
		||||
	if (JS_IsUndefined(users_array))
 | 
			
		||||
	{
 | 
			
		||||
		users_array = JS_NewArray(context);
 | 
			
		||||
	}
 | 
			
		||||
	int length = tf_util_get_length(context, users_array);
 | 
			
		||||
	JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name));
 | 
			
		||||
 | 
			
		||||
	JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL);
 | 
			
		||||
	JS_FreeValue(context, users_array);
 | 
			
		||||
	size_t value_length = 0;
 | 
			
		||||
	const char* value = JS_ToCStringLen(context, &value_length, json);
 | 
			
		||||
	if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK)
 | 
			
		||||
	{
 | 
			
		||||
		if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
 | 
			
		||||
		{
 | 
			
		||||
			result = sqlite3_step(statement) == SQLITE_DONE;
 | 
			
		||||
		}
 | 
			
		||||
		sqlite3_finalize(statement);
 | 
			
		||||
	}
 | 
			
		||||
	JS_FreeCString(context, value);
 | 
			
		||||
	JS_FreeValue(context, json);
 | 
			
		||||
 | 
			
		||||
	result = result && _set_account_password(context, db, name, password);
 | 
			
		||||
 | 
			
		||||
	tf_ssb_release_db_writer(ssb, db);
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void _visit_auth_identity(const char* identity, void* user_data)
 | 
			
		||||
{
 | 
			
		||||
	if (!*(char*)user_data)
 | 
			
		||||
	{
 | 
			
		||||
		snprintf((char*)user_data, k_id_base64_len, "%s", identity);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
 | 
			
		||||
{
 | 
			
		||||
	char id[k_id_base64_len] = { 0 };
 | 
			
		||||
	tf_ssb_db_identity_visit(ssb, ":auth", _visit_auth_identity, id);
 | 
			
		||||
	if (!*id)
 | 
			
		||||
	{
 | 
			
		||||
		uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
 | 
			
		||||
		uint8_t private_key[crypto_sign_SECRETKEYBYTES];
 | 
			
		||||
		if (tf_ssb_db_identity_create(ssb, ":auth", public_key, private_key))
 | 
			
		||||
		{
 | 
			
		||||
			tf_ssb_id_bin_to_str(id, sizeof(id), public_key);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!*id)
 | 
			
		||||
	{
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uint8_t private_key[crypto_sign_SECRETKEYBYTES];
 | 
			
		||||
	if (!tf_ssb_db_identity_get_private_key(ssb, ":auth", id, private_key, sizeof(private_key)))
 | 
			
		||||
	{
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uv_timespec64_t now = { 0 };
 | 
			
		||||
	uv_clock_gettime(UV_CLOCK_REALTIME, &now);
 | 
			
		||||
 | 
			
		||||
	JSContext* context = tf_ssb_get_context(ssb);
 | 
			
		||||
 | 
			
		||||
	const char* header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
 | 
			
		||||
	char header_base64[256];
 | 
			
		||||
	sodium_bin2base64(header_base64, sizeof(header_base64), (uint8_t*)header_json, strlen(header_json), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
 | 
			
		||||
 | 
			
		||||
	JSValue payload = JS_NewObject(context);
 | 
			
		||||
	JS_SetPropertyStr(context, payload, "name", JS_NewString(context, name));
 | 
			
		||||
	JS_SetPropertyStr(context, payload, "exp", JS_NewInt64(context, now.tv_sec * 1000 + now.tv_nsec / 1000000LL + k_refresh_interval));
 | 
			
		||||
	JSValue payload_json = JS_JSONStringify(context, payload, JS_NULL, JS_NULL);
 | 
			
		||||
	size_t payload_length = 0;
 | 
			
		||||
	const char* payload_string = JS_ToCStringLen(context, &payload_length, payload_json);
 | 
			
		||||
	char payload_base64[256];
 | 
			
		||||
	sodium_bin2base64(payload_base64, sizeof(payload_base64), (uint8_t*)payload_string, payload_length, sodium_base64_VARIANT_URLSAFE_NO_PADDING);
 | 
			
		||||
 | 
			
		||||
	char* result = NULL;
 | 
			
		||||
	uint8_t signature[crypto_sign_BYTES];
 | 
			
		||||
	unsigned long long signature_length = 0;
 | 
			
		||||
	char signature_base64[256] = { 0 };
 | 
			
		||||
	if (crypto_sign_detached(signature, &signature_length, (const uint8_t*)payload_base64, strlen(payload_base64), private_key) == 0)
 | 
			
		||||
	{
 | 
			
		||||
		sodium_bin2base64(signature_base64, sizeof(signature_base64), signature, sizeof(signature), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
 | 
			
		||||
		size_t size = strlen(header_base64) + 1 + strlen(payload_base64) + 1 + strlen(signature_base64) + 1;
 | 
			
		||||
		result = tf_malloc(size);
 | 
			
		||||
		snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	JS_FreeCString(context, payload_string);
 | 
			
		||||
	JS_FreeValue(context, payload_json);
 | 
			
		||||
	JS_FreeValue(context, payload);
 | 
			
		||||
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _verify_password(const char* password, const char* hash)
 | 
			
		||||
{
 | 
			
		||||
	char buffer[7 + 22 + 31 + 1];
 | 
			
		||||
	const char* out_hash = crypt_rn(password, hash, buffer, sizeof(buffer));
 | 
			
		||||
	return out_hash && strcmp(hash, out_hash) == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static const char* _get_property(tf_ssb_t* ssb, const char* id, const char* key)
 | 
			
		||||
{
 | 
			
		||||
	char* result = NULL;
 | 
			
		||||
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
 | 
			
		||||
	sqlite3_stmt* statement = NULL;
 | 
			
		||||
	if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
 | 
			
		||||
	{
 | 
			
		||||
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
 | 
			
		||||
			sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK)
 | 
			
		||||
		{
 | 
			
		||||
			if (sqlite3_step(statement) == SQLITE_ROW)
 | 
			
		||||
			{
 | 
			
		||||
				size_t length = sqlite3_column_bytes(statement, 0);
 | 
			
		||||
				result = tf_malloc(length + 1);
 | 
			
		||||
				memcpy(result, sqlite3_column_text(statement, 0), length);
 | 
			
		||||
				result[length] = '\0';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		sqlite3_finalize(statement);
 | 
			
		||||
	}
 | 
			
		||||
	tf_ssb_release_db_reader(ssb, db);
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool _set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
 | 
			
		||||
{
 | 
			
		||||
	bool result = false;
 | 
			
		||||
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
 | 
			
		||||
	sqlite3_stmt* statement = NULL;
 | 
			
		||||
	if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?, ?, ?)", -1, &statement, NULL) == SQLITE_OK)
 | 
			
		||||
	{
 | 
			
		||||
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
 | 
			
		||||
			sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
 | 
			
		||||
			sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
 | 
			
		||||
		{
 | 
			
		||||
			result = sqlite3_step(statement) == SQLITE_DONE;
 | 
			
		||||
		}
 | 
			
		||||
		sqlite3_finalize(statement);
 | 
			
		||||
	}
 | 
			
		||||
	tf_ssb_release_db_writer(ssb, db);
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void _httpd_endpoint_login(tf_http_request_t* request)
 | 
			
		||||
{
 | 
			
		||||
	tf_task_t* task = request->user_data;
 | 
			
		||||
	JSContext* context = tf_task_get_context(task);
 | 
			
		||||
	tf_ssb_t* ssb = tf_task_get_ssb(task);
 | 
			
		||||
	const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
 | 
			
		||||
	const char** form_data = _form_data_decode(request->query, request->query ? strlen(request->query) : 0);
 | 
			
		||||
	const char* account_name_copy = NULL;
 | 
			
		||||
	JSValue jwt = _authenticate_jwt(context, session);
 | 
			
		||||
 | 
			
		||||
	if (_session_is_authenticated_as_user(context, jwt))
 | 
			
		||||
	{
 | 
			
		||||
		const char* return_url = _form_data_get(form_data, "return");
 | 
			
		||||
		char url[1024];
 | 
			
		||||
		if (!return_url)
 | 
			
		||||
		{
 | 
			
		||||
			snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
 | 
			
		||||
			return_url = url;
 | 
			
		||||
		}
 | 
			
		||||
		const char* headers[] =
 | 
			
		||||
		{
 | 
			
		||||
			"Location", return_url,
 | 
			
		||||
		};
 | 
			
		||||
		tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
 | 
			
		||||
		goto done;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const char* send_session = tf_strdup(session);
 | 
			
		||||
	bool session_is_new = false;
 | 
			
		||||
	const char* login_error = NULL;
 | 
			
		||||
	bool may_become_first_admin = false;
 | 
			
		||||
	if (strcmp(request->method, "POST") == 0)
 | 
			
		||||
	{
 | 
			
		||||
		session_is_new = true;
 | 
			
		||||
		const char** post_form_data = _form_data_decode(request->body, request->content_length);
 | 
			
		||||
		const char* submit = _form_data_get(post_form_data, "submit");
 | 
			
		||||
		if (submit && strcmp(submit, "Login") == 0)
 | 
			
		||||
		{
 | 
			
		||||
			const char* account_name = _form_data_get(post_form_data, "name");
 | 
			
		||||
			account_name_copy = tf_strdup(account_name);
 | 
			
		||||
			const char* password = _form_data_get(post_form_data, "password");
 | 
			
		||||
			const char* new_password = _form_data_get(post_form_data, "new_password");
 | 
			
		||||
			const char* confirm = _form_data_get(post_form_data, "confirm");
 | 
			
		||||
			const char* change = _form_data_get(post_form_data, "change");
 | 
			
		||||
			const char* form_register = _form_data_get(post_form_data, "register");
 | 
			
		||||
			char account_passwd[256] = { 0 };
 | 
			
		||||
			bool have_account = _read_account(
 | 
			
		||||
				ssb,
 | 
			
		||||
				_form_data_get(post_form_data, "name"),
 | 
			
		||||
				account_passwd,
 | 
			
		||||
				sizeof(account_passwd));
 | 
			
		||||
 | 
			
		||||
			if (form_register && strcmp(form_register, "1") == 0)
 | 
			
		||||
			{
 | 
			
		||||
				if (!have_account &&
 | 
			
		||||
					_is_name_valid(account_name) &&
 | 
			
		||||
					password &&
 | 
			
		||||
					confirm &&
 | 
			
		||||
					strcmp(password, confirm) == 0 &&
 | 
			
		||||
					_register_account(ssb, account_name, password))
 | 
			
		||||
				{
 | 
			
		||||
					send_session = _make_session_jwt(ssb, account_name);
 | 
			
		||||
					may_become_first_admin = true;
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
				{
 | 
			
		||||
					login_error = "Error registering account.";
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			else if (change && strcmp(change, "1") == 0)
 | 
			
		||||
			{
 | 
			
		||||
				sqlite3* db = tf_ssb_acquire_db_writer(ssb);
 | 
			
		||||
				if (have_account &&
 | 
			
		||||
					_is_name_valid(account_name) &&
 | 
			
		||||
					new_password &&
 | 
			
		||||
					confirm &&
 | 
			
		||||
					strcmp(new_password, confirm) == 0 &&
 | 
			
		||||
					_verify_password(password, account_passwd) &&
 | 
			
		||||
					_set_account_password(context, db, account_name, new_password))
 | 
			
		||||
				{
 | 
			
		||||
					send_session = _make_session_jwt(ssb, account_name);
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
				{
 | 
			
		||||
					login_error = "Error changing password.";
 | 
			
		||||
				}
 | 
			
		||||
				tf_ssb_release_db_writer(ssb, db);
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				if (have_account && *account_passwd && _verify_password(password, account_passwd))
 | 
			
		||||
				{
 | 
			
		||||
					send_session = _make_session_jwt(ssb, account_name);
 | 
			
		||||
					may_become_first_admin = true;
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
				{
 | 
			
		||||
					login_error = "Invalid username or password.";
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			send_session = _make_session_jwt(ssb, "guest");
 | 
			
		||||
		}
 | 
			
		||||
		tf_free(post_form_data);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (session_is_new && _form_data_get(form_data, "return") && !login_error)
 | 
			
		||||
	{
 | 
			
		||||
		const char* return_url = _form_data_get(form_data, "return");
 | 
			
		||||
		char url[1024];
 | 
			
		||||
		if (!return_url)
 | 
			
		||||
		{
 | 
			
		||||
			snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
 | 
			
		||||
			return_url = url;
 | 
			
		||||
		}
 | 
			
		||||
		const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
 | 
			
		||||
		int length = send_session ? snprintf(NULL, 0, k_pattern, send_session, k_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
 | 
			
		||||
		char* cookie = length ? tf_malloc(length + 1) : NULL;
 | 
			
		||||
		if (cookie)
 | 
			
		||||
		{
 | 
			
		||||
			snprintf(cookie, length + 1, k_pattern, send_session, k_refresh_interval, request->is_tls ? "Secure; " : "");
 | 
			
		||||
		}
 | 
			
		||||
		const char* headers[] =
 | 
			
		||||
		{
 | 
			
		||||
			"Location", return_url,
 | 
			
		||||
			"Set-Cookie", cookie ? cookie : "",
 | 
			
		||||
		};
 | 
			
		||||
		tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
 | 
			
		||||
		tf_free(cookie);
 | 
			
		||||
		tf_free((void*)send_session);
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		tf_http_request_ref(request);
 | 
			
		||||
 | 
			
		||||
		const char* settings = _get_property(ssb, "core", "settings");
 | 
			
		||||
		JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
 | 
			
		||||
		JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct");
 | 
			
		||||
		const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value);
 | 
			
		||||
 | 
			
		||||
		bool have_administrator = false;
 | 
			
		||||
		JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
 | 
			
		||||
 | 
			
		||||
		JSPropertyEnum* ptab = NULL;
 | 
			
		||||
		uint32_t plen = 0;
 | 
			
		||||
		JS_GetOwnPropertyNames(context, &ptab, &plen, permissions, JS_GPN_STRING_MASK);
 | 
			
		||||
		for (int i = 0; i < (int)plen; i++)
 | 
			
		||||
		{
 | 
			
		||||
			JSPropertyDescriptor desc = { 0 };
 | 
			
		||||
			if (JS_GetOwnProperty(context, &desc, permissions, ptab[i].atom) == 1)
 | 
			
		||||
			{
 | 
			
		||||
				int permission_length = tf_util_get_length(context, desc.value);
 | 
			
		||||
				for (int i = 0; i < permission_length; i++)
 | 
			
		||||
				{
 | 
			
		||||
					JSValue entry = JS_GetPropertyUint32(context, desc.value, i);
 | 
			
		||||
					const char* permission = JS_ToCString(context, entry);
 | 
			
		||||
					if (permission && strcmp(permission, "administration") == 0)
 | 
			
		||||
					{
 | 
			
		||||
						have_administrator = true;
 | 
			
		||||
					}
 | 
			
		||||
					JS_FreeCString(context, permission);
 | 
			
		||||
					JS_FreeValue(context, entry);
 | 
			
		||||
				}
 | 
			
		||||
				JS_FreeValue(context, desc.setter);
 | 
			
		||||
				JS_FreeValue(context, desc.getter);
 | 
			
		||||
				JS_FreeValue(context, desc.value);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		for (uint32_t i = 0; i < plen; ++i)
 | 
			
		||||
		{
 | 
			
		||||
			JS_FreeAtom(context, ptab[i].atom);
 | 
			
		||||
		}
 | 
			
		||||
		js_free(context, ptab);
 | 
			
		||||
 | 
			
		||||
		if (!have_administrator && may_become_first_admin)
 | 
			
		||||
		{
 | 
			
		||||
			if (JS_IsUndefined(permissions))
 | 
			
		||||
			{
 | 
			
		||||
				permissions = JS_NewObject(context);
 | 
			
		||||
				JS_SetPropertyStr(context, settings_value, "permissions", permissions);
 | 
			
		||||
			}
 | 
			
		||||
			JSValue user = JS_GetPropertyStr(context, permissions, account_name_copy);
 | 
			
		||||
			if (JS_IsUndefined(user))
 | 
			
		||||
			{
 | 
			
		||||
				user = JS_NewArray(context);
 | 
			
		||||
				JS_SetPropertyStr(context, permissions, account_name_copy, user);
 | 
			
		||||
			}
 | 
			
		||||
			JS_SetPropertyUint32(context, user, tf_util_get_length(context, user), JS_NewString(context, "administration"));
 | 
			
		||||
 | 
			
		||||
			JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
 | 
			
		||||
			const char* settings_string = JS_ToCString(context, settings_json);
 | 
			
		||||
			_set_property(ssb, "core", "settings", settings_string);
 | 
			
		||||
			JS_FreeCString(context, settings_string);
 | 
			
		||||
			JS_FreeValue(context, settings_json);
 | 
			
		||||
		}
 | 
			
		||||
		JS_FreeValue(context, permissions);
 | 
			
		||||
 | 
			
		||||
		login_request_t* login = tf_malloc(sizeof(login_request_t));
 | 
			
		||||
		*login = (login_request_t)
 | 
			
		||||
		{
 | 
			
		||||
			.request = request,
 | 
			
		||||
			.name = account_name_copy,
 | 
			
		||||
			.jwt = jwt,
 | 
			
		||||
			.error = login_error,
 | 
			
		||||
			.session_cookie = send_session,
 | 
			
		||||
			.session_is_new = session_is_new,
 | 
			
		||||
			.code_of_conduct = tf_strdup(code_of_conduct),
 | 
			
		||||
			.have_administrator = have_administrator,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		JS_FreeCString(context, code_of_conduct);
 | 
			
		||||
		JS_FreeValue(context, code_of_conduct_value);
 | 
			
		||||
		JS_FreeValue(context, settings_value);
 | 
			
		||||
		tf_free((void*)settings);
 | 
			
		||||
		tf_file_read(request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
 | 
			
		||||
		jwt = JS_UNDEFINED;
 | 
			
		||||
		account_name_copy = NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
done:
 | 
			
		||||
	tf_free((void*)session);
 | 
			
		||||
	tf_free(form_data);
 | 
			
		||||
	tf_free((void*)account_name_copy);
 | 
			
		||||
	JS_FreeValue(context, jwt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void _httpd_endpoint_logout(tf_http_request_t* request)
 | 
			
		||||
{
 | 
			
		||||
	const char* k_set_cookie = request->is_tls ?
 | 
			
		||||
		"session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly" :
 | 
			
		||||
		"session=; path=/; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly";
 | 
			
		||||
	const char* k_location_format = "/login%s%s";
 | 
			
		||||
	int length = snprintf(NULL, 0, k_location_format, request->query ? "?" : "", request->query);
 | 
			
		||||
	char* location = alloca(length + 1);
 | 
			
		||||
	snprintf(location, length + 1, k_location_format, request->query ? "?" : "", request->query ? request->query : "");
 | 
			
		||||
	const char* headers[] =
 | 
			
		||||
	{
 | 
			
		||||
		"Set-Cookie", k_set_cookie,
 | 
			
		||||
		"Location", location,
 | 
			
		||||
	};
 | 
			
		||||
	tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void tf_httpd_register(JSContext* context)
 | 
			
		||||
{
 | 
			
		||||
	JS_NewClassID(&_httpd_class_id);
 | 
			
		||||
@@ -775,6 +1527,9 @@ void tf_httpd_register(JSContext* context)
 | 
			
		||||
	tf_http_add_handler(http, "/mem", _httpd_endpoint_mem, NULL, task);
 | 
			
		||||
	tf_http_add_handler(http, "/trace", _httpd_endpoint_trace, 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);
 | 
			
		||||
 | 
			
		||||
	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));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								src/ssb.c
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								src/ssb.c
									
									
									
									
									
								
							@@ -3910,3 +3910,29 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
 | 
			
		||||
	uv_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0);
 | 
			
		||||
	uv_unref((uv_handle_t*)&timer->timer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature)
 | 
			
		||||
{
 | 
			
		||||
	bool result = false;
 | 
			
		||||
 | 
			
		||||
	const char* public_key_start = public_key && *public_key == '@' ? public_key + 1 : public_key;
 | 
			
		||||
	const char* public_key_end = public_key_start ? strstr(public_key_start, ".ed25519") : NULL;
 | 
			
		||||
	if (public_key_start && !public_key_end)
 | 
			
		||||
	{
 | 
			
		||||
		public_key_end = public_key_start + strlen(public_key_start);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uint8_t bin_public_key[crypto_sign_PUBLICKEYBYTES] = { 0 };
 | 
			
		||||
	if (tf_base64_decode(public_key_start, public_key_end - public_key_start, bin_public_key, sizeof(bin_public_key)) > 0)
 | 
			
		||||
	{
 | 
			
		||||
		uint8_t bin_signature[crypto_sign_BYTES] = { 0 };
 | 
			
		||||
		if (tf_base64_decode(signature, strlen(signature), bin_signature, sizeof(bin_signature)) > 0)
 | 
			
		||||
		{
 | 
			
		||||
			if (crypto_sign_verify_detached(bin_signature, (const uint8_t*)payload, payload_length, bin_public_key) == 0)
 | 
			
		||||
			{
 | 
			
		||||
				result = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -172,7 +172,7 @@ int tf_ssb_db_identity_get_count_for_user(tf_ssb_t* ssb, const char* user);
 | 
			
		||||
** @param ssb The SSB instance.
 | 
			
		||||
** @param user The user's username.
 | 
			
		||||
** @param[out] out_public_key A buffer populated with the new public key.
 | 
			
		||||
** @param[out] out_private_key A buffer populated with the new privatee key.
 | 
			
		||||
** @param[out] out_private_key A buffer populated with the new private key.
 | 
			
		||||
** @return True if the identity was created.
 | 
			
		||||
*/
 | 
			
		||||
bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_public_key, uint8_t* out_private_key);
 | 
			
		||||
 
 | 
			
		||||
@@ -957,4 +957,6 @@ void tf_ssb_set_room_name(tf_ssb_t* ssb, const char* room_name);
 | 
			
		||||
*/
 | 
			
		||||
void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t* ssb, void* user_data), void* user_data);
 | 
			
		||||
 | 
			
		||||
bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature);
 | 
			
		||||
 | 
			
		||||
/** @} */
 | 
			
		||||
 
 | 
			
		||||
@@ -443,7 +443,7 @@ JSValue tf_taskstub_kill(tf_taskstub_t* stub)
 | 
			
		||||
	JSValue result = JS_UNDEFINED;
 | 
			
		||||
	if (!tf_task_get_one_proc(stub->_owner))
 | 
			
		||||
	{
 | 
			
		||||
		uv_process_kill(&stub->_process, SIGTERM);
 | 
			
		||||
		uv_process_kill(&stub->_process, SIGKILL);
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
 
 | 
			
		||||
@@ -701,6 +701,14 @@ static void _test_http_async(uv_async_t* async)
 | 
			
		||||
static void _test_http_thread(void* data)
 | 
			
		||||
{
 | 
			
		||||
	test_http_t* test = data;
 | 
			
		||||
	const char* value = tf_http_get_cookie("a=foo; b=bar", "a");
 | 
			
		||||
	assert(strcmp(value, "foo") == 0);
 | 
			
		||||
	tf_free((void*)value);
 | 
			
		||||
	value = tf_http_get_cookie("a=foo; b=bar", "b");
 | 
			
		||||
	assert(strcmp(value, "bar") == 0);
 | 
			
		||||
	tf_free((void*)value);
 | 
			
		||||
	assert(tf_http_get_cookie("a=foo; b=bar", "c") == NULL);
 | 
			
		||||
 | 
			
		||||
	int r = system("curl -v http://localhost:23456/404");
 | 
			
		||||
	assert(WEXITSTATUS(r) == 0);
 | 
			
		||||
	tf_printf("curl returned %d\n", WEXITSTATUS(r));
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ try:
 | 
			
		||||
	driver.get('http://localhost:8888')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'login').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('test_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
 | 
			
		||||
@@ -39,7 +39,13 @@ try:
 | 
			
		||||
	driver.switch_to.default_content()
 | 
			
		||||
 | 
			
		||||
	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
 | 
			
		||||
	driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
 | 
			
		||||
	# StaleElementReferenceException
 | 
			
		||||
	while True:
 | 
			
		||||
		try:
 | 
			
		||||
			driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
 | 
			
		||||
			break
 | 
			
		||||
		except:
 | 
			
		||||
			pass
 | 
			
		||||
 | 
			
		||||
	wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click()
 | 
			
		||||
	driver.switch_to.alert.accept()
 | 
			
		||||
@@ -71,7 +77,13 @@ try:
 | 
			
		||||
	driver.switch_to.default_content()
 | 
			
		||||
 | 
			
		||||
	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
 | 
			
		||||
	driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
 | 
			
		||||
	# StaleElementReferenceException
 | 
			
		||||
	while True:
 | 
			
		||||
		try:
 | 
			
		||||
			driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
 | 
			
		||||
			break
 | 
			
		||||
		except:
 | 
			
		||||
			pass
 | 
			
		||||
	# NoSuchShadowRootException
 | 
			
		||||
	while True:
 | 
			
		||||
		try:
 | 
			
		||||
@@ -89,15 +101,15 @@ try:
 | 
			
		||||
	driver.switch_to.default_content()
 | 
			
		||||
	driver.find_element(By.ID, 'allow').click()
 | 
			
		||||
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
 | 
			
		||||
 | 
			
		||||
	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
 | 
			
		||||
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guest_label').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').click()
 | 
			
		||||
 | 
			
		||||
@@ -109,7 +121,7 @@ try:
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout guest').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
 | 
			
		||||
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
 | 
			
		||||
@@ -120,7 +132,7 @@ try:
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
 | 
			
		||||
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_test_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('wrong_test_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
 | 
			
		||||
@@ -141,20 +153,20 @@ try:
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
 | 
			
		||||
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'change_label').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'new_password').send_keys('new_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('new_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
 | 
			
		||||
	wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('new_password')
 | 
			
		||||
	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
 | 
			
		||||
	wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user