Skip to content
This repository has been archived by the owner on Jun 3, 2020. It is now read-only.

Commit

Permalink
settings: Add an option to validate and add custom/self-signed certif…
Browse files Browse the repository at this point in the history
…icates.

This PR helps to validate custom/self-signed certificates for servers
by saving the certificate file in certificates folder in user's appData folder.
We now use this certificate with the request while validating the server
when adding the organization. This validation of certificate is done by the request module itself.

Fixes: #126.
  • Loading branch information
abhigyank authored and akashnimare committed Jun 22, 2018
1 parent 99a1711 commit 0a893c9
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 15 deletions.
2 changes: 1 addition & 1 deletion app/main/menu.js
Expand Up @@ -417,7 +417,7 @@ class AppMenu {
const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.';

// We save App's settings/configurations in following files
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
const settingFiles = ['window-state.json', 'domain.json', 'settings.json', 'certificates.json'];

dialog.showMessageBox({
type: 'warning',
Expand Down
25 changes: 25 additions & 0 deletions app/renderer/css/preference.css
Expand Up @@ -557,6 +557,31 @@ input.toggle-round:checked+label:after {
background: #329588;
}

.certificates-card {
width:70%
}

.certificate-input {
width:100%;
margin-top: 10px;
display:inline-flex;
}

.certificate-input div {
align-self:center;
}

.certificate-input .setting-input-value {
margin-left:10px;
max-width: 100%;
}

#add-certificate-button {
width:20%;
margin-right:0px;
height: 35px;
}

.tip {
background-color: hsl(46,63%,95%);
border: 1px solid hsl(46,63%,84%);
Expand Down
92 changes: 92 additions & 0 deletions app/renderer/js/pages/preference/add-certificate.js
@@ -0,0 +1,92 @@
'use-strict';

const { dialog } = require('electron').remote;

const BaseComponent = require(__dirname + '/../../components/base.js');
const CertificateUtil = require(__dirname + '/../../utils/certificate-util.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');

class AddCertificate extends BaseComponent {
constructor(props) {
super();
this.props = props;
this._certFile = '';
}

template() {
return `
<div class="settings-card server-center certificates-card">
<div class="certificate-input">
<div>Organization URL :</div>
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div>
<div class="certificate-input">
<div>Custom CA's certificate file :</div>
<button id="add-certificate-button">Add</button>
</div>
</div>
`;
}

init() {
this.$addCertificate = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$addCertificate);
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0];
this.initListeners();
}

validateAndAdd() {
const certificate = this._certFile;
const serverUrl = this.serverUrl.value;
if (certificate !== '' && serverUrl !== '') {
const server = encodeURIComponent(DomainUtil.formatUrl(serverUrl));
const fileName = certificate.substring(certificate.lastIndexOf('/') + 1);
const copy = CertificateUtil.copyCertificate(server, certificate, fileName);
if (!copy) {
console.log('We encountered error while saving the certificate.');
return;
}
CertificateUtil.setCertificate(server, fileName);
dialog.showMessageBox({
title: 'Success',
message: `Certificate saved!`
});
this.serverUrl.value = '';
} else {
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
'Enter an Organization URL' : 'Choose certificate file'}`);
}
}

addHandler() {
const showDialogOptions = {
title: 'Select file',
defaultId: 1,
properties: ['openFile'],
filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
};
dialog.showOpenDialog(showDialogOptions, selectedFile => {
if (selectedFile) {
this._certFile = selectedFile[0] || '';
this.validateAndAdd();
}
});
}

initListeners() {
this.addCertificateButton.addEventListener('click', () => {
this.addHandler();
});

this.serverUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode;

if (EnterkeyCode === 13) {
this.addHandler();
}
});
}
}

module.exports = AddCertificate;
13 changes: 13 additions & 0 deletions app/renderer/js/pages/preference/connected-org-section.js
Expand Up @@ -3,6 +3,7 @@
const BaseSection = require(__dirname + '/base-section.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const ServerInfoForm = require(__dirname + '/server-info-form.js');
const AddCertificate = require(__dirname + '/add-certificate.js');

class ConnectedOrgSection extends BaseSection {
constructor(props) {
Expand All @@ -16,6 +17,9 @@ class ConnectedOrgSection extends BaseSection {
<div class="page-title">Connected organizations</div>
<div class="title" id="existing-servers">All the connected orgnizations will appear here.</div>
<div id="server-info-container"></div>
<div class="page-title">Add Custom Certificates</div>
<div id="add-certificate-container"></div>
</div>
`;
}
Expand Down Expand Up @@ -44,6 +48,15 @@ class ConnectedOrgSection extends BaseSection {
onChange: this.reloadApp
}).init();
}

this.$addCertificateContainer = document.getElementById('add-certificate-container');
this.initAddCertificate();
}

initAddCertificate() {
new AddCertificate({
$root: this.$addCertificateContainer
}).init();
}

}
Expand Down
87 changes: 87 additions & 0 deletions app/renderer/js/utils/certificate-util.js
@@ -0,0 +1,87 @@
'use strict';

const { app, dialog } = require('electron').remote;
const fs = require('fs');
const path = require('path');
const JsonDB = require('node-json-db');
const Logger = require('./logger-util');
const { initSetUp } = require('./default-util');

initSetUp();

const logger = new Logger({
file: `certificate-util.log`,
timestamp: true
});

let instance = null;
const certificatesDir = `${app.getPath('userData')}/certificates`;

class CertificateUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}

this.reloadDB();
return instance;
}
getCertificate(server, defaultValue = null) {
this.reloadDB();
const value = this.db.getData('/')[server];
if (value === undefined) {
return defaultValue;
} else {
return value;
}
}
// Function to copy the certificate to userData folder
copyCertificate(server, location, fileName) {
let copied = false;
const filePath = `${certificatesDir}/${fileName}`;
try {
fs.copyFileSync(location, filePath);
copied = true;
} catch (err) {
dialog.showErrorBox(
'Error saving certificate',
'We encountered error while saving the certificate.'
);
logger.error('Error while copying the certificate to certificates folder.');
logger.error(err);
console.log(err);
}
return copied;
}
setCertificate(server, fileName) {
const filePath = `${certificatesDir}/${fileName}`;
this.db.push(`/${server}`, filePath, true);
this.reloadDB();
}
removeCertificate(server) {
this.db.delete(`/${server}`);
this.reloadDB();
}
reloadDB() {
const settingsJsonPath = path.join(app.getPath('userData'), '/certificates.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving the certificate.'
);
logger.error('Error while JSON parsing certificates.json: ');
logger.error(err);
}
}
this.db = new JsonDB(settingsJsonPath, true, true);
}
}

module.exports = new CertificateUtil();
6 changes: 6 additions & 0 deletions app/renderer/js/utils/default-util.js
Expand Up @@ -10,6 +10,7 @@ if (process.type === 'renderer') {

const zulipDir = app.getPath('userData');
const logDir = `${zulipDir}/Logs/`;
const certificatesDir = `${zulipDir}/certificates/`;
const initSetUp = () => {
// if it is the first time the app is running
// create zulip dir in userData folder to
Expand All @@ -22,6 +23,11 @@ const initSetUp = () => {
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}

if (!fs.existsSync(certificatesDir)) {
fs.mkdirSync(certificatesDir);
}

setupCompleted = true;
}
};
Expand Down
39 changes: 25 additions & 14 deletions app/renderer/js/utils/domain-util.js
Expand Up @@ -9,6 +9,8 @@ const escape = require('escape-html');

const Logger = require('./logger-util');

const CertificateUtil = require(__dirname + '/certificate-util.js');

const logger = new Logger({
file: `domain-util.log`,
timestamp: true
Expand Down Expand Up @@ -106,7 +108,19 @@ class DomainUtil {

domain = this.formatUrl(domain);

const checkDomain = domain + '/static/audio/zulip.ogg';
const certificate = CertificateUtil.getCertificate(encodeURIComponent(domain));
let certificateLocation = '';

if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificate);
} catch (err) {
console.log(err);
}
}
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
const checkDomain = (certificateLocation) ? ({url: domain + '/static/audio/zulip.ogg', ca: certificateLocation}) : domain + '/static/audio/zulip.ogg';

const serverConf = {
icon: defaultIconUrl,
Expand All @@ -116,29 +130,24 @@ class DomainUtil {

return new Promise((resolve, reject) => {
request(checkDomain, (error, response) => {
const certsError =
[
'Error: self signed certificate',
'Error: unable to verify the first certificate',
'Error: unable to get local issuer certificate'
];

// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];

// make sure that error is a error or string not undefined
// make sure that error is an error or string not undefined
// so validation does not throw error.
error = error || '';

const certsError = error.toString().includes('certificate');
if (!error && response.statusCode < 400) {
// Correct
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError.indexOf(error.toString()) >= 0) {
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
if (silent) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
Expand All @@ -147,9 +156,10 @@ class DomainUtil {
});
} else {
const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
const certErrorDetail = `The server you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\n Unless you have a good reason to believe otherwise, you should not proceed.
\n You can click here if you'd like to proceed with the connection.`;
const certErrorDetail = `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
\nUnless you have a good reason to believe otherwise, you should not proceed.
\nYou can click here if you'd like to proceed with the connection.`;

dialog.showMessageBox({
type: 'warning',
Expand All @@ -171,7 +181,8 @@ class DomainUtil {
}
} else {
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
\n(1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server`;
\n (1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server \
\n (4) the server has a valid certificate, you can add custom certificates in Settings>Organizations`;
reject(invalidZulipServerError);
}
});
Expand Down

0 comments on commit 0a893c9

Please sign in to comment.