Merge pull request #787 from neuroscr/tls-fix

Quality of life fixes
pull/794/head
Ryan Tharp 5 years ago committed by GitHub
commit 7919d22397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,4 +19,4 @@ If applicable, add screenshots or log files to help explain your problem.
* Device: [e.g. PC, Mac]
* OS: [e.g. Ubuntu 16.04, Windows 10]
* Loki messenger Version or Git commit hash:
* Session Version or Git commit hash:

@ -2,11 +2,11 @@
[![Build Status](https://travis-ci.org/loki-project/loki-messenger.svg?branch=development)](https://travis-ci.org/loki-project/loki-messenger)
Session allows for truly decentralized, end to end, and private encrypted chats. Session is built to handle both online and fully Asynchronous offline messages. Loki messenger implements the Signal protocol for message encryption. Our Client interface is a fork of [Signal Messenger](https://signal.org/). All communication that passes through Loki messenger is routed through [Lokinet](https://github.com/loki-project/loki-network).
Session allows for truly decentralized, end to end, and private encrypted chats. Session is built to handle both online and fully Asynchronous offline messages. Session implements the Signal protocol for message encryption. Our Client interface is a fork of [Signal Messenger](https://signal.org/). All communication that passes through Session is routed through [Lokinet](https://github.com/loki-project/loki-network).
## Summary
Loki messenger integrates directly with Loki [Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as both federated servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Loki messenger works, read the [Loki whitepaper](https://loki.network/whitepaper).
Session integrates directly with Loki [Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as both federated servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Session works, read the [Loki whitepaper](https://loki.network/whitepaper).
**Online Messages**

@ -2226,7 +2226,7 @@
},
"setAccountPasswordDescription": {
"message":
"Require password to unlock Sessions screen. You can still receive message notifications while Screen Lock is enabled. Loki Messengers notification settings allow you to customize information that is displayed",
"Require password to unlock Sessions screen. You can still receive message notifications while Screen Lock is enabled. Sessions notification settings allow you to customize information that is displayed",
"description": "Description for set account password setting view"
},
"changeAccountPasswordTitle": {

@ -1067,6 +1067,7 @@
const sslServerURL = `https://${rawserverURL}`;
const conversationId = `publicChat:${channelId}@${rawserverURL}`;
// quickly peak to make sure we don't already have it
const conversationExists = window.ConversationController.get(
conversationId
);
@ -1077,9 +1078,11 @@
});
}
// get server
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(
sslServerURL
);
// SSL certificate failure or offline
if (!serverAPI) {
// Url incorrect or server not compatible
return new Promise((_resolve, reject) => {
@ -1087,16 +1090,20 @@
});
}
// create conversation
const conversation = await window.ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
// convert conversation to a public one
await conversation.setPublicSource(sslServerURL, channelId);
// set friend and appropriate SYNC messages for multidevice
await conversation.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends
);
// and finally activate it
conversation.getPublicSendData(); // may want "await" if you want to use the API
return conversation;
@ -1966,7 +1973,10 @@
}
const isDuplicate = await isMessageDuplicate(message);
if (isDuplicate) {
window.log.warn('Received duplicate message', message.idForLogging());
// RSS expects duplciates, so squelch log
if (!descriptorId.match(/^rss:/)) {
window.log.warn('Received duplicate message', message.idForLogging());
}
return event.confirm();
}

@ -14,12 +14,19 @@
LokiFileServerAPI.secureRpcPubKey
);
let nextWaitSeconds = 1;
const checkForUpgrades = async () => {
const response = await window.tokenlessFileServerAdnAPI.serverRequest(
const result = await window.tokenlessFileServerAdnAPI.serverRequest(
'loki/v1/version/client/desktop'
);
if (response && response.response) {
const latestVer = semver.clean(response.response.data[0][0]);
if (
result &&
result.response &&
result.response.data &&
result.response.data.length &&
result.response.data[0].length
) {
const latestVer = semver.clean(result.response.data[0][0]);
if (semver.valid(latestVer)) {
const ourVersion = window.getVersion();
if (latestVer === ourVersion) {
@ -36,10 +43,11 @@
}
} else {
// give it a minute
log.warn('Could not check to see if newer version is available');
log.warn('Could not check to see if newer version is available', result);
nextWaitSeconds = 60;
setTimeout(async () => {
await checkForUpgrades();
}, 60 * 1000); // wait a minute
}, nextWaitSeconds * 1000); // wait a minute
}
// no message logged means serverRequest never returned...
};
@ -56,22 +64,33 @@
if (expiredVersion !== null) {
return res(expiredVersion);
}
log.info('Delaying sending checks for 1s, no version yet');
setTimeout(waitForVersion, 1000);
log.info(
'Delaying sending checks for',
nextWaitSeconds,
's, no version yet'
);
setTimeout(waitForVersion, nextWaitSeconds * 1000);
return true;
}
waitForVersion();
return true;
};
// just get current status
window.extension.expiredStatus = () => expiredVersion;
// actually wait until we know for sure
window.extension.expiredPromise = () => new Promise(resolveWhenReady);
window.extension.expired = cb => {
if (expiredVersion === null) {
// just give it another second
log.info('Delaying expire banner determination for 1s');
log.info(
'Delaying expire banner determination for',
nextWaitSeconds,
's'
);
setTimeout(() => {
window.extension.expired(cb);
}, 1000);
}, nextWaitSeconds * 1000);
return;
}
// yes we know

@ -561,6 +561,7 @@
type: this.isPrivate() ? 'direct' : 'group',
isMe: this.isMe(),
isPublic: this.isPublic(),
isRss: this.isRss(),
isClosable: this.isClosable(),
isTyping: typingKeys.length > 0,
lastUpdated: this.get('timestamp'),
@ -2798,11 +2799,19 @@
this.messageCollection.reset([]);
this.set({
lastMessage: null,
timestamp: null,
active_at: null,
});
// let's try to keep the RSS conversation open just empty...
if (this.isRss()) {
this.set({
lastMessage: null,
});
} else {
// this will remove the conversation from conversation lists...
this.set({
lastMessage: null,
timestamp: null,
active_at: null,
});
}
// Reset our friend status if we're not friends
if (!this.isFriend()) {

@ -185,6 +185,15 @@ class LokiAppDotNetServerAPI {
}
this.token = token;
// if no token to verify, just bail now
if (!token) {
//
if (!forceRefresh) {
token = await this.getOrRefreshServerToken(true);
}
return token;
}
// verify token info
const tokenRes = await this.serverRequest('token');
// if no problems and we have data
@ -270,7 +279,31 @@ class LokiAppDotNetServerAPI {
res = await this.proxyFetch(url);
} catch (e) {
log.error('requestToken request failed', e);
// should we retry here?
// no, this is the low level function
// not really an error, from a client's pov, network servers can fail...
if (e.code === 'ECONNREFUSED') {
// down
log.warn(
'requestToken request can not connect',
this.baseServerUrl,
e.message
);
} else if (e.code === 'ECONNRESET') {
// got disconnected
log.warn(
'requestToken request lost connection',
this.baseServerUrl,
e.message
);
} else {
log.error(
'requestToken request failed',
this.baseServerUrl,
e.code,
e.message
);
}
return null;
}
if (!res.ok) {
@ -302,14 +335,17 @@ class LokiAppDotNetServerAPI {
);
return res.ok;
} catch (e) {
log.error('submitToken proxyFetch failure', e.code, e.message);
return false;
}
}
async proxyFetch(urlObj, fetchOptions) {
async proxyFetch(urlObj, fetchOptions = { method: 'GET' }) {
if (
window.lokiFeatureFlags.useSnodeProxy &&
(this.baseServerUrl === 'https://file-dev.lokinet.org' ||
this.baseServerUrl === 'https://file.lokinet.org' ||
this.baseServerUrl === 'https://file-dev.getsession.org' ||
this.baseServerUrl === 'https://file.getsession.org')
) {
const finalOptions = { ...fetchOptions };
@ -408,7 +444,21 @@ class LokiAppDotNetServerAPI {
const result = await nodeFetch(url, firstHopOptions);
const txtResponse = await result.text();
let response = JSON.parse(txtResponse);
if (txtResponse === 'Service node is not ready: not in any swarm; \n') {
// mark snode bad
log.warn('Marking random snode bad', randSnode);
lokiSnodeAPI.markRandomNodeUnreachable(randSnode);
// retry (hopefully with new snode)
// FIXME: max number of retries...
return this._sendToProxy(endpoint, fetchOptions);
}
let response = {};
try {
response = JSON.parse(txtResponse);
} catch (e) {
log.warn(`_sendToProxy Could not parse outer JSON [${txtResponse}]`);
}
if (response.meta && response.meta.code === 200) {
// convert base64 in response to binary
@ -423,9 +473,17 @@ class LokiAppDotNetServerAPI {
const textDecoder = new TextDecoder();
const json = textDecoder.decode(decrypted);
// replace response
response = JSON.parse(json);
try {
response = JSON.parse(json);
} catch (e) {
log.warn(`_sendToProxy Could not parse inner JSON [${json}]`);
}
} else {
log.warn('file server secure_rpc gave an non-200 response');
log.warn(
'file server secure_rpc gave an non-200 response: ',
response,
` txtResponse[${txtResponse}]`
);
}
return { result, txtResponse, response };
}
@ -469,7 +527,7 @@ class LokiAppDotNetServerAPI {
fetchOptions.agent = snodeHttpsAgent;
}
} catch (e) {
log.info('serverRequest set up error:', JSON.stringify(e));
log.info('serverRequest set up error:', e.code, e.message);
return {
err: e,
};
@ -483,6 +541,8 @@ class LokiAppDotNetServerAPI {
if (
window.lokiFeatureFlags.useSnodeProxy &&
(this.baseServerUrl === 'https://file-dev.lokinet.org' ||
this.baseServerUrl === 'https://file.lokinet.org' ||
this.baseServerUrl === 'https://file-dev.getsession.org' ||
this.baseServerUrl === 'https://file.getsession.org')
) {
mode = '_sendToProxy';
@ -902,7 +962,11 @@ class LokiPublicChannelAPI {
try {
await this.pollOnceForModerators();
} catch (e) {
log.warn(`Error while polling for public chat moderators: ${e}`);
log.warn(
'Error while polling for public chat moderators:',
e.code,
e.message
);
}
if (this.running) {
this.timers.moderator = setTimeout(() => {
@ -1052,7 +1116,11 @@ class LokiPublicChannelAPI {
try {
await this.pollForChannelOnce();
} catch (e) {
log.warn(`Error while polling for public chat room details: ${e}`);
log.warn(
'Error while polling for public chat room details',
e.code,
e.message
);
}
if (this.running) {
this.timers.channel = setTimeout(() => {
@ -1103,7 +1171,11 @@ class LokiPublicChannelAPI {
try {
await this.pollOnceForDeletions();
} catch (e) {
log.warn(`Error while polling for public chat deletions: ${e}`);
log.warn(
'Error while polling for public chat deletions:',
e.code,
e.message
);
}
if (this.running) {
this.timers.delete = setTimeout(() => {
@ -1278,7 +1350,11 @@ class LokiPublicChannelAPI {
try {
await this.pollOnceForMessages();
} catch (e) {
log.warn(`Error while polling for public chat messages: ${e}`);
log.warn(
'Error while polling for public chat messages:',
e.code,
e.message
);
}
if (this.running) {
this.timers.message = setTimeout(() => {

@ -288,7 +288,7 @@ class LokiMessageAPI {
// Execute callback even with empty array to signal online status
callback(messages);
} catch (e) {
log.warn('Loki retrieve messages:', e);
log.warn('Loki retrieve messages:', e.code, e.message);
if (e instanceof textsecure.WrongSwarmError) {
const { newSwarm } = e;
await lokiSnodeAPI.updateSwarmNodes(this.ourKey, newSwarm);
@ -352,7 +352,6 @@ class LokiMessageAPI {
const lastHash = await window.Signal.Data.getLastHashBySnode(
nodes[i].address
);
this.ourSwarmNodes[nodes[i].address] = {
...nodes[i],
lastHash,

@ -1,4 +1,4 @@
/* global log, window */
/* global log, window, process */
const EventEmitter = require('events');
const nodeFetch = require('node-fetch');
const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
@ -17,6 +17,26 @@ class LokiPublicChatFactoryAPI extends EventEmitter {
await Promise.all(this.servers.map(server => server.close()));
}
static async validServer(serverUrl) {
// test to make sure it's online (and maybe has a valid SSL cert)
try {
// allow .loki (may only need an agent but not sure
// until we have a .loki to test with)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = serverUrl.match(/\.loki\//)
? 0
: 1;
await nodeFetch(serverUrl);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 1;
// const txt = await res.text();
} catch (e) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 1;
log.warn(`failing to created ${serverUrl}`, e.code, e.message);
// bail out if not valid enough
return false;
}
return true;
}
// server getter/factory
async findOrCreateServer(serverUrl) {
let thisServer = this.servers.find(
@ -24,7 +44,14 @@ class LokiPublicChatFactoryAPI extends EventEmitter {
);
if (!thisServer) {
log.info(`LokiAppDotNetAPI creating ${serverUrl}`);
if (!await this.constructor.validServer(serverUrl)) {
return null;
}
// after verification then we can start up all the pollers
thisServer = new LokiAppDotNetAPI(this.ourKey, serverUrl);
const gotToken = await thisServer.getOrRefreshServerToken();
if (!gotToken) {
log.warn(`Invalid server ${serverUrl}`);

@ -13,16 +13,18 @@ const LOKI_EPHEMKEY_HEADER = 'X-Loki-EphemKey';
const endpointBase = '/storage_rpc/v1';
const decryptResponse = async (response, address) => {
let plaintext = false;
try {
const ciphertext = await response.text();
const plaintext = await libloki.crypto.snodeCipher.decrypt(
address,
ciphertext
);
plaintext = await libloki.crypto.snodeCipher.decrypt(address, ciphertext);
const result = plaintext === '' ? {} : JSON.parse(plaintext);
return result;
} catch (e) {
log.warn(`Could not decrypt response from ${address}`, e);
log.warn(
`Could not decrypt response [${plaintext}] from [${address}],`,
e.code,
e.message
);
}
return {};
};
@ -33,12 +35,6 @@ const sendToProxy = async (options = {}, targetNode) => {
const url = `https://${randSnode.ip}:${randSnode.port}/proxy`;
log.info(
`Proxy snode request to ${targetNode.pubkey_ed25519} via ${
randSnode.pubkey_ed25519
}`
);
const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519);
const myKeys = window.libloki.crypto.snodeCipher._ephemeralKeyPair;
@ -86,11 +82,21 @@ const sendToProxy = async (options = {}, targetNode) => {
const textDecoder = new TextDecoder();
const plaintext = textDecoder.decode(plaintextBuffer);
const jsonRes = JSON.parse(plaintext);
jsonRes.json = () => JSON.parse(jsonRes.body);
return jsonRes;
try {
const jsonRes = JSON.parse(plaintext);
// emulate nodeFetch response...
jsonRes.json = () => JSON.parse(jsonRes.body);
return jsonRes;
} catch (e) {
log.error(
'lokiRpc sendToProxy error',
e.code,
e.message,
'json',
plaintext
);
}
return false;
};
// A small wrapper around node-fetch which deserializes response

@ -9,8 +9,8 @@ class LokiSnodeAPI {
if (!is.string(serverUrl)) {
throw new Error('WebAPI.initialize: Invalid server url');
}
this.serverUrl = serverUrl;
this.localUrl = localUrl;
this.serverUrl = serverUrl; // random.snode
this.localUrl = localUrl; // localhost.loki
this.randomSnodePool = [];
this.swarmsPendingReplenish = {};
}
@ -63,7 +63,7 @@ class LokiSnodeAPI {
pubkey_ed25519: snode.pubkey_ed25519,
}));
} catch (e) {
log.warn('initialiseRandomPool error', JSON.stringify(e));
log.warn('initialiseRandomPool error', e.code, e.message);
if (seedNodes.length === 0) {
throw new window.textsecure.SeedNodeError(
'Failed to contact seed node'
@ -73,6 +73,7 @@ class LokiSnodeAPI {
}
}
// nodeUrl is like 9hrje1bymy7hu6nmtjme9idyu3rm8gr3mkstakjyuw1997t7w4ny.snode
async unreachableNode(pubKey, nodeUrl) {
const conversation = ConversationController.get(pubKey);
const swarmNodes = [...conversation.get('swarmNodes')];
@ -82,6 +83,13 @@ class LokiSnodeAPI {
await conversation.updateSwarmNodes(filteredNodes);
}
markRandomNodeUnreachable(snode) {
this.randomSnodePool = _.without(
this.randomSnodePool,
_.find(this.randomSnodePool, { ip: snode.ip, port: snode.port })
);
}
async updateLastHash(snode, hash, expiresAt) {
await window.Signal.Data.updateLastHash({ snode, hash, expiresAt });
}
@ -151,12 +159,8 @@ class LokiSnodeAPI {
const snodes = result.snodes.filter(tSnode => tSnode.ip !== '0.0.0.0');
return snodes;
} catch (e) {
log.error('getSwarmNodes', JSON.stringify(e));
//
this.randomSnodePool = _.without(
this.randomSnodePool,
_.find(this.randomSnodePool, { ip: snode.ip })
);
log.error('getSwarmNodes error', e.code, e.message);
this.markRandomNodeUnreachable(snode);
return this.getSwarmNodes(pubKey);
}
}

@ -1,4 +1,4 @@
/* global Whisper, i18n, ConversationController, friends */
/* global Whisper, i18n, log */
// eslint-disable-next-line func-names
(function() {
@ -24,31 +24,13 @@
'click .cancel': 'close',
},
async attemptConnection(serverUrl, channelId) {
const rawServerUrl = serverUrl
.replace(/^https?:\/\//i, '')
.replace(/[/\\]+$/i, '');
const sslServerUrl = `https://${rawServerUrl}`;
const conversationId = `publicChat:${channelId}@${rawServerUrl}`;
const conversationExists = ConversationController.get(conversationId);
if (conversationExists) {
// We are already a member of this public chat
return this.resolveWith({ errorCode: i18n('publicChatExists') });
let conversation = null;
try {
conversation = await window.attemptConnection(serverUrl, channelId);
} catch (e) {
log.error('can not connect', e.message, e.code);
return this.resolveWith({ errorCode: e.message });
}
// create conversation
const conversation = await ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
// convert conversation to a public one
await conversation.setPublicSource(sslServerUrl, channelId);
// set friend and appropriate SYNC messages for multidevice
await conversation.setFriendRequestStatus(
friends.friendRequestStatusEnum.friends
);
// and finally activate it
conversation.getPublicSendData(); // may want "await" if you want to use the API
return this.resolveWith({ conversation });
},
resolveWith(result) {

@ -1860,8 +1860,8 @@
message = window.Signal.Emoji.replaceColons(message).trim();
const toastOptions = { type: 'info' };
const expiredVersion = await extension.expiredPromise();
if (expiredVersion) {
// let it pass if we're still trying to read it or it's false...
if (extension.expiredStatus() === true) {
toastOptions.title = i18n('expiredWarning');
toastOptions.id = 'expiredWarning';
}

@ -23,6 +23,7 @@ export type PropsData = {
avatarPath?: string;
isMe: boolean;
isPublic?: boolean;
isRss?: boolean;
isClosable?: boolean;
lastUpdated: number;
@ -176,6 +177,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
isBlocked,
isMe,
isClosable,
isRss,
isPublic,
hasNickname,
onDeleteContact,
@ -192,18 +194,18 @@ export class ConversationListItem extends React.PureComponent<Props> {
return (
<ContextMenu id={triggerId}>
{!isPublic && !isMe ? (
{!isPublic && !isRss && !isMe ? (
<MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
) : null}
{!isPublic && !isMe ? (
{!isPublic && !isRss && !isMe ? (
<MenuItem onClick={onChangeNickname}>
{i18n('changeNickname')}
</MenuItem>
) : null}
{!isPublic && !isMe && hasNickname ? (
{!isPublic && !isRss && !isMe && hasNickname ? (
<MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem>
) : null}
{!isPublic ? (
{!isPublic && !isRss ? (
<MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem>
) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>

@ -319,7 +319,9 @@ export class ConversationHeader extends React.Component<Props> {
return (
<ContextMenu id={triggerId}>
{this.renderPublicMenuItems()}
<MenuItem onClick={onCopyPublicKey}>{copyIdLabel}</MenuItem>
{!isRss ? (
<MenuItem onClick={onCopyPublicKey}>{copyIdLabel}</MenuItem>
) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
{isPrivateGroup || amMod ? (
<MenuItem onClick={onUpdateGroup}>{i18n('updateGroup')}</MenuItem>
@ -477,7 +479,8 @@ export class ConversationHeader extends React.Component<Props> {
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
);
const blockHandlerMenuItem = !isMe &&
!isGroup && <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>;
!isGroup &&
!isRss && <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>;
const changeNicknameMenuItem = !isMe &&
!isGroup && (
<MenuItem onClick={onChangeNickname}>{i18n('changeNickname')}</MenuItem>

Loading…
Cancel
Save