@azure/static-web-apps-cli
Version:
Azure Static Web Apps CLI
251 lines • 11.6 kB
JavaScript
import os from "node:os";
import waitOn from "wait-on";
import { logger } from "../../utils/logger.js";
import { isValidIpAddress } from "../../utils/net.js";
import { isWSL } from "../../utils/platform.js";
export class NativeCredentialsStore {
options;
constructor(options) {
this.options = options;
}
static KEYCHAIN_ENTRY_MAX_LENGTH = 2500;
static KEYCHAIN_ENTRY_CHUNK_SIZE = NativeCredentialsStore.KEYCHAIN_ENTRY_MAX_LENGTH - 100;
static KEYCHAIN_SERVICE = "swa-cli";
keychainCache;
async getPassword(service, account) {
logger.silly("Getting credentials from native keychain");
const keychain = await this.requireKeychain();
logger.silly("Got native keychain reference");
const credentials = await keychain.getPassword(service, account);
logger.silly("Got credentials from native keychain: " + (credentials ? "<hidden>" : "<empty>"));
if (credentials) {
logger.silly("Credentials found in native keychain");
try {
let { content, hasNextChunk } = JSON.parse(credentials);
if (!content || !hasNextChunk) {
return credentials;
}
logger.silly("Credentials is chunked. Reading all chunks...");
let index = 1;
while (hasNextChunk) {
const nextChunk = await keychain.getPassword(service, `${account}-${index}`);
const result = JSON.parse(nextChunk);
content += result.content;
hasNextChunk = result.hasNextChunk;
index++;
}
logger.silly("Got all chunks successfully");
return content;
}
catch {
logger.silly("Credentials is not chunked");
logger.silly("Returning credentials as is");
return credentials;
}
}
logger.silly("Credentials not found in native keychain");
return credentials;
}
async setPassword(service, account, credentials) {
logger.silly("Setting credentials in native keychain");
const keychain = await this.requireKeychain();
logger.silly("Got native keychain reference");
const MAX_SET_ATTEMPTS = 3;
// Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times.
const setPasswordWithRetry = async (service, account, credentials) => {
let attempts = 0;
let error;
while (attempts < MAX_SET_ATTEMPTS) {
try {
logger.silly("Attempting to set credentials");
await keychain.setPassword(service, account, credentials);
logger.silly("Set credentials successfully");
return;
}
catch (error) {
error = error;
logger.warn("Error attempting to set a credentials. Trying again... (" + attempts + ")");
logger.warn(error);
attempts++;
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
throw error;
};
if (credentials.length > NativeCredentialsStore.KEYCHAIN_ENTRY_MAX_LENGTH) {
logger.silly("Credentials value is too long. Chunking it.");
let index = 0;
let chunk = 0;
let hasNextChunk = true;
while (hasNextChunk) {
const credentialsChunk = credentials.substring(index, index + NativeCredentialsStore.KEYCHAIN_ENTRY_CHUNK_SIZE);
index += NativeCredentialsStore.KEYCHAIN_ENTRY_CHUNK_SIZE;
hasNextChunk = credentials.length - index > 0;
const content = {
content: credentialsChunk,
hasNextChunk: hasNextChunk,
};
logger.silly("Setting credentials chunk #" + chunk + " ...");
await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
chunk++;
}
}
else {
await setPasswordWithRetry(service, account, credentials);
}
}
async deletePassword(service, account) {
logger.silly("Deleting credentials from native keychain");
const keychain = await this.requireKeychain();
logger.silly("Got native keychain reference");
const didDelete = await keychain.deletePassword(service, account);
logger.silly("Deleted credentials from native keychain: " + didDelete);
return didDelete;
}
async findPassword(service) {
logger.silly("Finding password in native keychain");
const keychain = await this.requireKeychain();
logger.silly("Got native keychain reference");
const credentials = await keychain.findPassword(service);
logger.silly("Got credentials from native keychain: " + (credentials ? "<hidden>" : "<empty>"));
return credentials;
}
async findCredentials(service) {
logger.silly("Finding credentials in native keychain");
const keychain = await this.requireKeychain();
logger.silly("Got native keychain reference");
const credentials = await keychain.findCredentials(service);
logger.silly("Got credentials from native keychain: " + (credentials ? "<hidden>" : "<empty>"));
return credentials;
}
clear() {
logger.silly("Clear native keychain");
if (this.keychainCache instanceof InMemoryCredentialsStore) {
logger.silly("Clearing credentials from in-memory keychain");
return this.keychainCache.clear();
}
// We don't know how to properly clear Keytar because we don't know
// what services have stored credentials. For reference, a "service" is an extension.
// TODO: should we clear credentials for the built-in auth extensions?
return Promise.resolve();
}
async getSecretStoragePrefix() {
return Promise.resolve(NativeCredentialsStore.KEYCHAIN_SERVICE);
}
async requireKeychain() {
logger.silly("Getting keychain reference");
logger.silly(`isKeychainEnabled: ${this.options.enabled}`);
logger.silly(`KeychainCache: ${this.keychainCache}`);
if (this.keychainCache) {
return this.keychainCache;
}
if (this.options.enabled === false) {
logger.silly("keychain is disabled. Using in-memory credential store instead.");
this.keychainCache = new InMemoryCredentialsStore();
return this.keychainCache;
}
try {
logger.silly("Attempting to load native keychain");
await this.validateX11Environment();
this.keychainCache = await import("keytar");
// Try using keytar to see if it throws or not.
await this.keychainCache.findCredentials("Out of the mountain of despair, a stone of hope");
}
catch (error) {
throw error;
}
logger.silly("Got native keychain reference");
return this.keychainCache;
}
async validateX11Environment() {
if (!isWSL()) {
// we assume that if we're not on WSL, we're on a sane environment
// that has a valid X11 environment.
return;
}
const { DISPLAY, WAYLAND_DISPLAY, MIR_SOCKET, WAYLAND_SOCKET } = process.env;
let x11Host = DISPLAY || WAYLAND_DISPLAY || MIR_SOCKET || WAYLAND_SOCKET;
const printX11ErrorAndExit = () => logger.error(`An X11 server is required when keychain is enabled. You can disable keychain using --no-use-keychain or try a different login method.`, true);
if (!x11Host) {
logger.error(`Environment variable DISPLAY is not set.`);
printX11ErrorAndExit();
}
else {
logger.silly("X11 is set: " + x11Host);
// An X11 address can be one of the following:
// - hostname:D.S means screen S on display D of host hostname; the X server for this display is listening at TCP port 6000+D.
// - host/unix:D.S means screen S on display D of host host; the X server for this display is listening at UNIX domain socket /tmp/.X11-unix/XD
// (so it's only reachable from host).
// - :D.S is equivalent to host/unix:D.S, where host is the local hostname.
let [x11Hostname, x11Display] = x11Host.split(":");
let x11Port = 6000;
if (x11Display) {
let [display, _screen] = x11Display.split(".");
x11Port += parseInt(display, 10);
}
logger.silly("X11 hostname: " + x11Hostname);
if (isValidIpAddress(x11Hostname)) {
logger.silly("X11 has a valid IP address");
}
else {
x11Hostname = os.hostname();
logger.silly("X11 value is not a valid hostname. Forcing X11 host name to " + x11Hostname);
}
logger.silly(`checking if X11 host ${x11Hostname}:${x11Port} is reachable. This may take a few seconds...`);
try {
await waitOn({
resources: ["tcp:" + x11Hostname + ":" + x11Port],
delay: 5000, // 5 seconds
timeout: 10000, // 10 seconds
});
}
catch (error) {
logger.error(`X11 host ${x11Hostname}:${x11Port} is not reachable.`);
printX11ErrorAndExit();
}
}
}
}
class InMemoryCredentialsStore {
secretVault = {};
async getPassword(service, account) {
logger.silly("Getting password from in-memory keychain");
return this.secretVault[service]?.[account] ?? null;
}
async setPassword(service, account, credentials) {
logger.silly("Setting password in in-memory keychain");
this.secretVault[service] = this.secretVault[service] ?? {};
this.secretVault[service][account] = credentials;
}
async deletePassword(service, account) {
logger.silly("Deleting password from in-memory keychain");
if (!this.secretVault[service]?.[account]) {
logger.silly("Password not found in in-memory keychain");
return false;
}
delete this.secretVault[service][account];
if (Object.keys(this.secretVault[service]).length === 0) {
delete this.secretVault[service];
}
logger.silly("Password deleted from in-memory keychain");
return true;
}
async findPassword(service) {
logger.silly("Finding password in in-memory keychain");
return JSON.stringify(this.secretVault[service]) ?? null;
}
async findCredentials(service) {
logger.silly("Finding credentials in in-memory keychain");
const credentials = [];
for (const account of Object.keys(this.secretVault[service] || {})) {
credentials.push({ account, password: this.secretVault[service][account] });
}
logger.silly("Got credentials from native keychain: " + (credentials ? "<hidden>" : "<empty>"));
return credentials;
}
async clear() {
logger.silly("Clearing in-memory keychain");
this.secretVault = {};
}
}
//# sourceMappingURL=credentials-store.js.map