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 core from './core.js'; | ||||||
| import * as form from './form.js'; | import * as form from './form.js'; | ||||||
|  |  | ||||||
| let gDatabase = new Database('auth'); |  | ||||||
|  |  | ||||||
| const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000; | 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 |  * TODOC | ||||||
|  * @param {*} headers most likely an object |  * @param {*} headers most likely an object | ||||||
| @@ -181,175 +125,6 @@ function getCookies(headers) { | |||||||
| 	return cookies; | 	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 ? |  * Gets a user's permissions based on it's session ? | ||||||
|  * @param {*} session TODOC |  * @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) { | 		if (tildefriends.https_port && gGlobalSettings.http_redirect) { | ||||||
| 			httpd.set_http_redirect(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('/app/socket', app.socket); | ||||||
| 		httpd.all('', function default_http_handler(request, response) { | 		httpd.all('', function default_http_handler(request, response) { | ||||||
| 			let match; | 			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; | 	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); | 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. | ** Send a websocket message. | ||||||
| ** @param request The HTTP request which was previously updated to a websocket | ** @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 "http.h" | ||||||
| #include "log.h" | #include "log.h" | ||||||
| #include "mem.h" | #include "mem.h" | ||||||
|  | #include "ssb.h" | ||||||
|  | #include "ssb.db.h" | ||||||
| #include "task.h" | #include "task.h" | ||||||
| #include "tlscontext.js.h" | #include "tlscontext.js.h" | ||||||
| #include "trace.h" | #include "trace.h" | ||||||
| #include "util.js.h" | #include "util.js.h" | ||||||
|  |  | ||||||
|  | #include "ow-crypt.h" | ||||||
|  |  | ||||||
| #include "picohttpparser.h" | #include "picohttpparser.h" | ||||||
|  |  | ||||||
|  | #include "sodium/crypto_sign.h" | ||||||
|  | #include "sodium/utils.h" | ||||||
|  |  | ||||||
|  | #include "sqlite3.h" | ||||||
|  |  | ||||||
| #include <assert.h> | #include <assert.h> | ||||||
| #include <stdlib.h> | #include <stdlib.h> | ||||||
| #include <string.h> | #include <string.h> | ||||||
|  | #include <time.h> | ||||||
|  |  | ||||||
| #include <openssl/sha.h> | #include <openssl/sha.h> | ||||||
|  |  | ||||||
| @@ -23,6 +33,8 @@ | |||||||
|  |  | ||||||
| #define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a)))) | #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 JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); | ||||||
|  |  | ||||||
| static JSClassID _httpd_class_id; | static JSClassID _httpd_class_id; | ||||||
| @@ -732,6 +744,746 @@ static void _httpd_endpoint_debug(tf_http_request_t* request) | |||||||
| 	tf_free(response); | 	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) | void tf_httpd_register(JSContext* context) | ||||||
| { | { | ||||||
| 	JS_NewClassID(&_httpd_class_id); | 	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, "/mem", _httpd_endpoint_mem, NULL, task); | ||||||
| 	tf_http_add_handler(http, "/trace", _httpd_endpoint_trace, 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, "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)); | ||||||
|   | |||||||
							
								
								
									
										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_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0); | ||||||
| 	uv_unref((uv_handle_t*)&timer->timer); | 	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 ssb The SSB instance. | ||||||
| ** @param user The user's username. | ** @param user The user's username. | ||||||
| ** @param[out] out_public_key A buffer populated with the new public key. | ** @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. | ** @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); | 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); | 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; | 	JSValue result = JS_UNDEFINED; | ||||||
| 	if (!tf_task_get_one_proc(stub->_owner)) | 	if (!tf_task_get_one_proc(stub->_owner)) | ||||||
| 	{ | 	{ | ||||||
| 		uv_process_kill(&stub->_process, SIGTERM); | 		uv_process_kill(&stub->_process, SIGKILL); | ||||||
| 	} | 	} | ||||||
| 	else | 	else | ||||||
| 	{ | 	{ | ||||||
|   | |||||||
| @@ -701,6 +701,14 @@ static void _test_http_async(uv_async_t* async) | |||||||
| static void _test_http_thread(void* data) | static void _test_http_thread(void* data) | ||||||
| { | { | ||||||
| 	test_http_t* test = 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"); | 	int r = system("curl -v http://localhost:23456/404"); | ||||||
| 	assert(WEXITSTATUS(r) == 0); | 	assert(WEXITSTATUS(r) == 0); | ||||||
| 	tf_printf("curl returned %d\n", WEXITSTATUS(r)); | 	tf_printf("curl returned %d\n", WEXITSTATUS(r)); | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ try: | |||||||
| 	driver.get('http://localhost:8888') | 	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-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, '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, '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, 'confirm').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, 'loginButton').click() | ||||||
| @@ -39,7 +39,13 @@ try: | |||||||
| 	driver.switch_to.default_content() | 	driver.switch_to.default_content() | ||||||
|  |  | ||||||
| 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) | 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) | ||||||
|  | 	# StaleElementReferenceException | ||||||
|  | 	while True: | ||||||
|  | 		try: | ||||||
| 			driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) | 			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() | 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click() | ||||||
| 	driver.switch_to.alert.accept() | 	driver.switch_to.alert.accept() | ||||||
| @@ -71,7 +77,13 @@ try: | |||||||
| 	driver.switch_to.default_content() | 	driver.switch_to.default_content() | ||||||
|  |  | ||||||
| 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) | 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) | ||||||
|  | 	# StaleElementReferenceException | ||||||
|  | 	while True: | ||||||
|  | 		try: | ||||||
| 			driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) | 			driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) | ||||||
|  | 			break | ||||||
|  | 		except: | ||||||
|  | 			pass | ||||||
| 	# NoSuchShadowRootException | 	# NoSuchShadowRootException | ||||||
| 	while True: | 	while True: | ||||||
| 		try: | 		try: | ||||||
| @@ -89,15 +101,15 @@ try: | |||||||
| 	driver.switch_to.default_content() | 	driver.switch_to.default_content() | ||||||
| 	driver.find_element(By.ID, 'allow').click() | 	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, '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, '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, 'loginButton').click() | ||||||
|  |  | ||||||
| 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) | 	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, 'guest_label').click() | ||||||
| 	driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').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-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, '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, '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, '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, '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, '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, '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, '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, 'confirm').send_keys('wrong_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, '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, '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, '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, '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, '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, 'confirm').send_keys('new_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, 'loginButton').click() | ||||||
| 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) | 	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, '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, '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, '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, '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, '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, 'password').send_keys('new_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, 'loginButton').click() | ||||||
| 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) | 	wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user