remove stuff related to web api
parent
6abda94e62
commit
75b7788fa9
@ -1,356 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
const { Agent } = require('https');
|
||||
|
||||
/* global Buffer, setTimeout, log, _ */
|
||||
|
||||
/* eslint-disable more/no-then, no-bitwise, no-nested-ternary */
|
||||
|
||||
function _btoa(str) {
|
||||
let buffer;
|
||||
|
||||
if (str instanceof Buffer) {
|
||||
buffer = str;
|
||||
} else {
|
||||
buffer = Buffer.from(str.toString(), 'binary');
|
||||
}
|
||||
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
|
||||
const _call = object => Object.prototype.toString.call(object);
|
||||
|
||||
const ArrayBufferToString = _call(new ArrayBuffer());
|
||||
const Uint8ArrayToString = _call(new Uint8Array());
|
||||
|
||||
function _getString(thing) {
|
||||
if (typeof thing !== 'string') {
|
||||
if (_call(thing) === Uint8ArrayToString) {
|
||||
return String.fromCharCode.apply(null, thing);
|
||||
}
|
||||
if (_call(thing) === ArrayBufferToString) {
|
||||
return _getString(new Uint8Array(thing));
|
||||
}
|
||||
}
|
||||
return thing;
|
||||
}
|
||||
|
||||
function _validateResponse(response, schema) {
|
||||
try {
|
||||
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
||||
for (const i in schema) {
|
||||
switch (schema[i]) {
|
||||
case 'object':
|
||||
case 'string':
|
||||
case 'number':
|
||||
// eslint-disable-next-line valid-typeof
|
||||
if (typeof response[i] !== schema[i]) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||
const agents = {
|
||||
unauth: null,
|
||||
auth: null,
|
||||
};
|
||||
|
||||
function getContentType(response) {
|
||||
if (response.headers && response.headers.get) {
|
||||
return response.headers.get('content-type');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function _promiseAjax(providedUrl, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = providedUrl || `${options.host}/${options.path}`;
|
||||
if (options.disableLogs) {
|
||||
log.info(
|
||||
`${options.type} [REDACTED_URL]${
|
||||
options.unauthenticated ? ' (unauth)' : ''
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
`${options.type} ${url}${options.unauthenticated ? ' (unauth)' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
const timeout =
|
||||
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
|
||||
|
||||
const { proxyUrl } = options;
|
||||
const agentType = options.unauthenticated ? 'unauth' : 'auth';
|
||||
const cacheKey = `${proxyUrl}-${agentType}`;
|
||||
|
||||
const { timestamp } = agents[cacheKey] || {};
|
||||
if (!timestamp || timestamp + FIVE_MINUTES < Date.now()) {
|
||||
if (timestamp) {
|
||||
log.info(`Cycling agent for type ${cacheKey}`);
|
||||
}
|
||||
agents[cacheKey] = {
|
||||
agent: new Agent({ keepAlive: true }),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
const { agent } = agents[cacheKey];
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.type,
|
||||
body: options.data || null,
|
||||
headers: {
|
||||
'User-Agent': 'Session',
|
||||
'X-Loki-Messenger-Agent': 'OWD',
|
||||
...options.headers,
|
||||
},
|
||||
redirect: options.redirect,
|
||||
agent,
|
||||
ca: options.certificateAuthority,
|
||||
timeout,
|
||||
};
|
||||
|
||||
if (fetchOptions.body instanceof ArrayBuffer) {
|
||||
// node-fetch doesn't support ArrayBuffer, only node Buffer
|
||||
const contentLength = fetchOptions.body.byteLength;
|
||||
fetchOptions.body = Buffer.from(fetchOptions.body);
|
||||
|
||||
// node-fetch doesn't set content-length like S3 requires
|
||||
fetchOptions.headers['Content-Length'] = contentLength;
|
||||
}
|
||||
|
||||
const { accessKey, unauthenticated } = options;
|
||||
if (unauthenticated) {
|
||||
if (!accessKey) {
|
||||
throw new Error(
|
||||
'_promiseAjax: mode is aunathenticated, but accessKey was not provided'
|
||||
);
|
||||
}
|
||||
// Access key is already a Base64 string
|
||||
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
|
||||
} else if (options.user && options.password) {
|
||||
const user = _getString(options.user);
|
||||
const password = _getString(options.password);
|
||||
const auth = _btoa(`${user}:${password}`);
|
||||
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
||||
}
|
||||
|
||||
if (options.contentType) {
|
||||
fetchOptions.headers['Content-Type'] = options.contentType;
|
||||
}
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then(response => {
|
||||
let resultPromise;
|
||||
if (
|
||||
options.responseType === 'json' &&
|
||||
response.headers.get('Content-Type') === 'application/json'
|
||||
) {
|
||||
resultPromise = response.json();
|
||||
} else if (
|
||||
options.responseType === 'arraybuffer' ||
|
||||
options.responseType === 'arraybufferwithdetails'
|
||||
) {
|
||||
resultPromise = response.buffer();
|
||||
} else {
|
||||
resultPromise = response.text();
|
||||
}
|
||||
|
||||
return resultPromise.then(result => {
|
||||
if (
|
||||
options.responseType === 'arraybuffer' ||
|
||||
options.responseType === 'arraybufferwithdetails'
|
||||
) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result = result.buffer.slice(
|
||||
result.byteOffset,
|
||||
result.byteOffset + result.byteLength
|
||||
);
|
||||
}
|
||||
if (options.responseType === 'json') {
|
||||
if (options.validateResponse) {
|
||||
if (!_validateResponse(result, options.validateResponse)) {
|
||||
if (options.disableLogs) {
|
||||
log.info(
|
||||
options.type,
|
||||
'[REDACTED_URL]',
|
||||
response.status,
|
||||
'Error'
|
||||
);
|
||||
} else {
|
||||
log.error(options.type, url, response.status, 'Error');
|
||||
}
|
||||
return reject(
|
||||
HTTPError(
|
||||
'promiseAjax: invalid response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.status >= 0 && response.status < 400) {
|
||||
if (options.disableLogs) {
|
||||
log.info(
|
||||
options.type,
|
||||
'[REDACTED_URL]',
|
||||
response.status,
|
||||
'Success'
|
||||
);
|
||||
} else {
|
||||
log.info(options.type, url, response.status, 'Success');
|
||||
}
|
||||
if (options.responseType === 'arraybufferwithdetails') {
|
||||
return resolve({
|
||||
data: result,
|
||||
contentType: getContentType(response),
|
||||
response,
|
||||
});
|
||||
}
|
||||
return resolve(result, response.status);
|
||||
}
|
||||
|
||||
if (options.disableLogs) {
|
||||
log.info(options.type, '[REDACTED_URL]', response.status, 'Error');
|
||||
} else {
|
||||
log.error(options.type, url, response.status, 'Error');
|
||||
}
|
||||
return reject(
|
||||
HTTPError(
|
||||
'promiseAjax: error response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
if (options.disableLogs) {
|
||||
log.error(options.type, '[REDACTED_URL]', 0, 'Error');
|
||||
} else {
|
||||
log.error(options.type, url, 0, 'Error');
|
||||
}
|
||||
const stack = `${e.stack}\nInitial stack:\n${options.stack}`;
|
||||
reject(HTTPError('promiseAjax catch', 0, e.toString(), stack));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _retryAjax(url, options, providedLimit, providedCount) {
|
||||
const count = (providedCount || 0) + 1;
|
||||
const limit = providedLimit || 3;
|
||||
return _promiseAjax(url, options).catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === -1 && count < limit) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(_retryAjax(url, options, limit, count));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function _outerAjax(url, options) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.stack = new Error().stack; // just in case, save stack here.
|
||||
return _retryAjax(url, options);
|
||||
}
|
||||
|
||||
function HTTPError(message, providedCode, response, stack) {
|
||||
const code = providedCode > 999 || providedCode < 100 ? -1 : providedCode;
|
||||
const e = new Error(`${message}; code: ${code}`);
|
||||
e.name = 'HTTPError';
|
||||
e.code = code;
|
||||
e.stack += `\nOriginal stack:\n${stack}`;
|
||||
if (response) {
|
||||
e.response = response;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
};
|
||||
|
||||
// We first set up the data that won't change during this session of the app
|
||||
function initialize() {
|
||||
// Thanks to function-hoisting, we can put this return statement before all of the
|
||||
// below function definitions.
|
||||
return {
|
||||
connect,
|
||||
};
|
||||
|
||||
// Then we connect to the server with user-specific information. This is the only API
|
||||
// exposed to the browser context, ensuring that it can't connect to arbitrary
|
||||
// locations.
|
||||
function connect() {
|
||||
// Thanks, function hoisting!
|
||||
return {
|
||||
getAttachment,
|
||||
getProxiedSize,
|
||||
makeProxiedRequest,
|
||||
};
|
||||
|
||||
function getAttachment(fileUrl) {
|
||||
return _outerAjax(fileUrl, {
|
||||
contentType: 'application/octet-stream',
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 0,
|
||||
type: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
async function getProxiedSize(url) {
|
||||
const result = await _outerAjax(url, {
|
||||
processData: false,
|
||||
responseType: 'arraybufferwithdetails',
|
||||
proxyUrl: '',
|
||||
type: 'HEAD',
|
||||
disableLogs: true,
|
||||
});
|
||||
|
||||
const { response } = result;
|
||||
if (!response.headers || !response.headers.get) {
|
||||
throw new Error('getProxiedSize: Problem retrieving header value');
|
||||
}
|
||||
|
||||
const size = response.headers.get('content-length');
|
||||
return parseInt(size, 10);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
function makeProxiedRequest(url, options = {}) {
|
||||
const { returnArrayBuffer, start, end } = options;
|
||||
let headers;
|
||||
|
||||
if (_.isNumber(start) && _.isNumber(end)) {
|
||||
headers = {
|
||||
Range: `bytes=${start}-${end}`,
|
||||
};
|
||||
}
|
||||
|
||||
return _outerAjax(url, {
|
||||
processData: false,
|
||||
responseType: returnArrayBuffer ? 'arraybufferwithdetails' : null,
|
||||
proxyUrl: '',
|
||||
type: 'GET',
|
||||
redirect: 'follow',
|
||||
disableLogs: true,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
/* global window, textsecure, libsession */
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
const DebugFlagsEnum = {
|
||||
GROUP_SYNC_MESSAGES: 1,
|
||||
CONTACT_SYNC_MESSAGES: 2,
|
||||
FALLBACK_MESSAGES: 8,
|
||||
SESSION_BACKGROUND_MESSAGE: 32,
|
||||
GROUP_REQUEST_INFO: 64,
|
||||
// If you add any new flag, be sure it is bitwise safe! (unique and 2 multiples)
|
||||
ALL: 65535,
|
||||
};
|
||||
|
||||
const debugFlags = DebugFlagsEnum.ALL;
|
||||
|
||||
const debugLogFn = (...args) => {
|
||||
if (window.lokiFeatureFlags.debugMessageLogs) {
|
||||
window.log.warn(...args);
|
||||
}
|
||||
};
|
||||
|
||||
function logGroupSync(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.GROUP_SYNC_MESSAGES) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function logGroupRequestInfo(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.GROUP_REQUEST_INFO) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function logContactSync(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.CONTACT_SYNC_MESSAGES) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function logBackgroundMessage(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.SESSION_BACKGROUND_MESSAGE) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
async function createContactSyncMessage(sessionContacts) {
|
||||
if (sessionContacts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawContacts = await Promise.all(
|
||||
sessionContacts.map(async conversation => {
|
||||
const profile = conversation.getLokiProfile();
|
||||
const name = profile
|
||||
? profile.displayName
|
||||
: conversation.getProfileName();
|
||||
const status = await conversation.safeGetVerified();
|
||||
|
||||
return {
|
||||
name,
|
||||
number: conversation.getNumber(),
|
||||
nickname: conversation.getNickname(),
|
||||
blocked: conversation.isBlocked(),
|
||||
expireTimer: conversation.get('expireTimer'),
|
||||
verifiedStatus: status,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return new libsession.Messages.Outgoing.ContactSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
rawContacts,
|
||||
});
|
||||
}
|
||||
|
||||
function createGroupSyncMessage(sessionGroup) {
|
||||
// We are getting a single open group here
|
||||
|
||||
const rawGroup = {
|
||||
id: sessionGroup.id,
|
||||
name: sessionGroup.get('name'),
|
||||
members: sessionGroup.get('members') || [],
|
||||
blocked: sessionGroup.isBlocked(),
|
||||
expireTimer: sessionGroup.get('expireTimer'),
|
||||
admins: sessionGroup.get('groupAdmins') || [],
|
||||
};
|
||||
|
||||
return new libsession.Messages.Outgoing.ClosedGroupSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
rawGroup,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendSessionRequestsToMembers(members = []) {
|
||||
// For every member, trigger a session request if needed
|
||||
members.forEach(async memberStr => {
|
||||
const ourPubKey = textsecure.storage.user.getNumber();
|
||||
if (memberStr !== ourPubKey) {
|
||||
const memberPubkey = new libsession.Types.PubKey(memberStr);
|
||||
await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(memberStr, 'private');
|
||||
await libsession.Protocols.SessionProtocol.sendSessionRequestIfNeeded(
|
||||
memberPubkey
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const debug = {
|
||||
logContactSync,
|
||||
logGroupSync,
|
||||
logBackgroundMessage,
|
||||
logGroupRequestInfo,
|
||||
};
|
||||
|
||||
window.libloki.api = {
|
||||
sendSessionRequestsToMembers,
|
||||
createContactSyncMessage,
|
||||
createGroupSyncMessage,
|
||||
debug,
|
||||
};
|
||||
})();
|
@ -1,388 +0,0 @@
|
||||
/* global textsecure, WebAPI, window, libloki, _, libsession */
|
||||
|
||||
/* eslint-disable more/no-then, no-bitwise */
|
||||
|
||||
function stringToArrayBuffer(str) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new Error('Passed non-string to stringToArrayBuffer');
|
||||
}
|
||||
const res = new ArrayBuffer(str.length);
|
||||
const uint = new Uint8Array(res);
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
uint[i] = str.charCodeAt(i);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function Message(options) {
|
||||
this.body = options.body;
|
||||
this.attachments = options.attachments || [];
|
||||
this.quote = options.quote;
|
||||
this.preview = options.preview;
|
||||
this.group = options.group;
|
||||
this.flags = options.flags;
|
||||
this.recipients = options.recipients;
|
||||
this.timestamp = options.timestamp;
|
||||
this.needsSync = options.needsSync;
|
||||
this.expireTimer = options.expireTimer;
|
||||
this.profileKey = options.profileKey;
|
||||
this.profile = options.profile;
|
||||
this.groupInvitation = options.groupInvitation;
|
||||
this.sessionRestoration = options.sessionRestoration || false;
|
||||
|
||||
if (!(this.recipients instanceof Array)) {
|
||||
throw new Error('Invalid recipient list');
|
||||
}
|
||||
|
||||
if (!this.group && this.recipients.length !== 1) {
|
||||
throw new Error('Invalid recipient list for non-group');
|
||||
}
|
||||
|
||||
if (typeof this.timestamp !== 'number') {
|
||||
throw new Error('Invalid timestamp');
|
||||
}
|
||||
|
||||
if (this.expireTimer !== undefined && this.expireTimer !== null) {
|
||||
if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) {
|
||||
throw new Error('Invalid expireTimer');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.attachments) {
|
||||
if (!(this.attachments instanceof Array)) {
|
||||
throw new Error('Invalid message attachments');
|
||||
}
|
||||
}
|
||||
if (this.flags !== undefined) {
|
||||
if (typeof this.flags !== 'number') {
|
||||
throw new Error('Invalid message flags');
|
||||
}
|
||||
}
|
||||
if (this.isEndSession()) {
|
||||
if (
|
||||
this.body !== null ||
|
||||
this.group !== null ||
|
||||
this.attachments.length !== 0
|
||||
) {
|
||||
throw new Error('Invalid end session message');
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof this.timestamp !== 'number' ||
|
||||
(this.body && typeof this.body !== 'string')
|
||||
) {
|
||||
throw new Error('Invalid message body');
|
||||
}
|
||||
if (this.group) {
|
||||
if (
|
||||
typeof this.group.id !== 'string' ||
|
||||
typeof this.group.type !== 'number'
|
||||
) {
|
||||
throw new Error('Invalid group context');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message.prototype = {
|
||||
constructor: Message,
|
||||
isEndSession() {
|
||||
return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
},
|
||||
toProto() {
|
||||
if (this.dataMessage instanceof textsecure.protobuf.DataMessage) {
|
||||
return this.dataMessage;
|
||||
}
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
if (this.body) {
|
||||
proto.body = this.body;
|
||||
}
|
||||
proto.attachments = this.attachmentPointers;
|
||||
if (this.flags) {
|
||||
proto.flags = this.flags;
|
||||
}
|
||||
if (this.group) {
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(this.group.id);
|
||||
proto.group.type = this.group.type;
|
||||
}
|
||||
if (Array.isArray(this.preview)) {
|
||||
proto.preview = this.preview.map(preview => {
|
||||
const item = new textsecure.protobuf.DataMessage.Preview();
|
||||
item.title = preview.title;
|
||||
item.url = preview.url;
|
||||
item.image = preview.image || null;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
if (this.quote) {
|
||||
const { QuotedAttachment } = textsecure.protobuf.DataMessage.Quote;
|
||||
const { Quote } = textsecure.protobuf.DataMessage;
|
||||
|
||||
proto.quote = new Quote();
|
||||
const { quote } = proto;
|
||||
|
||||
quote.id = this.quote.id;
|
||||
quote.author = this.quote.author;
|
||||
quote.text = this.quote.text;
|
||||
quote.attachments = (this.quote.attachments || []).map(attachment => {
|
||||
const quotedAttachment = new QuotedAttachment();
|
||||
|
||||
quotedAttachment.contentType = attachment.contentType;
|
||||
quotedAttachment.fileName = attachment.fileName;
|
||||
if (attachment.attachmentPointer) {
|
||||
quotedAttachment.thumbnail = attachment.attachmentPointer;
|
||||
}
|
||||
|
||||
return quotedAttachment;
|
||||
});
|
||||
}
|
||||
if (this.expireTimer) {
|
||||
proto.expireTimer = this.expireTimer;
|
||||
}
|
||||
|
||||
if (this.profileKey) {
|
||||
proto.profileKey = this.profileKey;
|
||||
}
|
||||
|
||||
// Set the loki profile
|
||||
if (this.profile) {
|
||||
const profile = new textsecure.protobuf.DataMessage.LokiProfile();
|
||||
if (this.profile.displayName) {
|
||||
profile.displayName = this.profile.displayName;
|
||||
}
|
||||
|
||||
const conversation = window
|
||||
.getConversationController()
|
||||
.get(textsecure.storage.user.getNumber());
|
||||
const avatarPointer = conversation.get('avatarPointer');
|
||||
if (avatarPointer) {
|
||||
profile.avatar = avatarPointer;
|
||||
}
|
||||
proto.profile = profile;
|
||||
}
|
||||
|
||||
if (this.groupInvitation) {
|
||||
proto.groupInvitation = new textsecure.protobuf.DataMessage.GroupInvitation(
|
||||
{
|
||||
serverAddress: this.groupInvitation.serverAddress,
|
||||
channelId: this.groupInvitation.channelId,
|
||||
serverName: this.groupInvitation.serverName,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (this.sessionRestoration) {
|
||||
proto.flags = textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
|
||||
}
|
||||
|
||||
this.dataMessage = proto;
|
||||
return proto;
|
||||
},
|
||||
toArrayBuffer() {
|
||||
return this.toProto().toArrayBuffer();
|
||||
},
|
||||
};
|
||||
|
||||
function MessageSender() {
|
||||
// Currently only used for getProxiedSize() and makeProxiedRequest(), which are only used for fetching previews
|
||||
this.server = WebAPI.connect();
|
||||
}
|
||||
|
||||
MessageSender.prototype = {
|
||||
constructor: MessageSender,
|
||||
|
||||
async sendContactSyncMessage(convos) {
|
||||
let convosToSync;
|
||||
if (!convos) {
|
||||
convosToSync = await libsession.Utils.SyncMessageUtils.getSyncContacts();
|
||||
} else {
|
||||
convosToSync = convos;
|
||||
}
|
||||
|
||||
if (convosToSync.size === 0) {
|
||||
window.log.info('No contacts to sync.');
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
libloki.api.debug.logContactSync(
|
||||
'Triggering contact sync message with:',
|
||||
convosToSync
|
||||
);
|
||||
|
||||
// We need to sync across 3 contacts at a time
|
||||
// This is to avoid hitting storage server limit
|
||||
const chunked = _.chunk(convosToSync, 3);
|
||||
const syncMessages = await Promise.all(
|
||||
chunked.map(c => libloki.api.createContactSyncMessage(c))
|
||||
);
|
||||
|
||||
const syncPromises = syncMessages.map(syncMessage =>
|
||||
libsession.getMessageQueue().sendSyncMessage(syncMessage)
|
||||
);
|
||||
|
||||
return Promise.all(syncPromises);
|
||||
},
|
||||
|
||||
sendGroupSyncMessage(conversations) {
|
||||
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
||||
// primaryDevicePubKey is set to our own number if we are the master device
|
||||
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
||||
if (!primaryDeviceKey) {
|
||||
window.log.debug('sendGroupSyncMessage: no primary device pubkey');
|
||||
return Promise.resolve();
|
||||
}
|
||||
// We only want to sync across closed groups that we haven't left
|
||||
const activeGroups = conversations.filter(
|
||||
c => c.isClosedGroup() && !c.get('left') && !c.get('isKickedFromGroup')
|
||||
);
|
||||
if (activeGroups.length === 0) {
|
||||
window.log.info('No closed group to sync.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const mediumGroups = activeGroups.filter(c => c.isMediumGroup());
|
||||
|
||||
window.libsession.ClosedGroupV2.syncMediumGroups(mediumGroups);
|
||||
|
||||
const legacyGroups = activeGroups.filter(c => !c.isMediumGroup());
|
||||
|
||||
// We need to sync across 1 group at a time
|
||||
// This is because we could hit the storage server limit with one group
|
||||
const syncPromises = legacyGroups
|
||||
.map(c => libloki.api.createGroupSyncMessage(c))
|
||||
.map(syncMessage =>
|
||||
libsession.getMessageQueue().sendSyncMessage(syncMessage)
|
||||
);
|
||||
|
||||
return Promise.all(syncPromises);
|
||||
},
|
||||
|
||||
async sendOpenGroupsSyncMessage(convos) {
|
||||
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
||||
// primaryDevicePubKey is set to our own number if we are the master device
|
||||
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
||||
if (!primaryDeviceKey) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const conversations = Array.isArray(convos) ? convos : [convos];
|
||||
|
||||
const openGroupsConvos = await libsession.Utils.SyncMessageUtils.filterOpenGroupsConvos(
|
||||
conversations
|
||||
);
|
||||
|
||||
if (!openGroupsConvos.length) {
|
||||
window.log.info('No open groups to sync');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Send the whole list of open groups in a single message
|
||||
const openGroupsDetails = openGroupsConvos.map(conversation => ({
|
||||
url: conversation.id,
|
||||
channelId: conversation.get('channelId'),
|
||||
}));
|
||||
const openGroupsSyncParams = {
|
||||
timestamp: Date.now(),
|
||||
openGroupsDetails,
|
||||
};
|
||||
const openGroupsSyncMessage = new libsession.Messages.Outgoing.OpenGroupSyncMessage(
|
||||
openGroupsSyncParams
|
||||
);
|
||||
|
||||
return libsession.getMessageQueue().sendSyncMessage(openGroupsSyncMessage);
|
||||
},
|
||||
async sendBlockedListSyncMessage() {
|
||||
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
||||
// primaryDevicePubKey is set to our own number if we are the master device
|
||||
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
||||
if (!primaryDeviceKey) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const currentlyBlockedNumbers = window.BlockedNumberController.getBlockedNumbers();
|
||||
|
||||
// currently we only sync user blocked, not groups
|
||||
const blockedSyncMessage = new libsession.Messages.Outgoing.BlockedListSyncMessage(
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
numbers: currentlyBlockedNumbers,
|
||||
groups: [],
|
||||
}
|
||||
);
|
||||
return libsession.getMessageQueue().sendSyncMessage(blockedSyncMessage);
|
||||
},
|
||||
syncReadMessages(reads) {
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
// FIXME currently not in used
|
||||
if (myDevice !== 1 && myDevice !== '1') {
|
||||
const syncReadMessages = new libsession.Messages.Outgoing.SyncReadMessage(
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
readMessages: reads,
|
||||
}
|
||||
);
|
||||
return libsession.getMessageQueue().sendSyncMessage(syncReadMessages);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
async syncVerification(destination, state, identityKey) {
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
// FIXME currently not in used
|
||||
if (myDevice === 1 || myDevice === '1') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// send a session established message (used as a nullMessage)
|
||||
const destinationPubKey = new libsession.Types.PubKey(destination);
|
||||
|
||||
const sessionEstablished = new window.libsession.Messages.Outgoing.SessionEstablishedMessage(
|
||||
{ timestamp: Date.now() }
|
||||
);
|
||||
const { padding } = sessionEstablished;
|
||||
await libsession
|
||||
.getMessageQueue()
|
||||
.send(destinationPubKey, sessionEstablished);
|
||||
|
||||
const verifiedSyncParams = {
|
||||
state,
|
||||
destination: destinationPubKey,
|
||||
identityKey,
|
||||
padding,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const verifiedSyncMessage = new window.libsession.Messages.Outgoing.VerifiedSyncMessage(
|
||||
verifiedSyncParams
|
||||
);
|
||||
|
||||
return libsession.getMessageQueue().sendSyncMessage(verifiedSyncMessage);
|
||||
},
|
||||
|
||||
makeProxiedRequest(url, options) {
|
||||
return this.server.makeProxiedRequest(url, options);
|
||||
},
|
||||
getProxiedSize(url) {
|
||||
return this.server.getProxiedSize(url);
|
||||
},
|
||||
};
|
||||
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
textsecure.MessageSender = function MessageSenderWrapper() {
|
||||
const sender = new MessageSender();
|
||||
this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
|
||||
this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
|
||||
this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.bind(
|
||||
sender
|
||||
);
|
||||
this.syncReadMessages = sender.syncReadMessages.bind(sender);
|
||||
this.syncVerification = sender.syncVerification.bind(sender);
|
||||
this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
|
||||
this.getProxiedSize = sender.getProxiedSize.bind(sender);
|
||||
this.sendBlockedListSyncMessage = sender.sendBlockedListSyncMessage.bind(
|
||||
sender
|
||||
);
|
||||
};
|
||||
|
||||
textsecure.MessageSender.prototype = {
|
||||
constructor: textsecure.MessageSender,
|
||||
};
|
@ -1,88 +0,0 @@
|
||||
/* global Event, textsecure, window, libsession */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
async function SyncRequest() {
|
||||
// this.receiver = receiver;
|
||||
|
||||
window.log.info('SyncRequest created. Sending config sync request...');
|
||||
const { CONFIGURATION } = textsecure.protobuf.SyncMessage.Request.Type;
|
||||
const { RequestSyncMessage } = window.libsession.Messages.Outgoing;
|
||||
|
||||
const requestConfigurationSyncMessage = new RequestSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
reqestType: CONFIGURATION,
|
||||
});
|
||||
|
||||
await libsession
|
||||
.getMessageQueue()
|
||||
.sendSyncMessage(requestConfigurationSyncMessage);
|
||||
|
||||
window.log.info('SyncRequest now sending contact sync message...');
|
||||
const { CONTACTS } = textsecure.protobuf.SyncMessage.Request.Type;
|
||||
const requestContactSyncMessage = new RequestSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
reqestType: CONTACTS,
|
||||
});
|
||||
await libsession
|
||||
.getMessageQueue()
|
||||
.sendSyncMessage(requestContactSyncMessage);
|
||||
|
||||
window.log.info('SyncRequest now sending group sync messsage...');
|
||||
const { GROUPS } = textsecure.protobuf.SyncMessage.Request.Type;
|
||||
const requestGroupSyncMessage = new RequestSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
reqestType: GROUPS,
|
||||
});
|
||||
await libsession.getMessageQueue().sendSyncMessage(requestGroupSyncMessage);
|
||||
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
|
||||
}
|
||||
|
||||
SyncRequest.prototype = new textsecure.EventTarget();
|
||||
SyncRequest.prototype.extend({
|
||||
constructor: SyncRequest,
|
||||
onContactSyncComplete() {
|
||||
this.contactSync = true;
|
||||
this.update();
|
||||
},
|
||||
onGroupSyncComplete() {
|
||||
this.groupSync = true;
|
||||
this.update();
|
||||
},
|
||||
update() {
|
||||
if (this.contactSync && this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
this.cleanup();
|
||||
}
|
||||
},
|
||||
onTimeout() {
|
||||
if (this.contactSync || this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
} else {
|
||||
this.dispatchEvent(new Event('timeout'));
|
||||
}
|
||||
this.cleanup();
|
||||
},
|
||||
cleanup() {
|
||||
clearTimeout(this.timeout);
|
||||
delete this.listeners;
|
||||
},
|
||||
});
|
||||
|
||||
textsecure.SyncRequest = function SyncRequestWrapper() {
|
||||
const syncRequest = new SyncRequest();
|
||||
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
|
||||
this.removeEventListener = syncRequest.removeEventListener.bind(
|
||||
syncRequest
|
||||
);
|
||||
};
|
||||
|
||||
textsecure.SyncRequest.prototype = {
|
||||
constructor: textsecure.SyncRequest,
|
||||
};
|
||||
})();
|
@ -1,13 +0,0 @@
|
||||
window.setImmediate = window.nodeSetImmediate;
|
||||
|
||||
const fakeCall = () => Promise.resolve();
|
||||
|
||||
const fakeAPI = {
|
||||
getAttachment: fakeCall,
|
||||
putAttachment: fakeCall,
|
||||
putAvatar: fakeCall,
|
||||
};
|
||||
|
||||
window.WebAPI = {
|
||||
connect: () => fakeAPI,
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
describe('Helpers', () => {
|
||||
describe('ArrayBuffer->String conversion', () => {
|
||||
it('works', () => {
|
||||
const b = new ArrayBuffer(3);
|
||||
const a = new Uint8Array(b);
|
||||
a[0] = 0;
|
||||
a[1] = 255;
|
||||
a[2] = 128;
|
||||
assert.equal(getString(b), '\x00\xff\x80');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringToArrayBuffer', () => {
|
||||
it('returns ArrayBuffer when passed string', () => {
|
||||
const anArrayBuffer = new ArrayBuffer(1);
|
||||
const typedArray = new Uint8Array(anArrayBuffer);
|
||||
typedArray[0] = 'a'.charCodeAt(0);
|
||||
assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer);
|
||||
});
|
||||
it('throws an error when passed a non string', () => {
|
||||
const notStringable = [{}, undefined, null, new ArrayBuffer()];
|
||||
notStringable.forEach(notString => {
|
||||
assert.throw(() => {
|
||||
stringToArrayBuffer(notString);
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,104 +0,0 @@
|
||||
/* global libsignal, textsecure, SignalProtocolStore */
|
||||
|
||||
describe('MessageReceiver', () => {
|
||||
textsecure.storage.impl = new SignalProtocolStore();
|
||||
const { WebSocket } = window;
|
||||
const number = '+19999999999';
|
||||
const deviceId = 1;
|
||||
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
|
||||
textsecure.storage.put('password', 'password');
|
||||
textsecure.storage.put('signaling_key', signalingKey);
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = WebSocket;
|
||||
});
|
||||
|
||||
describe('connecting', () => {
|
||||
const attrs = {
|
||||
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
|
||||
source: number,
|
||||
sourceDevice: deviceId,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const websocketmessage = new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: { verb: 'PUT', path: '/messages' },
|
||||
});
|
||||
|
||||
before(done => {
|
||||
const signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
|
||||
|
||||
const aesKey = signalingKey.slice(0, 32);
|
||||
const macKey = signalingKey.slice(32, 32 + 20);
|
||||
|
||||
window.crypto.subtle
|
||||
.importKey('raw', aesKey, { name: 'AES-CBC' }, false, ['encrypt'])
|
||||
.then(key => {
|
||||
const iv = libsignal.crypto.getRandomBytes(16);
|
||||
window.crypto.subtle
|
||||
.encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal)
|
||||
.then(ciphertext => {
|
||||
window.crypto.subtle
|
||||
.importKey(
|
||||
'raw',
|
||||
macKey,
|
||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
||||
false,
|
||||
['sign']
|
||||
)
|
||||
.then(innerKey => {
|
||||
window.crypto.subtle
|
||||
.sign({ name: 'HMAC', hash: 'SHA-256' }, innerKey, signal)
|
||||
.then(mac => {
|
||||
const version = new Uint8Array([1]);
|
||||
const message = dcodeIO.ByteBuffer.concat([
|
||||
version,
|
||||
iv,
|
||||
ciphertext,
|
||||
mac,
|
||||
]);
|
||||
websocketmessage.request.body = message.toArrayBuffer();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('connects', done => {
|
||||
const mockServer = new MockServer(
|
||||
`ws://localhost:8080/v1/websocket/?login=${encodeURIComponent(
|
||||
number
|
||||
)}.1&password=password`
|
||||
);
|
||||
|
||||
mockServer.on('connection', server => {
|
||||
server.send(new Blob([websocketmessage.toArrayBuffer()]));
|
||||
});
|
||||
|
||||
window.addEventListener('textsecure:message', ev => {
|
||||
const signal = ev.proto;
|
||||
const keys = Object.keys(attrs);
|
||||
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
assert.strictEqual(attrs[key], signal[key]);
|
||||
}
|
||||
assert.strictEqual(signal.message.body, 'hello');
|
||||
mockServer.close();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
window.messageReceiver = new textsecure.MessageReceiver(
|
||||
'signalingKey'
|
||||
// 'ws://localhost:8080',
|
||||
// window,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,66 +0,0 @@
|
||||
/* global TextSecureWebSocket */
|
||||
|
||||
describe('TextSecureWebSocket', () => {
|
||||
const RealWebSocket = window.WebSocket;
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = RealWebSocket;
|
||||
});
|
||||
it('connects and disconnects', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8080');
|
||||
mockServer.on('connection', server => {
|
||||
socket.close();
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8080');
|
||||
});
|
||||
|
||||
it('sends and receives', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8080');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', () => {
|
||||
server.send('ack');
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8080');
|
||||
socket.onmessage = response => {
|
||||
assert.strictEqual(response.data, 'ack');
|
||||
socket.close();
|
||||
done();
|
||||
};
|
||||
socket.send('syn');
|
||||
});
|
||||
|
||||
it('exposes the socket status', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8082');
|
||||
mockServer.on('connection', server => {
|
||||
assert.strictEqual(socket.getStatus(), WebSocket.OPEN);
|
||||
server.close();
|
||||
socket.close();
|
||||
});
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8082');
|
||||
socket.onclose = () => {
|
||||
assert.strictEqual(socket.getStatus(), WebSocket.CLOSING);
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it('reconnects', function thisNeeded(done) {
|
||||
this.timeout(60000);
|
||||
const mockServer = new MockServer('ws://localhost:8082');
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8082');
|
||||
socket.onclose = () => {
|
||||
const secondServer = new MockServer('ws://localhost:8082');
|
||||
secondServer.on('connection', server => {
|
||||
socket.close();
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
};
|
||||
mockServer.close();
|
||||
});
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
import { SyncMessage } from './SyncMessage';
|
||||
import { SignalService } from '../../../../../protobuf';
|
||||
import { MessageParams } from '../../Message';
|
||||
import { Constants } from '../../../..';
|
||||
|
||||
interface RequestSyncMessageParams extends MessageParams {
|
||||
requestType: SignalService.SyncMessage.Request.Type;
|
||||
}
|
||||
|
||||
export abstract class RequestSyncMessage extends SyncMessage {
|
||||
private readonly requestType: SignalService.SyncMessage.Request.Type;
|
||||
|
||||
constructor(params: RequestSyncMessageParams) {
|
||||
super({ timestamp: params.timestamp, identifier: params.identifier });
|
||||
this.requestType = params.requestType;
|
||||
}
|
||||
|
||||
public ttl(): number {
|
||||
return Constants.TTL_DEFAULT.REGULAR_MESSAGE;
|
||||
}
|
||||
|
||||
public contentProto(): SignalService.Content {
|
||||
return new SignalService.Content({
|
||||
syncMessage: this.syncProto(),
|
||||
});
|
||||
}
|
||||
|
||||
protected syncProto(): SignalService.SyncMessage {
|
||||
const syncMessage = super.syncProto();
|
||||
syncMessage.request = new SignalService.SyncMessage.Request({
|
||||
type: this.requestType,
|
||||
});
|
||||
|
||||
return syncMessage;
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import { SyncMessage } from './SyncMessage';
|
||||
import { SignalService } from '../../../../../protobuf';
|
||||
import { MessageParams } from '../../Message';
|
||||
import { PubKey } from '../../../../types';
|
||||
|
||||
interface VerifiedSyncMessageParams extends MessageParams {
|
||||
padding: Buffer;
|
||||
identityKey: Uint8Array;
|
||||
destination: PubKey;
|
||||
state: SignalService.Verified.State;
|
||||
}
|
||||
|
||||
export abstract class VerifiedSyncMessage extends SyncMessage {
|
||||
public readonly state: SignalService.Verified.State;
|
||||
public readonly destination: PubKey;
|
||||
public readonly identityKey: Uint8Array;
|
||||
public readonly padding: Buffer;
|
||||
|
||||
constructor(params: VerifiedSyncMessageParams) {
|
||||
super({ timestamp: params.timestamp, identifier: params.identifier });
|
||||
this.state = params.state;
|
||||
this.destination = params.destination;
|
||||
this.identityKey = params.identityKey;
|
||||
this.padding = params.padding;
|
||||
}
|
||||
|
||||
protected syncProto(): SignalService.SyncMessage {
|
||||
const syncMessage = super.syncProto();
|
||||
syncMessage.verified = new SignalService.Verified();
|
||||
syncMessage.verified.state = this.state;
|
||||
syncMessage.verified.destination = this.destination.key;
|
||||
syncMessage.verified.identityKey = this.identityKey;
|
||||
syncMessage.verified.nullMessage = this.padding;
|
||||
|
||||
return syncMessage;
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
export * from './RequestSyncMessage';
|
||||
export * from './ContactSyncMessage';
|
||||
export * from './ClosedGroupSyncMessage';
|
||||
export * from './OpenGroupSyncMessage';
|
||||
export * from './SyncMessage';
|
||||
export * from './SentSyncMessage';
|
||||
export * from './SyncReadMessage';
|
||||
export * from './VerifiedSyncMessage';
|
||||
export * from './BlockedListSyncMessage';
|
||||
|
Loading…
Reference in New Issue