import * as vscode from 'vscode'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import DevstarAPIHandler from './devstar-api'; import * as utils from './utils'; const { generateKeyPairSync, createHash } = require('node:crypto'); const sshpk = require('sshpk'); export default class User { private context: vscode.ExtensionContext; // 修改devstarDomain会影响hostname, username key, user token key private devstarDomain: string; private devstarHostname: string; private usernameKey: string; // devstarUsername_ private userTokenKey: string; // devstarUserToken_ /** * devstarDomain由配置项提供 * @param context */ constructor(context: vscode.ExtensionContext); /** * devstarDomain由open with vscode链接提供 * @param context * @param devstarDomain */ constructor(context: vscode.ExtensionContext, devstarDomain: string); constructor(context: vscode.ExtensionContext, devstarDomain?: string) { this.context = context; // 提取devstar domain的主域名,用于本地ssh key的命名 if (devstarDomain != undefined && devstarDomain != "") { // open with vscode链接提供域名 this.devstarDomain = devstarDomain.endsWith('/') ? devstarDomain.slice(0, -1) : devstarDomain let parsedUrl = new URL(devstarDomain); this.devstarHostname = parsedUrl.hostname.replace(/\./g, '_'); //提取hostname,并用下划线替换. } else { const devstarDomainFromConfig: string | undefined = utils.devstarDomain(); if (devstarDomainFromConfig != undefined && devstarDomainFromConfig != "") { // 用户配置项提供域名 this.devstarDomain = devstarDomainFromConfig.endsWith('/') ? devstarDomainFromConfig.slice(0, -1) : devstarDomainFromConfig let parsedUrl = new URL(this.devstarDomain); this.devstarHostname = parsedUrl.hostname.replace(/\./g, '_'); //提取hostname,并用下划线替换. } else { // 如果没有配置devstar domain,则默认domain为https://devstar.cn this.devstarDomain = "https://devstar.cn" this.devstarHostname = "devstar_cn" } } this.usernameKey = "devstarUsername_" + this.devstarDomain this.userTokenKey = "devstarUserToken_" + this.devstarDomain } setDevstarDomain(devstarDomain: string) { // 修改devstar domain会影响hostname、usertoken key、username key if (devstarDomain != "") { this.devstarDomain = devstarDomain.endsWith('/') ? devstarDomain.slice(0, -1) : devstarDomain const parsedUrl = new URL(devstarDomain) this.devstarHostname = parsedUrl.hostname.replace(/\./g, '_'); this.usernameKey = "devstarUsername_" + this.devstarDomain this.userTokenKey = "devstarUserToken_" + this.devstarDomain } else { console.error("devstar domain is null") } } getDevstarDomain(): string { return this.devstarDomain } public async login(token: string, username: string): Promise { const devstarAPIHandler = new DevstarAPIHandler(this.devstarDomain) try { const res = await devstarAPIHandler.verifyToken(token, username) if (!res) { throw new Error('Token verification failed') } else { // token与用户名验证通过 // 插件登录:存储token与用户名 this.setUserTokenToLocal(token) this.setUsernameToLocal(username) // 检查本地是否有用户所属公钥,没有则创建 if (!this.existUserPublicKey()) { await this.createUserSSHKey() // 上传公钥 const uploadResult = await devstarAPIHandler.uploadUserPublicKey(this) if (uploadResult !== 'ok') { throw new Error('Upload user public key failed') } } vscode.window.showInformationMessage(vscode.l10n.t('User login successfully!')) return 'ok' } } catch (error) { console.error(error) await utils.showErrorNotification('用户登录失败!') return 'login failed' } } public logout() { this.setUserTokenToLocal("") this.setUsernameToLocal("") vscode.window.showInformationMessage(vscode.l10n.t("User has logged out!")) } public async isLogged(): Promise { const username: string|undefined = this.context.globalState.get(this.usernameKey) const userToken: string|undefined = this.context.globalState.get(this.userTokenKey) if ((username != undefined && username != '') && (userToken != undefined && userToken != '')) { const devstarAPIHandler = new DevstarAPIHandler(this.devstarDomain) const res = await devstarAPIHandler.verifyToken(userToken, username) if (!res) { console.error("username or token is error") return false; } else { return true; } } else { return false; } } public getUsernameFromLocal(): string | undefined { return this.context.globalState.get(this.usernameKey); } public getUserTokenFromLocal(): string | undefined { return this.context.globalState.get(this.userTokenKey); } public setUsernameToLocal(username: string) { this.context.globalState.update(this.usernameKey, username); if (username !== "") { console.log('Username has been stored.') } else { console.log('Username has been cleaned up.') } } public setUserTokenToLocal(userToken: string) { this.context.globalState.update(this.userTokenKey, userToken) if (userToken !== "") { console.log('Token has been stored.'); } else { console.log('Token has been cleaned up.') } } public updateLocalUserPrivateKeyPath(localUserPrivateKeyPath: string) { this.context.globalState.update('localUserPrivateKeyPath', localUserPrivateKeyPath) } public getLocalUserPrivateKeyPath(): undefined | string { return this.context.globalState.get('localUserPrivateKeyPath') } public getUserPrivateKeyPath(): string { if (!this.isLogged()) { return ''; } else { const username: string|undefined = this.context.globalState.get(this.usernameKey) // islogged为true,username不为空 return path.join(os.homedir(), '.ssh', `id_rsa_${username}_${this.devstarHostname}`) } } public getUserPublicKeyPath(): string { if (!this.isLogged()) { return ''; } else { const username: string|undefined = this.context.globalState.get(this.usernameKey) // islogged为true,username不为空 return path.join(os.homedir(), '.ssh', `id_rsa_${username}_${this.devstarHostname}.pub`) } } public existUserPublicKey(): boolean { const userPublicKeyPath = this.getUserPublicKeyPath(); return fs.existsSync(userPublicKeyPath) } public existUserPrivateKey(): boolean { const userPrivateKeyPath = this.getUserPrivateKeyPath(); return fs.existsSync(userPrivateKeyPath) } public getUserPublicKey(): string { const userPublicKeyPath = this.getUserPublicKeyPath(); const userPublicKey = fs.readFileSync(userPublicKeyPath, 'utf-8'); // remove `\r` `\n` const trimmedDefaultPublicKey = userPublicKey.replace(/[\r\n]/g, ""); return trimmedDefaultPublicKey; } public getUserPrivateKey(): string { const userPrivateKey = this.getUserPrivateKeyPath(); return fs.readFileSync(userPrivateKey, 'utf-8'); } public async createUserSSHKey() { if (this.existUserPublicKey() && this.existUserPrivateKey()) { // if both public and private key exists, stop return; } const { publicKey, privateKey, } = await generateKeyPairSync('rsa', { modulusLength: 4096, publicKeyEncoding: { type: 'pkcs1', format: 'pem', }, privateKeyEncoding: { type: 'pkcs1', format: 'pem', }, }); const publicKeyFingerprint = sshpk.parseKey(publicKey, 'pem').toString('ssh'); const publicKeyStr = publicKeyFingerprint; // public key is public key fingerprint const privateKeyStr = privateKey; try { // 确保公/私钥目录存在 const publicKeyDir = path.dirname(this.getUserPublicKeyPath()) if (!fs.existsSync(publicKeyDir)) { console.log(`Directory ${publicKeyDir} does not exist, creating it...`); // 公钥与私钥的目录一样,所以只用创建一次 fs.mkdirSync(publicKeyDir, { recursive: true }) } fs.writeFileSync(this.getUserPublicKeyPath(), publicKeyStr); fs.writeFileSync(this.getUserPrivateKeyPath(), privateKeyStr); // limit the permission of private key to prevent that the private key not works fs.chmodSync(this.getUserPrivateKeyPath(), 0o600) console.log(`User's ssh key has been created.`) // 更新local user private key path this.updateLocalUserPrivateKeyPath(this.getUserPrivateKeyPath()) console.log(`Update local user private key path.`) } catch (error) { const username: string|undefined = this.context.globalState.get(this.usernameKey) console.error(`Failed to write public/private key into the user(${username}) ssh public/key file: `, error); } } }