blob: 0fe3269bb08f19deb923720fb35b1612b569aed1 [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Namespace for the whole nassh project.
*
* We export this with 'var' as we access it across background pages.
* It's messy and probably should be cleaned up at some point.
*/
// eslint-disable-next-line no-var
var nassh = {};
/**
* Non-null if nassh is running as an extension.
*/
nassh.browserAction =
window.browser && browser.browserAction ? browser.browserAction :
window.chrome && chrome.browserAction ? chrome.browserAction :
null;
lib.registerInit(
'nassh',
/**
* Register a static initializer for nassh.*.
*/
() => {
// Since our translation process only preserves \n (and discards \r), we
// have to manually insert them ourselves.
hterm.messageManager.useCrlf = true;
});
/**
* Modify if running in chrome-untrusted://. We will use
* lib.Storage.TerminalPrivate as the default storage, load messages via XHR,
* and polyfill chrome.runtime.getManifest.
*/
nassh.setupForWebApp = function() {
// Modifications if running as a web app.
if (location.href.startsWith('chrome-untrusted://')) {
lib.registerInit('terminal-private-storage', () => {
hterm.defaultStorage = new lib.Storage.TerminalPrivate();
});
lib.registerInit('messages', nassh.loadMessages);
// Polyfill chrome.runtime.getManifest since it is not available when
// We require name, version, and icons.
if (chrome && chrome.runtime && !chrome.runtime.getManifest) {
chrome.runtime.getManifest = () => {
return /** @type {!chrome.runtime.Manifest} */ ({
'name': 'Terminal',
'version': 'system',
'icons': {'192': '/images/dev/crostini-192.png'},
});
};
}
}
};
/**
* Loads messages for when chrome.i18n is not available.
*
* This should only be used in contexts outside of extensions/apps.
*/
nassh.loadMessages = async function() {
// Load hterm.messageManager from /_locales/<lang>/messages.json.
hterm.messageManager.useCrlf = true;
const url = lib.f.getURL('/_locales/$1/messages.json');
await hterm.messageManager.findAndLoadMessages(url);
};
/**
* Return a formatted message in the current locale.
*
* @param {string} name The name of the message to return.
* @param {!Array=} args The message arguments, if required.
* @return {string} The localized & formatted message.
*/
nassh.msg = function(name, args) {
return hterm.messageManager.get(name, args, name);
};
/**
* Request the persistent HTML5 filesystem for this extension.
*
* This will also create the /.ssh/ directory if it does not exits.
*
* @return {!Promise<!FileSystem>} The root filesystem handle.
*/
nassh.getFileSystem = function() {
const requestFS = window.requestFileSystem || window.webkitRequestFileSystem;
return new Promise((resolve, reject) => {
function onFileSystem(fileSystem) {
// We create /.ssh/identity/ subdir for storing keys. We need a dedicated
// subdir for users to import files to avoid collisions with standard ssh
// config files.
lib.fs.getOrCreateDirectory(fileSystem.root, '/.ssh/identity')
.then(() => resolve(fileSystem))
.catch(reject);
}
requestFS(window.PERSISTENT,
16 * 1024 * 1024,
onFileSystem,
(e) => {
console.error(`Error initializing filesystem: ${e}`);
reject(e);
});
});
};
/**
* Export the current list of nassh connections, and any hterm profiles
* they reference.
*
* This is method must be given a completion callback because the hterm
* profiles need to be loaded asynchronously.
*
* @param {function(!Object)} onComplete Callback to be invoked when export is
* complete.
* The callback will receive a plain JS object representing the state of
* nassh preferences. The object can be passed back to
* nassh.importPreferences.
*/
nassh.exportPreferences = function(onComplete) {
let pendingReads = 0;
const rv = {};
const onReadStorage = function(profile, prefs) {
rv.hterm[profile] = prefs.exportAsJson();
if (--pendingReads < 1) {
onComplete(rv);
}
};
rv.magic = 'nassh-prefs';
rv.version = 1;
const nasshPrefs = new nassh.PreferenceManager();
nasshPrefs.readStorage(function() {
// Export all the connection settings.
rv.nassh = nasshPrefs.exportAsJson();
// Save all the profiles.
rv.hterm = {};
hterm.PreferenceManager.listProfiles(nasshPrefs.storage, (profiles) => {
profiles.forEach((profile) => {
rv.hterm[profile] = null;
const prefs = new hterm.PreferenceManager(profile);
prefs.readStorage(onReadStorage.bind(null, profile, prefs));
pendingReads++;
});
if (profiles.length == 0) {
onComplete(rv);
}
});
});
};
/**
* Import a preferences object.
*
* This will not overwrite any existing preferences.
*
* @param {!Object} prefsObject A preferences object created with
* nassh.exportPreferences.
* @param {function()=} onComplete A callback to be invoked when the import is
* complete.
*/
nassh.importPreferences = function(prefsObject, onComplete) {
let pendingReads = 0;
const onReadStorage = function(terminalProfile, prefs) {
prefs.importFromJson(prefsObject.hterm[terminalProfile]);
if (--pendingReads < 1 && onComplete) {
onComplete();
}
};
if (prefsObject.magic != 'nassh-prefs') {
throw new Error('Not a JSON object or bad value for \'magic\'.');
}
if (prefsObject.version != 1) {
throw new Error('Bad version, expected 1, got: ' + prefsObject.version);
}
const nasshPrefs = new nassh.PreferenceManager();
nasshPrefs.importFromJson(prefsObject.nassh, () => {
for (const terminalProfile in prefsObject.hterm) {
const prefs = new hterm.PreferenceManager(terminalProfile);
prefs.readStorage(onReadStorage.bind(null, terminalProfile, prefs));
pendingReads++;
}
});
};
/**
* Create a new window to the options page for customizing preferences.
*
* @param {string=} page The specific options page to navigate to.
*/
nassh.openOptionsPage = function(page = '') {
const fallback = () => {
lib.f.openWindow(`/html/nassh_preferences_editor.html#${page}`);
};
if (!page && window.chrome && chrome.runtime &&
chrome.runtime.openOptionsPage) {
// This is a bit convoluted because, in some scenarios (e.g. crosh), the
// openOptionsPage helper might fail. If it does, fallback to a tab.
chrome.runtime.openOptionsPage(() => {
const err = lib.f.lastError();
if (err) {
console.warn(err);
fallback();
}
});
} else {
fallback();
}
};
/**
* Trigger the flow for sending feedback.
*/
nassh.sendFeedback = function() {
lib.f.openWindow('https://goo.gl/vb94JY');
};
/**
* Register this extension to handle URIs like ssh://.
*
* The protocol should be one allowed by the specifications:
* https://html.spec.whatwg.org/multipage/webappapis.html#webappapis
* https://chromium.googlesource.com/chromium/src/+blame/HEAD/third_party/WebKit/Source/modules/navigatorcontentutils/NavigatorContentUtils.cpp
* https://www.iana.org/assignments/uri-schemes/prov/sftp
*
* @param {string} proto The protocol name to register.
*/
nassh.registerProtocolHandler = function(proto) {
try {
navigator.registerProtocolHandler(
proto,
chrome.runtime.getURL('/html/nassh.html#uri:%s'),
chrome.runtime.getManifest().name);
} catch (e) {
console.error(`Unable to register '${proto}' handler:`, e);
}
// Not all runtimes allow direct registration, so also register with the
// 'web+' prefix just in case.
if (!proto.startsWith('web+')) {
nassh.registerProtocolHandler(`web+${proto}`);
}
};
/**
* Disable automatic tab discarding for our windows.
*
* Newer versions of Chrome are a bit more proactive in discarding tabs. Signal
* that we shouldn't be discarded as restarting crosh/ssh sessions is not easy
* for users.
* https://crbug.com/868155
*
* Note: This code updates tab properties asynchronously, but that should be
* fine for our usage as we don't generally create windows/tabs on the fly.
*/
nassh.disableTabDiscarding = function() {
if (window.chrome && chrome.tabs) {
chrome.tabs.getCurrent((tab) => {
chrome.tabs.update(tab.id, {autoDiscardable: false});
});
}
};
/**
* Convert a base64url encoded string to the base64 encoding.
*
* The difference here is in the last two characters of the alphabet.
* So converting between them is easy.
*
* base64: https://tools.ietf.org/html/rfc4648#section-4
* 62 +
* 63 /
* base64url: https://tools.ietf.org/html/rfc4648#section-5
* 62 -
* 63 _
*
* We re-add any trailing = padding characters.
*
* @param {string} data The base64url encoded data.
* @return {string} The data in base64 encoding.
*/
nassh.base64UrlToBase64 = function(data) {
const replacements = {'-': '+', '_': '/'};
let ret = data.replace(/[-_]/g, (ch) => replacements[ch]);
switch (ret.length % 4) {
case 1:
throw new Error(`Invalid base64url length: ${ret.length}`);
case 2:
ret += '==';
break;
case 3:
ret += '=';
break;
}
return ret;
};
/**
* Convert a base64 encoded string to the base64url encoding.
*
* This is the inverse of nassh.base64UrlToBase64.
*
* We strip off any = padding characters too.
*
* @param {string} data The base64 encoded data.
* @return {string} The data in base64url encoding.
*/
nassh.base64ToBase64Url = function(data) {
const replacements = {'+': '-', '/': '_', '=': ''};
return data.replace(/[+/=]/g, (ch) => replacements[ch]);
};
/**
* Workaround missing chrome.runtime in older versions of Chrome.
*
* As detailed in https://crbug.com/925118, the chrome.runtime object might be
* missing when we run. In order to workaround it, we need to reload the page.
* While this is fixed in R72+, we unfortunately have EOL Chromebooks that will
* never be able to upgrade to that version, so we have to keep this around for
* a long time -- once we update minimum_chrome_version in the manifest to 72+.
*
* @return {boolean} True if bug was detected and the caller should halt all
* processing.
*/
nassh.workaroundMissingChromeRuntime = function() {
// Chrome has a bug where it sometimes doesn't initialize chrome.runtime.
// Try and workaround it by forcing a refresh. https://crbug.com/924656
if (window.chrome && !window.chrome.runtime) {
console.warn('chrome.runtime is undefined; reloading to workaround ' +
'https://crbug.com/925118');
document.location.reload();
return true;
}
return false;
};
/**
* Helper to get the background page once it's fully initialized.
*
* If the background page doesn't exist yet (fresh startup, or it's gone quiet
* and Chrome automatically exited it), then the getBackgroundPage helper will
* create a new instance on the fly and return it. Unfortunately, we will often
* then try to call funcs in it directly before it's finished initializing which
* will cause random failures as it hits race conditions.
*
* @return {!Promise<!Window>} A promise resolving to the background page once
* it is fully initialized.
*/
nassh.getBackgroundPage = function() {
if (!window.chrome || !chrome.runtime || !chrome.runtime.getBackgroundPage) {
return Promise.reject();
}
return new Promise((resolve, reject) => {
chrome.runtime.getBackgroundPage((bg) => {
if (bg === undefined) {
return reject();
}
const checkInitialized = () => {
if (bg.loaded) {
return resolve(bg);
}
console.log('Background page not initialized; retrying');
setTimeout(checkInitialized, 100);
};
checkInitialized();
});
});
};
/**
* Generate an SGR escape sequence.
*
* @param {!Object=} settings
* @return {string} The SGR escape sequence.
*/
nassh.sgrSequence = function(
{bold, faint, italic, underline, blink, fg, bg} = {}) {
const parts = [];
if (bold) {
parts.push('1');
}
if (faint) {
parts.push('2');
}
if (italic) {
parts.push('3');
}
if (underline) {
if (underline === true) {
parts.push('4');
} else {
parts.push(`4:${underline}`);
}
}
if (blink) {
parts.push('5');
}
if (fg) {
parts.push(fg);
}
if (bg) {
parts.push(bg);
}
return `\x1b[${parts.join(';')}m`;
};
/**
* Apply SGR styling to text.
*
* This will reset the SGR style to the default.
*
* @param {string} text The text to be stylized.
* @param {!Object=} settings The SGR settings to apply.
* @return {string} The text wrapped in SGR escape sequences.
*/
nassh.sgrText = function(text, settings) {
return nassh.sgrSequence(settings) + text + nassh.sgrSequence();
};
/**
* Generate a hyperlink using OSC-8 escape sequence.
*
* @param {string} url The link target.
* @param {string=} text The user visible text.
* @return {string} The hyperlink with OSC-8 escape sequences.
*/
nassh.osc8Link = function(url, text = url) {
if (url.startsWith('/')) {
url = lib.f.getURL(url);
}
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
};
/**
* @typedef {{
* name: string,
* isWebFont: boolean,
* }}
*/
nassh.Font;
/** @type {!Array<!nassh.Font>} */
nassh.FONTS = [
{name: 'Noto Sans Mono', isWebFont: false},
{name: 'Cousine', isWebFont: true},
{name: 'Inconsolata', isWebFont: true},
{name: 'Roboto Mono', isWebFont: true},
{name: 'Source Code Pro', isWebFont: true},
];
/**
* Add css to load web fonts from fonts.googleapis.com.
*
* @param {!Document} document The document to load into.
*/
nassh.loadWebFonts = function(document) {
const imports = [];
const fontFaces = [];
for (const font of nassh.FONTS) {
if (font.isWebFont) {
// Load normal (400) and bold (700).
imports.push(`@import url('https://fonts.googleapis.com/css2?family=` +
`${encodeURIComponent(font.name)}:wght@400;700&display=swap');`);
}
fontFaces.push(`
@font-face {
font-family: 'Powerline For ${font.name}';
src: url('../fonts/PowerlineFor${font.name.replace(/\s/g, '')}.woff2')
format('woff2');
font-weight: normal bold;
unicode-range:
U+2693,U+26A1,U+2699,U+270E,U+2714,U+2718,U+273C,U+279C,U+27A6,
U+2B06-2B07,U+E0A0-E0D4;
}`);
}
const style = document.createElement('style');
style.textContent = imports.join('\n') + fontFaces.join('');
document.head.appendChild(style);
};
/**
* A Promise wrapper for the chrome.runtime.sendMessage API.
*
* @param {*} args The arguments to sendMessage.
* @return {!Promise<*>} A promise to resolve with the remote's response.
*/
nassh.runtimeSendMessage = function(...args) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(...args, (response) => {
// If the remote side doesn't exist (which is normal), Chrome complains
// if we don't read the lastError. Clear that here.
const err = lib.f.lastError();
if (err) {
reject(err);
} else {
resolve(response);
}
});
});
};