| // Copyright 2022 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import * as fs from 'fs'; | |
| import * as net from 'net'; | |
| import * as os from 'os'; | |
| import * as path from 'path'; | |
| import * as vscode from 'vscode'; | |
| import * as commonUtil from '../common/common_util'; | |
| const onDidRunSudoWithPasswordEmitter = new vscode.EventEmitter<void>(); | |
| /** | |
| * Fired on running sudo successfully with one or more password prompts. | |
| * It is not fired if sudo failed, or sudo succeeded without password. | |
| */ | |
| export const onDidRunSudoWithPassword = onDidRunSudoWithPasswordEmitter.event; | |
| export interface SudoExecOptions extends commonUtil.ExecOptions { | |
| /** | |
| * String that tells the user why sudo is required. It must start with "to ". | |
| * Example: 'to generate C++ cross references' | |
| */ | |
| sudoReason: `to ${string}`; | |
| } | |
| /** | |
| * Runs command as the root user with sudo. | |
| * | |
| * It asks the password on VSCode input box if needed, hence a service. | |
| */ | |
| export async function execSudo( | |
| name: string, | |
| args: string[], | |
| options: SudoExecOptions | |
| ): ReturnType<typeof commonUtil.exec> { | |
| const askpass = await AskpassServer.start(options); | |
| try { | |
| const result = await commonUtil.exec( | |
| 'sudo', | |
| ['--askpass', '--', name, ...args], | |
| { | |
| ...options, | |
| env: { | |
| ...(options.env ?? {}), | |
| SUDO_ASKPASS: askpass.scriptPath, | |
| }, | |
| } | |
| ); | |
| if (!(result instanceof Error) && askpass.attempts > 0) { | |
| onDidRunSudoWithPasswordEmitter.fire(); | |
| } | |
| return result; | |
| } finally { | |
| await askpass.stop(); | |
| } | |
| } | |
| /** | |
| * Server for sudo askpass. | |
| * | |
| * A server listens on a UNIX domain socket created on a temporary directory, | |
| * and saves a Python script that behaves as an askpass command. When the | |
| * script is run by sudo, it connects to the UNIX domain socket, which makes | |
| * VSCode show a password prompt and send an entered password to the socket. | |
| */ | |
| class AskpassServer { | |
| attempts = 0; | |
| private constructor( | |
| private readonly options: SudoExecOptions, | |
| private readonly server: net.Server, | |
| private readonly tempDir: string | |
| ) { | |
| server.on('connection', socket => this.handleConnection(socket)); | |
| server.on('error', () => {}); // ignore errors | |
| } | |
| static async start(options: SudoExecOptions): Promise<AskpassServer> { | |
| const tempDir = await fs.promises.mkdtemp( | |
| path.join(os.tmpdir(), 'cros-ide-askpass.') | |
| ); | |
| const socketPath = path.join(tempDir, 'socket'); | |
| const scriptPath = path.join(tempDir, 'askpass'); | |
| // Write an askpass script. | |
| const script = `#!/usr/bin/env python3 | |
| import os, socket, sys | |
| sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
| sock.connect(os.path.join(os.path.dirname(__file__), 'socket')) | |
| sys.stdout.write(sock.makefile().read()) | |
| `; | |
| await fs.promises.mkdir(tempDir, {recursive: true}); | |
| await fs.promises.writeFile(scriptPath, script, { | |
| encoding: 'utf-8', | |
| mode: '0700', | |
| }); | |
| // Start a server. | |
| const server = net.createServer(); | |
| await new Promise<void>(resolve => { | |
| server.listen(socketPath, resolve); | |
| }); | |
| return new AskpassServer(options, server, tempDir); | |
| } | |
| async stop(): Promise<void> { | |
| this.server.close(); | |
| await fs.promises.rm(this.tempDir, {recursive: true}); | |
| } | |
| get scriptPath(): string { | |
| return path.join(this.tempDir, 'askpass'); | |
| } | |
| private handleConnection(socket: net.Socket): void { | |
| this.attempts++; | |
| void (async () => { | |
| const password = await vscode.window.showInputBox({ | |
| password: true, | |
| title: `sudo password for ${os.userInfo().username}`, | |
| prompt: `ChromiumIDE needs your password ${this.options.sudoReason}`, | |
| ignoreFocusOut: true, | |
| }); | |
| if (password) { | |
| await new Promise<void>(resolve => { | |
| socket.write(password, () => { | |
| resolve(); | |
| }); | |
| }); | |
| } | |
| await new Promise<void>(resolve => { | |
| socket.end(() => { | |
| resolve(); | |
| }); | |
| }); | |
| })(); | |
| } | |
| } |