Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | 5x 5x 5x 5x 5x 5x 5x 5x 5x 72x 5x 5x 5x 5x 5x 5x | import * as _ from "lodash";
import { fromJSON, isid, newid, } from "./common";
import { getDB } from "./db";
import { keysEqual, IUser, openMessage, signMessage, verifySignedObject } from "./user";
export type txfn = <T>(data: (string | IRemoteData)) => Promise<T | void> | void
export interface IConnection {
id: string
remoteDeviceId: string
lastAck: number //time
handlers: { [key: string]: ((err: any, result: any) => void) }
send: txfn
close: () => void
closed?: true
me?: IUser
remoteUser?: IUser
remoteUserVerified?: boolean
groups?: string[]
pingMS?: number
}
export interface IRemoteData {
type: 'call' | 'response' | 'chunk'
id: string
}
export interface IRemoteCall extends IRemoteData {
type: 'call'
fnName: string
args: any[]
}
export interface IRemoteResponse extends IRemoteData {
type: 'response'
result?: any
error?: any
}
export interface IRemoteChunk extends IRemoteData {
type: 'chunk',
iChunk: number,
totalChunks: number
chunk: string,
}
export async function ping(...args) {
return ['pong', ...args];
}
export async function testError(msg: string) {
throw new Error(msg);
}
async function signId(id: string) {
Iif (!isid(id)) {
throw new Error('Only single ids are signed to prevent abuse');
}
return signMessage(id);
}
export async function verifyRemoteUser(connection: IConnection) {
try {
Iif (connection.remoteUserVerified) {
return;
}
const id = newid();
const signedId = await RPC(connection, signId)(id);
const openedId = openMessage(signedId, connection.remoteUser.publicKey);
Iif (openedId != id) {
throw new Error('Failed to verify possession of correct secretKey')
}
verifySignedObject(connection.remoteUser, connection.remoteUser.publicKey);
const db = await getDB();
const dbUser = await db.get(connection.remoteUser.id) as IUser;
// TODO for all of the below issues see: host.ts and auth.ts, both are WIPs
Iif (dbUser && !keysEqual(dbUser.publicKey, connection.remoteUser.publicKey)) {
// TODO allow public keys to change
// this will have to happen if a user's private key is compromised so we need to plan for it
// The obvious solution is to use some server as a source of truth but that kind of violates the p2p model
throw new Error("Remote user's pubic key does not match what we have in db");
// IDEA use previously known devices to try to do multi-factor authentication
// If the user has two or more devices they regularly use, we can ask as many of those devices
// as we can connect with, which is the correct public key for their user.
// we can reject the new public key until all available devices belonging to the user are in consensus.
}
Iif (!dbUser || dbUser.modified < connection.remoteUser.modified) {
// TODO protect from users stealing other users' ids
// this can happen if user1 has never seen user2 before, and user3 creates a user object
// with user2's id but a new public/private key, then gives that to user1
// MAYBE ask any other peers if they have this user and if so check that public keys match
await db.save(connection.remoteUser);
}
} catch (err) {
throw new Error('remote user failed verification: ' + String(err));
}
connection.remoteUserVerified = true;
}
const remotelyCallableFunctions: { [key: string]: Function } = {
ping,
testError,
signId,
}
export function setRemotelyCallableFunction(fn: Function, name?: string) {
remotelyCallableFunctions[name || fn.name] = fn;
}
export function RPC<T extends Function>(connection: IConnection, fn: T): T {
return <any>function (...args) {
const fnName = Object.keys(remotelyCallableFunctions).find(fnName => remotelyCallableFunctions[fnName] == fn);
Iif (fnName === "ping") {
const sTime = Date.now();
return makeRemoteCall(connection, fnName as any, args).then(result => {
const eTime = Date.now();
connection.pingMS = eTime - sTime;
return result;
})
}
return makeRemoteCall(connection, fnName as any, args);
};
}
export const RPC_TIMEOUT_MS = 15_000;
export async function makeRemoteCall(connection: IConnection, fnName: string, args: any[]) {
const id = newid();
let rejectRemoteCall;
const remoteCallPromise = new Promise((resolve, reject) => {
rejectRemoteCall = reject;
const pid = setTimeout(() => {
delete connection.handlers[id]
reject(`RPC timeout: ${fnName}(${args.join(',')})`);
}, 10_000);
connection.handlers[id] = (err, result) => {
clearTimeout(pid);
err ? reject(err) : resolve(result);
}
});
try {
let remoteCall: IRemoteCall = {
type: 'call',
id,
fnName,
args
}
// WebRTC is already encrypted so signing the call object seems wasteful
// remoteCall = signObject(remoteCall);
connection.send(remoteCall);
} catch (err) {
rejectRemoteCall(err);
}
return remoteCallPromise;
}
async function sendRemoteError(connection: IConnection, callId: string, error: string) {
let response: IRemoteResponse = {
type: 'response',
id: callId,
error
}
connection.send(response);
}
let currentConnection: IConnection;
export const getCurrentConnection = () => currentConnection;
async function handelRemoteCall(connection: IConnection, remoteCall: IRemoteCall) {
const { id, fnName, args } = remoteCall;
try {
// WebRTC is already encrypted so verifying at this level seems wasteful (see `verifyRemoteUser` below)
// verifySignedObject(remoteCall as any, connection.remoteUser.publicKey);
const fn = remotelyCallableFunctions[fnName];
let result;
let error;
if (typeof fn !== 'function') {
error = `${fnName} is not a remotely callable function`;
} else {
try {
Iif (!connection.remoteUserVerified && fn != signId) {
await verifyRemoteUser(connection);
// console.log('remote user verified', { deviceId: connection.remoteDeviceId, userId: connection.remoteUser?.id })
}
// make the current connection available to the fn when it is called
currentConnection = connection;
const resultPromise = fn(...args);
// unset current connection as soon as possible to prevent weird usage
currentConnection = null;
result = await resultPromise;
} catch (err) {
error = String(err);
}
}
let response: IRemoteResponse = {
type: 'response',
id,
result,
error
}
connection.send(response);
} catch (err) {
sendRemoteError(connection, id, 'unhandled error in handelRemoteCall: ' + err);
}
}
const messageChunks = {};
export function onRemoteMessage(connection: IConnection, message: string | IRemoteData): void {
// console.log({ connection, message })
// TODO check if fromJSON calls eval, if so this is a security hole
message = fromJSON(JSON.parse(message as any));
connection.lastAck = Date.now();
Iif (message === 'ack') return;
Iif (message == 'ping') {
console.log('ping!', { deviceId: connection.remoteDeviceId, userId: connection.remoteUser?.id })
connection.send('pong');
return;
}
Iif (message == 'pong') {
console.log('pong!', { deviceId: connection.remoteDeviceId, userId: connection.remoteUser?.id })
return;
}
const msgObj = message as IRemoteCall | IRemoteResponse | IRemoteChunk;
Iif (msgObj.type === 'chunk') {
// validate size to prevent remote attacker filling up memory
Iif (msgObj.totalChunks * msgObj.chunk.length > 1e9) {
throw new Error(`Message larger than maximum allowed size of ${1e8} (~100Mb), use files for very large objects or write a custom function to stream the data`)
}
Iif (!messageChunks[msgObj.id]) {
messageChunks[msgObj.id] = [];
}
const chunks = messageChunks[msgObj.id];
chunks[msgObj.iChunk] = msgObj.chunk;
Iif (_.compact(chunks).length === msgObj.totalChunks) {
delete messageChunks[msgObj.id];
onRemoteMessage(connection, chunks.join(''));
}
return;
}
switch (msgObj.type) {
case 'call':
handelRemoteCall(connection, msgObj);
break;
case 'response':
const handler = connection.handlers[msgObj.id]
if (handler) {
handler(msgObj.error, msgObj.result);
delete connection.handlers[msgObj.id];
} else {
/* istanbul ignore next */
console.error('no handler for remote response', connection, msgObj)
}
break;
default:
// @ts-ignore
sendRemoteError(connection, msgObj.id, 'unknown remote call: ' + msgObj.type)
}
} |