mirror of
https://github.com/Rolands-Laucis/Socio.git
synced 2026-05-21 06:46:19 -06:00
auto_recon_by_ip and minor patch
This commit is contained in:
parent
2eb7b129e7
commit
0ca0dad395
4 changed files with 102 additions and 64 deletions
|
|
@ -16,6 +16,7 @@ type QueryObjectSQL = { sql?: string, endpoint?: string, params?: object | null
|
|||
type QueryObject = QueryObjectSQL & { onUpdate: SubscribeCallbackObject };
|
||||
type QueryPromise = { res: Function, prom:Promise<any> | null, start_buff: number, payload_size?:number };
|
||||
export type ProgressOnUpdate = (percentage: number) => void;
|
||||
type ReconData = { id?: id, result: { old_client_id: ClientID, auth: boolean }, success: Bit };
|
||||
|
||||
type PropUpdateCallback = ((new_val: PropValue, diff?: diff_lib.rdiffResult[]) => void) | null;
|
||||
export type ClientProp = { val: PropValue | undefined, subs: { [id: id]: PropUpdateCallback } };
|
||||
|
|
@ -131,7 +132,7 @@ export class SocioClient extends LogHandler {
|
|||
}
|
||||
case ClientMessageKind.AUTH:{
|
||||
this.#FindID(kind, data?.id)
|
||||
if (data?.result?.success as Bit !== 1)
|
||||
if (data?.result as Bit !== 1)
|
||||
this.HandleInfo(`AUTH returned FALSE, which means websocket has not authenticated.`);
|
||||
|
||||
this.#authenticated = data?.result as Bit === 1;
|
||||
|
|
@ -189,10 +190,14 @@ export class SocioClient extends LogHandler {
|
|||
break;
|
||||
}
|
||||
case ClientMessageKind.RECON:{
|
||||
this.#FindID(kind, data?.id);
|
||||
//@ts-expect-error
|
||||
this.#queries.get(data.id)(data);
|
||||
this.#queries.delete(data.id); //clear memory
|
||||
if(data?.id){
|
||||
this.#FindID(kind, data.id);
|
||||
(this.#queries.get(data.id) as QueryPromise).res(data);
|
||||
this.#queries.delete(data.id); //clear memory
|
||||
}
|
||||
// sent without id, so the recon was automatic on the server-side, not requested by this client
|
||||
else
|
||||
this.#Reconnect(data as unknown as ReconData);
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.RECV_FILES:{
|
||||
|
|
@ -464,15 +469,12 @@ export class SocioClient extends LogHandler {
|
|||
}
|
||||
#HandleBasicPromiseMessage(kind: ClientMessageKind, data:ClientMessageDataObj){
|
||||
this.#FindID(kind, data?.id);
|
||||
const q = this.#queries.get(data.id);
|
||||
// @ts-expect-error
|
||||
if(q?.res)
|
||||
const q = this.#queries.get(data.id) as QueryObject | QueryPromise;
|
||||
if (q.hasOwnProperty('res'))
|
||||
(q as QueryPromise).res(data?.result as any);
|
||||
// @ts-expect-error
|
||||
else if (q?.onUpdate)
|
||||
if ((q as QueryObject)?.onUpdate?.success)
|
||||
// @ts-expect-error
|
||||
(q as QueryObject).onUpdate.success(data?.result as Bit);
|
||||
else if (q.hasOwnProperty('onUpdate') && (q as QueryObject).onUpdate?.success)
|
||||
//@ts-expect-error
|
||||
q.onUpdate.success(data?.result as Bit);
|
||||
|
||||
this.#queries.delete(data.id); //clear memory
|
||||
}
|
||||
|
|
@ -510,20 +512,19 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
//ask the server for a reconnection to an old session via our one-time token
|
||||
this.Send(CoreMessageKind.RECON, { id, data: { type: 'POST', token } });
|
||||
const res = await prom;
|
||||
|
||||
//@ts-ignore
|
||||
if (res?.success){
|
||||
//@ts-ignore
|
||||
this.#authenticated = res?.result?.auth;
|
||||
|
||||
//@ts-ignore
|
||||
this.done(`${this.config.name} reconnected successfully. ${res?.result?.old_client_id} -> ${this.#client_id} (old client ID -> new/current client ID)`)
|
||||
}
|
||||
else
|
||||
this.HandleError(new E('Failed to reconnect', res));
|
||||
const res = await (prom as unknown as Promise<ReconData>);
|
||||
this.#Reconnect(res); //sets the trusted values from the server, like auth bool
|
||||
}
|
||||
}
|
||||
//sets the trusted values from the server, like auth bool
|
||||
#Reconnect(data:ReconData){
|
||||
if (data?.success) {
|
||||
this.#authenticated = data?.result.auth;
|
||||
this.done(`${this.config.name} reconnected successfully. ${data.result.old_client_id} -> ${this.#client_id} (old client ID -> new/current client ID)`, data);
|
||||
}
|
||||
else
|
||||
this.HandleError(new E('Failed to reconnect', data));
|
||||
}
|
||||
|
||||
// for dev debug, if u want
|
||||
LogMaps(){
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export type QueryFunction = (client: SocioSession, id: id, sql: string, params?:
|
|||
type SessionsDefaults = { timeouts: boolean, timeouts_check_interval_ms?: number, session_delete_delay_ms?: number, recon_ttl_ms?: number } & SessionOpts;
|
||||
type DecryptOptions = { decrypt_sql: boolean, decrypt_prop: boolean, decrypt_endpoint: boolean };
|
||||
type DBOpts = { Query: QueryFunction, Arbiter?: (initiator: { client: SocioSession, sql: string, params: any }, current: { client: SocioSession, hook: SubObj }) => boolean | Promise<boolean>};
|
||||
type SocioServerOptions = { db: DBOpts, socio_security?: SocioSecurity | null, decrypt_opts?: DecryptOptions, hard_crash?: boolean, session_defaults?: SessionsDefaults, prop_upd_diff?: boolean, [key:string]:any } & LoggingOpts;
|
||||
type SocioServerOptions = { db: DBOpts, socio_security?: SocioSecurity | null, decrypt_opts?: DecryptOptions, hard_crash?: boolean, session_defaults?: SessionsDefaults, prop_upd_diff?: boolean, auto_recon_by_ip?:boolean, [key:string]:any } & LoggingOpts;
|
||||
type AdminMessageDataObj = {function:string, args?:any[], secure_key:string};
|
||||
type BasicClientResponse = { id: id | string, data?: any, result?: Bit | string | {success: Bit | string} | object, [key: string]: any };
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ type BasicClientResponse = { id: id | string, data?: any, result?: Bit | string
|
|||
//whereas public variables are free for you to alter freely at any time during runtime.
|
||||
|
||||
export class SocioServer extends LogHandler {
|
||||
// private:
|
||||
//---private:
|
||||
#wss: WebSocketServer;
|
||||
#sessions: Map<ClientID, SocioSession> = new Map(); //Maps are quite more performant than objects. And their keys dont overlap with Object prototype.
|
||||
|
||||
|
|
@ -63,17 +63,18 @@ export class SocioServer extends LogHandler {
|
|||
//global flag to send prop obj diffs using the diff lib instead of the full object every time.
|
||||
#prop_upd_diff = false;
|
||||
|
||||
//public:
|
||||
//---public:
|
||||
db!: DBOpts;
|
||||
session_defaults: SessionsDefaults = { timeouts: false, timeouts_check_interval_ms: 1000 * 60, session_timeout_ttl_ms: Infinity, session_delete_delay_ms: 1000 * 5, recon_ttl_ms: 1000 * 60 * 60 };
|
||||
prop_reg_timeout_ms!: number;
|
||||
auto_recon_by_ip:boolean = false;
|
||||
|
||||
constructor(opts: ServerOptions | undefined = {}, { db, socio_security = null, logging = { verbose: false, hard_crash: false }, decrypt_opts = { decrypt_sql: true, decrypt_prop: false, decrypt_endpoint:false}, session_defaults = undefined, prop_upd_diff=false, prop_reg_timeout_ms=1000*10 }: SocioServerOptions){
|
||||
constructor(opts: ServerOptions | undefined = {}, { db, socio_security = null, logging = { verbose: false, hard_crash: false }, decrypt_opts = { decrypt_sql: true, decrypt_prop: false, decrypt_endpoint: false }, session_defaults = undefined, prop_upd_diff = false, prop_reg_timeout_ms = 1000 * 10, auto_recon_by_ip = false }: SocioServerOptions){
|
||||
super({ ...logging, prefix:'SocioServer'});
|
||||
//verbose - print stuff to the console using my lib. Doesnt affect the log handlers
|
||||
//hard_crash will just crash the class instance and propogate (throw) the error encountered without logging it anywhere - up to you to handle.
|
||||
//both are public and settable at runtime
|
||||
|
||||
|
||||
//private:
|
||||
this.#wss = new WebSocketServer({ ...opts, clientTracking: true }); //take a look at the WebSocketServer docs - the opts can have a server param, that can be your http server
|
||||
this.#secure = { socio_security, ...decrypt_opts };
|
||||
|
|
@ -84,6 +85,7 @@ export class SocioServer extends LogHandler {
|
|||
this.db = db;
|
||||
this.session_defaults = Object.assign(this.session_defaults, session_defaults);
|
||||
this.prop_reg_timeout_ms = prop_reg_timeout_ms;
|
||||
this.auto_recon_by_ip = auto_recon_by_ip;
|
||||
|
||||
this.#wss.on('connection', this.#Connect.bind(this)); //https://thenewstack.io/mastering-javascript-callbacks-bind-apply-call/ have to bind 'this' to the function, otherwise it will use the .on()'s 'this', so that this.[prop] are not undefined
|
||||
this.#wss.on('close', (...stuff) => { this.HandleInfo('WebSocketServer close event', ...stuff) });
|
||||
|
|
@ -95,8 +97,8 @@ export class SocioServer extends LogHandler {
|
|||
|
||||
const addr: AddressInfo = this.#wss.address() as AddressInfo;
|
||||
if (this.verbose) this.done(`Created SocioServer on `, addr);
|
||||
// if (addr.family == 'ws')
|
||||
// this.HandleInfo('WARNING! Your server is using an unsecure WebSocket protocol, setup wss:// instead, when you can!');
|
||||
if (addr.family == 'ws')
|
||||
this.HandleInfo('WARNING! Your server is using an unsecure WebSocket protocol, setup wss:// instead, when you can!');
|
||||
}
|
||||
|
||||
async #Connect(conn: WebSocket, request: IncomingMessage){
|
||||
|
|
@ -118,10 +120,6 @@ export class SocioServer extends LogHandler {
|
|||
if (this.#lifecycle_hooks.con)
|
||||
await this.#lifecycle_hooks.con(client, request); //u can get the client_id and client_ip off the client object
|
||||
|
||||
//notify the client of their ID
|
||||
client.Send(ClientMessageKind.CON, client_id);
|
||||
this.HandleInfo('CON', client_id); //, this.#wss.clients
|
||||
|
||||
//set this client websockets event handlers
|
||||
conn.on('message', (req: Buffer | ArrayBuffer | Buffer[], isBinary: Boolean) => {
|
||||
if (this.#sessions.has(client_id))//@ts-expect-error
|
||||
|
|
@ -130,6 +128,24 @@ export class SocioServer extends LogHandler {
|
|||
});
|
||||
conn.on('close', (code:number, reason:Buffer) => { this.#SocketClosed.bind(this)(client, {code, reason:reason.toString('utf8')}) });
|
||||
conn.on('error', (error: Error) => { this.#SocketClosed.bind(this)(client, error) }); //https://github.com/websockets/ws/blob/master/doc/ws.md#event-error-1
|
||||
|
||||
// socio can recognize that the IP matches an existing session, so it can reconnect to it, keeping the old sessions data
|
||||
if(this.auto_recon_by_ip){
|
||||
// find an IP matching session
|
||||
for (const [id, ses] of this.#sessions.entries()){
|
||||
if(id !== client_id && ses.ipAddr === client_ip){
|
||||
//recon procedure
|
||||
const old_client = this.#sessions.get(id) as SocioSession;
|
||||
this.ReconnectClientSession(client, old_client);
|
||||
this.HandleInfo(`AUTO IP RECON | old id: ${id} -> new id: ${client.id} | IP: ${client_ip}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//notify the client of their ID
|
||||
client.Send(ClientMessageKind.CON, client_id);
|
||||
this.HandleInfo('CON', { id: client_id, ip: client_ip }); //, this.#wss.clients
|
||||
} catch (e: err) { this.HandleError(e); }
|
||||
}
|
||||
|
||||
|
|
@ -251,13 +267,13 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
case CoreMessageKind.AUTH: {//client requests to authenticate itself with the server
|
||||
if (client.authenticated) //check if already has auth
|
||||
client.Send(ClientMessageKind.AUTH, { id: data.id, result: {success: 1} });
|
||||
client.Send(ClientMessageKind.AUTH, { id: data.id, result: 1 });
|
||||
else if (this.#lifecycle_hooks.auth) {
|
||||
const res = await client.Authenticate(this.#lifecycle_hooks.auth, data.params) //bcs its a private class field, give this function the hook to call and params to it. It will set its field and give back the result. NOTE this is safer than adding a setter to a private field
|
||||
client.Send(ClientMessageKind.AUTH, { id: data.id, result: res == true ? 1 : 0 }) //authenticated can be any truthy or falsy value, but the client will only receive a boolean, so its safe to set this to like an ID or token or smth for your own use
|
||||
const res = await client.Authenticate(this.#lifecycle_hooks.auth, data.params); //bcs its a private class field, give this function the hook to call and params to it. It will set its field and give back the result. NOTE this is safer than adding a setter to a private field
|
||||
client.Send(ClientMessageKind.AUTH, { id: data.id, result: res === true ? 1 : 0 }); //authenticated can be any truthy or falsy value, but the client will only receive a boolean, so its safe to set this to like an ID or token or smth for your own use
|
||||
} else {
|
||||
this.HandleError('AUTH function hook not registered, so client not authenticated. [#no-auth-func]')
|
||||
client.Send(ClientMessageKind.AUTH, { id: data.id, result: 0 })
|
||||
this.HandleError('AUTH function hook not registered, so client not authenticated. [#no-auth-func]');
|
||||
client.Send(ClientMessageKind.AUTH, { id: data.id, result: 0 });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -461,22 +477,8 @@ export class SocioServer extends LogHandler {
|
|||
|
||||
//recon procedure
|
||||
const old_client = this.#sessions.get(old_c_id) as SocioSession;
|
||||
old_client.Restore();//stop the old session deletion, since a reconnect was actually attempted
|
||||
client.CopySessionFrom(old_client);
|
||||
|
||||
//clear the subscriptions on the sockets, since the new instance will define new ones on the new page. Also to avoid ID conflicts
|
||||
this.#ClearClientSessionSubs(old_c_id);
|
||||
this.#ClearClientSessionSubs(client.id);
|
||||
|
||||
//delete old session for good
|
||||
old_client.Destroy(() => {
|
||||
this.#ClearClientSessionSubs(old_c_id);
|
||||
this.#sessions.delete(old_c_id);
|
||||
}, this.session_defaults.session_delete_delay_ms as number);
|
||||
|
||||
//notify the client
|
||||
client.Send(ClientMessageKind.RECON, { id: data.id, result: { old_client_id: old_c_id, auth: client.authenticated }, success: 1 });
|
||||
this.HandleInfo(`RECON ${old_c_id} -> ${client.id} (old client ID -> new/current client ID)`);
|
||||
this.ReconnectClientSession(client, old_client, data.id as id);
|
||||
this.HandleInfo(`RECON | old id: ${old_c_id} -> new id: ${client.id}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -788,6 +790,27 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
}
|
||||
|
||||
ReconnectClientSession(new_session: SocioSession, old_session: SocioSession, client_notify_msg_id?:id){
|
||||
const new_id = new_session.id, old_id = old_session.id;
|
||||
old_session.Restore();//stop the old session deletion, since a reconnect was actually attempted
|
||||
new_session.CopySessionFrom(old_session);
|
||||
|
||||
//clear the subscriptions on the sockets, since the new instance will define new ones on the new page. Also to avoid ID conflicts
|
||||
this.#ClearClientSessionSubs(old_id);
|
||||
this.#ClearClientSessionSubs(new_id);
|
||||
|
||||
//delete old session for good
|
||||
old_session.Destroy(() => {
|
||||
this.#ClearClientSessionSubs(old_id);
|
||||
this.#sessions.delete(old_id);
|
||||
}, this.session_defaults.session_delete_delay_ms as number);
|
||||
|
||||
//notify the client
|
||||
const data = { result: { old_client_id: old_id, auth: new_session.authenticated }, success: 1 };
|
||||
if (client_notify_msg_id) data['id'] = client_notify_msg_id;
|
||||
new_session.Send(ClientMessageKind.RECON, data);
|
||||
}
|
||||
|
||||
get session_ids(){return this.#sessions.keys();}
|
||||
get server_info() { return this.#wss.address(); }
|
||||
get raw_websocket_server() { return this.#wss; }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { ClientMessageKind } from './core-client.js';
|
|||
|
||||
//types
|
||||
import type { WebSocket } from 'ws'; //https://github.com/websockets/ws https://github.com/websockets/ws/blob/master/doc/ws.md
|
||||
import type { id, Bit, LoggingOpts, SessionOpts } from './types.js';
|
||||
import type { id, Bit, LoggingOpts, SessionOpts, Auth_Hook } from './types.js';
|
||||
import type { RateLimit } from './ratelimit.js';
|
||||
|
||||
export type SubObj = {
|
||||
|
|
@ -90,9 +90,9 @@ export class SocioSession extends LogHandler {
|
|||
}
|
||||
|
||||
get authenticated() { return this.#authenticated }
|
||||
async Authenticate(auth_func:Function, params:object|null=null) { //auth func can return any truthy or falsy value, the client will only receive a boolean, so its safe to set it to some credential or id or smth, as this would be accessible and useful to you when checking the session access to tables
|
||||
const auth:boolean = await auth_func(this, params);
|
||||
this.#authenticated = auth == true;
|
||||
async Authenticate(auth_func: Auth_Hook, params:object|null=null) { //auth func can return any truthy or falsy value, the client will only receive a boolean, so its safe to set it to some credential or id or smth, as this would be accessible and useful to you when checking the session access to tables
|
||||
const auth = await auth_func(this, params);
|
||||
this.#authenticated = auth === true;
|
||||
return auth;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue