├── resources ├── sidebar.png ├── logo_128.png ├── screenshot.png └── icons │ ├── new-dark.png │ ├── new-light.png │ ├── session-dark.png │ └── session-light.png ├── tsconfig.json ├── LICENSE ├── .gitignore ├── README.md ├── src ├── utils.ts ├── viewProviders.ts └── extension.ts └── package.json /resources/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/HEAD/resources/sidebar.png -------------------------------------------------------------------------------- /resources/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/HEAD/resources/logo_128.png -------------------------------------------------------------------------------- /resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/HEAD/resources/screenshot.png -------------------------------------------------------------------------------- /resources/icons/new-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/HEAD/resources/icons/new-dark.png -------------------------------------------------------------------------------- /resources/icons/new-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/HEAD/resources/icons/new-light.png -------------------------------------------------------------------------------- /resources/icons/session-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/HEAD/resources/icons/session-dark.png -------------------------------------------------------------------------------- /resources/icons/session-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/HEAD/resources/icons/session-light.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["ES2019"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "strict": false, 9 | "rootDir": "src" 10 | }, 11 | "exclude": ["node_modules", ".vscode"] 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ian Mckay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Custom 107 | out/ 108 | *.vsix 109 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS CloudShell plugin for VS Code 2 | 3 | [![](https://img.shields.io/badge/-VS%20Code%20Marketplace-brightgreen)](https://marketplace.visualstudio.com/items?itemName=iann0036.aws-cloudshell) 4 | 5 | An unofficial AWS CloudShell plugin for VS Code. Open multiple AWS CloudShell terminals within VS Code on demand. 6 | 7 | >**Note:** This extension is still in alpha stages. Please [raise an issue](https://github.com/iann0036/vscode-aws-cloudshell/issues) if you experience any problems. 8 | 9 | ![AWS CloudShell plugin for VS Code Screenshot](https://raw.githubusercontent.com/iann0036/vscode-aws-cloudshell/master/resources/screenshot.png) 10 | 11 | ## Setup 12 | 13 | In order to use this extension, you will need: 14 | 15 | * The [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed 16 | * The [Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) installed 17 | * A profile configured that meets the below requirements 18 | 19 | Once you have installed the extension, you should go to your VS Code preferences (hit F1 then enter "Preferences: Open Settings (UI)" -> Extensions -> AWS CloudShell Configuration) and specify your `region` and one or both of `profile` and/or `assumeRole`. Once the settings are updated, you may click the sidebar icon and then the "Start Session" (+) button. 20 | 21 | Currently, you **MUST** either use a profile with a session token attached to it, or use the `assumeRole` property to assume a role with the [necessary permissions](https://console.aws.amazon.com/iam/home?region=us-east-1#/policies/arn:aws:iam::aws:policy/AWSCloudShellFullAccess$jsonEditor). Alternatives may be provided in a future release. 22 | 23 | 24 | ## Settings 25 | 26 | Here is the list of all [settings](https://code.visualstudio.com/docs/getstarted/settings) you can set within this extension: 27 | 28 | Setting | Description 29 | ------- | ----------- 30 | `awscloudshell.profile` | The profile name (usually as specified in `~/.aws/credentials`) 31 | `awscloudshell.region` | The AWS region to connect to 32 | `awscloudshell.assumeRole` | The role ARN to assume 33 | `awscloudshell.enableUpload` | Whether to enable an upload menu item from the Explorer view (experimental) 34 | `awscloudshell.vpcid` | VPC Id (experimental) 35 | `awscloudshell.subnetid` | Subnet Id (experimental) 36 | `awscloudshell.securitygroupid` | Security Group Id (experimental) 37 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { defaultProvider } from '@aws-sdk/credential-provider-node'; 4 | import { STS } from '@aws-sdk/client-sts'; 5 | 6 | function promptForMFAIfRequired(serial: string): Promise { 7 | return new Promise((resolve, reject) => { 8 | vscode.window.showInputBox({ 9 | placeHolder: "", 10 | prompt: "Enter your MFA code.", 11 | value: "", 12 | ignoreFocusOut: false 13 | }).then(function(mfa_token){ 14 | resolve(mfa_token); 15 | }); 16 | }); 17 | } 18 | 19 | export function GetAWSCreds(): Thenable { 20 | return new Promise(async (resolve, reject) => { 21 | let extensionConfig = vscode.workspace.getConfiguration('awscloudshell'); 22 | let awsregion = extensionConfig.get('region'); 23 | let assumeRole = extensionConfig.get('assumeRole'); 24 | 25 | let creds = await defaultProvider({ 26 | profile: extensionConfig.get('profile') || null, 27 | mfaCodeProvider: promptForMFAIfRequired 28 | })(); 29 | 30 | if (assumeRole) { 31 | const stsclient = new STS({ credentials: creds }); 32 | 33 | const assumedSession = await stsclient.assumeRole({ 34 | RoleArn: assumeRole.toString(), 35 | RoleSessionName: 'VSCode' 36 | }); 37 | 38 | resolve({ 39 | 'accessKey': assumedSession.Credentials.AccessKeyId, 40 | 'secretKey': assumedSession.Credentials.SecretAccessKey, 41 | 'sessionToken': assumedSession.Credentials.SessionToken 42 | }); 43 | } else { 44 | resolve({ 45 | 'accessKey': creds.accessKeyId, 46 | 'secretKey': creds.secretAccessKey, 47 | 'sessionToken': creds.sessionToken 48 | }); 49 | } 50 | }); 51 | } 52 | 53 | export function GetRegion(): string { 54 | let extensionConfig = vscode.workspace.getConfiguration('awscloudshell'); 55 | return extensionConfig.get('region') || "us-east-1"; 56 | } 57 | 58 | export function GetVPCId(): string | null { 59 | let extensionConfig = vscode.workspace.getConfiguration('awscloudshell'); 60 | return extensionConfig.get('vpcid'); 61 | } 62 | 63 | export function GetSubnetId(): string | null { 64 | let extensionConfig = vscode.workspace.getConfiguration('awscloudshell'); 65 | return extensionConfig.get('subnetid'); 66 | } 67 | 68 | export function GetSecurityGroupId(): string | null { 69 | let extensionConfig = vscode.workspace.getConfiguration('awscloudshell'); 70 | return extensionConfig.get('securitygroupid'); 71 | } 72 | 73 | export function GetProxy(): string | null { 74 | let extensionConfig = vscode.workspace.getConfiguration('awscloudshell'); 75 | let proxy: string = extensionConfig.get('proxy'); 76 | 77 | if (proxy == "") 78 | return null; 79 | 80 | return proxy; 81 | } 82 | 83 | export function ReducePromises(array, fn) { 84 | var results = []; 85 | return array.reduce(function(p, item) { 86 | return p.then(function () { 87 | return fn(item).then(function (data) { 88 | results.push(data); 89 | return results; 90 | }).catch((y) => { 91 | console.error(y); 92 | }); 93 | }).catch((x) => { 94 | console.error(x); 95 | }); 96 | }, Promise.resolve()); 97 | } 98 | -------------------------------------------------------------------------------- /src/viewProviders.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | class Session extends vscode.TreeItem { 6 | 7 | public label: string 8 | public state: string 9 | public name: string 10 | public terminal: vscode.Terminal 11 | public creds; 12 | public environmentId; 13 | 14 | constructor( 15 | public region: string, 16 | public collapsibleState: vscode.TreeItemCollapsibleState, 17 | public command?: vscode.Command 18 | ) { 19 | super("", collapsibleState); 20 | this.region = region; 21 | this.label = region + " - (connecting)"; 22 | this.state = "CONNECTING"; 23 | } 24 | 25 | setSessionName(name: string): void { 26 | this.name = name; 27 | this.label = this.region + " - " + this.name + " (connecting)"; 28 | } 29 | 30 | setConnected(): void { 31 | this.state = "CONNECTED"; 32 | this.contextValue = "connectedSession"; 33 | this.label = this.region + " - " + this.name + " (connected)"; 34 | } 35 | 36 | setTerminal(terminal: vscode.Terminal): void { 37 | this.terminal = terminal; 38 | vscode.window.activeColorTheme.kind 39 | } 40 | 41 | setCreds(creds): void { 42 | this.creds = creds; 43 | } 44 | 45 | setEnvironmentId(environmentId): void { 46 | this.environmentId = environmentId; 47 | } 48 | 49 | iconPath = path.join(__filename, '..', '..', 'resources', 'icons', (vscode.window.activeColorTheme.kind == vscode.ColorThemeKind.Light ? 'session-light.png' : 'session-dark.png')); 50 | 51 | contextValue = 'connectingSession'; 52 | } 53 | 54 | export class SessionProvider implements vscode.TreeDataProvider { 55 | 56 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 57 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 58 | 59 | public sessions: Session[] 60 | 61 | private uploadCmdDisposable; 62 | 63 | constructor() { 64 | this.sessions = [] 65 | } 66 | 67 | addSession(region: string): Session { 68 | let session = new Session(region, vscode.TreeItemCollapsibleState.None); 69 | this.sessions.push(session); 70 | this.refresh(); 71 | 72 | return session; 73 | } 74 | 75 | clearAll(): void { 76 | this.sessions = []; 77 | this.refresh(); 78 | } 79 | 80 | refresh(): void { 81 | this._onDidChangeTreeData.fire(null); 82 | } 83 | 84 | getTreeItem(element: Session): vscode.TreeItem { 85 | return element; 86 | } 87 | 88 | getChildren(element?: Session): Thenable { 89 | return new Promise(resolve => { 90 | resolve(this.sessions); 91 | }); 92 | } 93 | 94 | getLastSession(): Session { 95 | return this.sessions[this.sessions.length - 1]; 96 | } 97 | 98 | onError(): void { 99 | setTimeout((that) => { 100 | for (let i=0; i" 20 | ], 21 | "contributors": [ 22 | "Ian Mckay " 23 | ], 24 | "activationEvents": [ 25 | "*" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/iann0036/vscode-aws-cloudshell.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/iann0036/vscode-aws-cloudshell/issues" 33 | }, 34 | "keywords": [ 35 | "AWS", 36 | "CloudShell", 37 | "terminal", 38 | "collaboration" 39 | ], 40 | "galleryBanner": { 41 | "color": "#3c3c3c", 42 | "theme": "dark" 43 | }, 44 | "main": "./out/extension", 45 | "contributes": { 46 | "menus": { 47 | "view/title": [ 48 | { 49 | "command": "awscloudshell.startSession", 50 | "when": "view == aws-cloudshell-view-1-sessions", 51 | "group": "navigation" 52 | } 53 | ], 54 | "explorer/context": [ 55 | { 56 | "command": "awscloudshell.uploadFile", 57 | "when": "resourceScheme == file && config.awscloudshell.enableUpload == true" 58 | } 59 | ] 60 | }, 61 | "commands": [ 62 | { 63 | "command": "awscloudshell.startSession", 64 | "title": "Start Session", 65 | "category": "AWS CloudShell", 66 | "icon": { 67 | "light": "resources/icons/new-light.png", 68 | "dark": "resources/icons/new-dark.png" 69 | } 70 | }, 71 | { 72 | "command": "awscloudshell.uploadFile", 73 | "title": "Upload file to AWS CloudShell", 74 | "category": "AWS CloudShell" 75 | } 76 | ], 77 | "viewsContainers": { 78 | "activitybar": [ 79 | { 80 | "id": "aws-cloudshell-vc", 81 | "title": "AWS CloudShell", 82 | "icon": "resources/sidebar.png" 83 | } 84 | ] 85 | }, 86 | "views": { 87 | "aws-cloudshell-vc": [ 88 | { 89 | "id": "aws-cloudshell-view-1-sessions", 90 | "name": "Sessions" 91 | } 92 | ] 93 | }, 94 | "configuration": { 95 | "type": "object", 96 | "title": "AWS CloudShell Configuration", 97 | "properties": { 98 | "awscloudshell.region": { 99 | "type": "string", 100 | "default": "us-east-1", 101 | "description": "The AWS CloudShell region." 102 | }, 103 | "awscloudshell.profile": { 104 | "type": "string", 105 | "default": "", 106 | "description": "The profile to use for an INI credential provider." 107 | }, 108 | "awscloudshell.assumeRole": { 109 | "type": "string", 110 | "default": "", 111 | "description": "The ARN of a role to assume into." 112 | }, 113 | "awscloudshell.enableUpload": { 114 | "type": "boolean", 115 | "default": false, 116 | "description": "Enable upload file menu option from Explorer view." 117 | }, 118 | "awscloudshell.vpcid": { 119 | "type": "string", 120 | "default": "", 121 | "description": "The VPC Id to attach this environment to." 122 | }, 123 | "awscloudshell.subnetid": { 124 | "type": "string", 125 | "default": "", 126 | "description": "The Subnet Id to attach this environment to." 127 | }, 128 | "awscloudshell.securitygroupid": { 129 | "type": "string", 130 | "default": "", 131 | "description": "The Security Group Id to attach this environment to." 132 | } 133 | } 134 | } 135 | }, 136 | "scripts": { 137 | "vscode:prepublish": "npm run compile", 138 | "compile": "tsc -p ./", 139 | "lint": "eslint . --ext .ts,.tsx", 140 | "watch": "tsc -watch -p ./" 141 | }, 142 | "devDependencies": { 143 | "@types/node": "^12.12.0", 144 | "@types/vscode": "^1.34.0", 145 | "@typescript-eslint/eslint-plugin": "^3.0.2", 146 | "@typescript-eslint/parser": "^3.0.2", 147 | "eslint": "^7.1.0", 148 | "typescript": "^4.0.2" 149 | }, 150 | "dependencies": { 151 | "@aws-sdk/client-sts": "^3.0.0", 152 | "@aws-sdk/credential-provider-node": "^3.0.0", 153 | "@aws-sdk/credential-provider-process": "^3.0.0", 154 | "@aws-sdk/shared-ini-file-loader": "^3.0.0", 155 | "aws4": "^1.11.0", 156 | "axios": "^0.21.0", 157 | "axios-cookiejar-support": "^1.0.1", 158 | "form-data": "^3.0.0", 159 | "tough-cookie": "^4.0.0", 160 | "utf-8-validate": "^5.0.3", 161 | "xml2js": "^0.4.23" 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import axios, { AxiosRequestConfig, AxiosPromise } from 'axios'; 3 | import * as axiosCookieJarSupport from 'axios-cookiejar-support'; 4 | import * as tough from 'tough-cookie'; 5 | 6 | import * as aws4 from 'aws4'; 7 | import * as Utils from './Utils'; 8 | import * as ViewProviders from './ViewProviders'; 9 | import * as fs from 'fs'; 10 | import * as FormData from 'form-data' 11 | import { GetSessionTokenCommand } from '@aws-sdk/client-sts'; 12 | import { spawn } from 'child_process'; 13 | 14 | function getSessionManagerPath(): string { 15 | if (process.platform == "win32") { 16 | return "C:\\Program Files\\Amazon\\SessionManagerPlugin\\bin\\session-manager-plugin.exe"; 17 | } 18 | 19 | return "session-manager-plugin"; 20 | } 21 | 22 | export function activate(context: vscode.ExtensionContext) { 23 | console.info('AWS CloudShell extension loaded'); 24 | 25 | let sessionProvider = new ViewProviders.SessionProvider(); 26 | 27 | vscode.window.onDidCloseTerminal(terminal => { 28 | sessionProvider.onTerminalDisposed(terminal); 29 | }) 30 | 31 | let sessionView = vscode.window.createTreeView('aws-cloudshell-view-1-sessions', { 32 | 'treeDataProvider': sessionProvider 33 | }); 34 | 35 | context.subscriptions.push(vscode.commands.registerCommand('awscloudshell.startSession', async () => { 36 | try { 37 | await createSession(sessionProvider); 38 | } catch(err) { 39 | sessionProvider.onError(); 40 | vscode.window.setStatusBarMessage("", 60000); 41 | vscode.window.showErrorMessage(err.toString()); 42 | } 43 | })); 44 | 45 | context.subscriptions.push(vscode.commands.registerCommand('awscloudshell.uploadFile', async (file) => { 46 | uploadFile(file, sessionProvider); 47 | })); 48 | } 49 | 50 | async function uploadFile(file, sessionProvider: ViewProviders.SessionProvider) { 51 | const session = sessionProvider.getLastSession(); 52 | 53 | if (!session || session.state != "CONNECTED") { 54 | vscode.window.showWarningMessage("Session not connected, cannot proceed with upload"); 55 | return; 56 | } 57 | 58 | const filename = file.path.split("/").pop().split("\\").pop(); // TODO: Find an actual util for this 59 | 60 | let statusBar = vscode.window.setStatusBarMessage("$(globe) Uploading '" + filename + "'...", 60000); 61 | 62 | let awsreq = aws4.sign({ 63 | service: 'cloudshell', 64 | region: session.region, 65 | method: 'POST', 66 | path: '/getFileUploadUrls', 67 | headers: {}, 68 | body: JSON.stringify({ 69 | EnvironmentId: session.environmentId, 70 | FileUploadPath: filename 71 | }) 72 | }, session.creds); 73 | 74 | const csFileUploadPaths = await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 75 | headers: awsreq.headers 76 | }); 77 | 78 | const formData = Object.entries(csFileUploadPaths.data.FileUploadPresignedFields).reduce((fd, [ key, val ]) => 79 | (fd.append(key, val), fd), new FormData()); 80 | 81 | formData.append('File', fs.readFileSync(file.path)); 82 | 83 | const fileUpload = await axios.post(csFileUploadPaths.data.FileUploadPresignedUrl, formData, { 84 | headers: { 85 | "Content-Type": `multipart/form-data; boundary=${formData.getBoundary()}`, 86 | "Content-Length": formData.getLengthSync() 87 | } 88 | }); 89 | 90 | console.info("Uploaded"); 91 | 92 | awsreq = aws4.sign({ 93 | service: 'cloudshell', 94 | region: session.region, 95 | method: 'POST', 96 | path: '/createEnvironment', 97 | headers: {}, 98 | body: JSON.stringify({}) 99 | }, session.creds); 100 | 101 | const csEnvironment = await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 102 | headers: awsreq.headers 103 | }); 104 | 105 | let csSession = await startCsSession(csEnvironment, session); 106 | 107 | const sideSession = spawn(getSessionManagerPath(), [JSON.stringify(csSession.data), session.region, "StartSession"]); 108 | 109 | console.log(csFileUploadPaths.data.FileDownloadPresignedUrl); 110 | await new Promise(resolve => setTimeout(resolve, 3000)); 111 | 112 | sideSession.stdin.write("wget " + csFileUploadPaths.data.FileDownloadPresignedUrl + "\nexit\n"); 113 | sideSession.stdin.end(); 114 | 115 | statusBar.dispose(); 116 | } 117 | 118 | async function createSession(sessionProvider: ViewProviders.SessionProvider) { 119 | let awsregion = Utils.GetRegion(); 120 | let aws_creds = await Utils.GetAWSCreds(); 121 | 122 | if (!aws_creds.sessionToken) { 123 | vscode.window.showErrorMessage("The credentials provided do not have a session token. Please configure credentials which return a session token."); 124 | return; 125 | } 126 | 127 | axiosCookieJarSupport.default(axios); 128 | 129 | const cookieJar = new tough.CookieJar(); 130 | 131 | let statusBar = vscode.window.setStatusBarMessage("$(globe) Connecting to AWS CloudShell...", 60000); 132 | 133 | let session = sessionProvider.addSession(awsregion); 134 | 135 | let signintoken = await axios.get('https://signin.aws.amazon.com/federation?Action=getSigninToken&SessionDuration=3600&Session=' + encodeURIComponent(JSON.stringify({ 136 | 'sessionId': aws_creds.accessKey, 137 | 'sessionKey': aws_creds.secretKey, 138 | 'sessionToken': aws_creds.sessionToken, 139 | })), { 140 | jar: cookieJar, 141 | withCredentials: true 142 | }); 143 | 144 | await axios.get('https://signin.aws.amazon.com/federation?Action=login&Destination=' + encodeURIComponent('https://console.aws.amazon.com/console/home') + '&SigninToken=' + signintoken.data['SigninToken'], { 145 | jar: cookieJar, 146 | withCredentials: true 147 | }); 148 | 149 | let consoleHtmlResponse = await axios.get('https://' + (awsregion == 'us-east-1' ? '' : (awsregion + ".")) + 'console.aws.amazon.com/cloudshell/home?region=' + awsregion + '&state=hashArgs%23&hashArgs=%23', { 150 | jar: cookieJar, 151 | withCredentials: true 152 | }); 153 | 154 | let messyTagPrefix = '', startTag); 157 | 158 | const tbdata = JSON.parse(consoleHtmlResponse.data.substr(startTag + messyTagPrefix.length, endTag - startTag - messyTagPrefix.length).replace(/\"\;/g, "\"")); 159 | 160 | let credsResp = await axios.post('https://' + (awsregion == 'us-east-1' ? '' : (awsregion + ".")) + 'console.aws.amazon.com/cloudshell/tb/creds', null, { 161 | jar: cookieJar, 162 | withCredentials: true, 163 | headers: { 164 | 'x-csrf-token': tbdata.csrfToken, 165 | 'Accept': '*/*', 166 | 'Referer': 'https://' + (awsregion == 'us-east-1' ? '' : (awsregion + ".")) + 'console.aws.amazon.com/cloudshell/home?region=us-east-1' 167 | } 168 | }); 169 | 170 | aws_creds = credsResp.data; 171 | 172 | session.setCreds(aws_creds); 173 | 174 | let vpc_id = Utils.GetVPCId(); 175 | let subnet_id = Utils.GetSubnetId(); 176 | let security_group_id = Utils.GetSecurityGroupId(); 177 | let body = JSON.stringify({}); 178 | 179 | if (vpc_id && subnet_id && security_group_id) { 180 | body = JSON.stringify({ 181 | EnvironmentName: 'vscode-aws-cloudshell', 182 | VpcConfig: { 183 | VpcId: vpc_id, 184 | SecurityGroupIds: [security_group_id], 185 | SubnetIds: [subnet_id] 186 | } 187 | }) 188 | } 189 | 190 | let awsreq = aws4.sign({ 191 | service: 'cloudshell', 192 | region: awsregion, 193 | method: 'POST', 194 | path: '/createEnvironment', 195 | headers: {}, 196 | body: body 197 | }, aws_creds); 198 | 199 | const csEnvironment = await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 200 | headers: awsreq.headers 201 | }); 202 | 203 | console.log("Initial createEnvironment response:"); 204 | console.log(csEnvironment.data); 205 | 206 | session.setSessionName(csEnvironment.data.EnvironmentId.split("-")[0]); 207 | session.setEnvironmentId(csEnvironment.data.EnvironmentId); 208 | 209 | sessionProvider.refresh(); 210 | 211 | console.info("Connecting to " + csEnvironment.data.EnvironmentId + " (" + csEnvironment.data.Status + ")"); 212 | 213 | let csSession = await startCsSession(csEnvironment, session); 214 | 215 | // creds put 216 | 217 | try { 218 | awsreq = aws4.sign({ 219 | service: 'cloudshell', 220 | host: 'auth.cloudshell.' + awsregion + '.aws.amazon.com', 221 | region: awsregion, 222 | method: 'GET', 223 | signQuery: true, 224 | path: "/oauth?EnvironmentId=" + csEnvironment.data.EnvironmentId + "&codeVerifier=R0r-XINZhRJqEkRk-2EjocwI2aqrhcjO6IlGRPYcIo0&redirectUri=" + encodeURIComponent('https://auth.cloudshell.' + awsregion + '.aws.amazon.com/callback.js?state=1') 225 | }, aws_creds); 226 | 227 | const csOAuth = await axios.get("https://auth.cloudshell." + awsregion + ".aws.amazon.com" + awsreq.path, { 228 | jar: cookieJar, 229 | withCredentials: true 230 | }); 231 | 232 | messyTagPrefix = 'main("'; 233 | startTag = csOAuth.data.indexOf(messyTagPrefix); 234 | endTag = csOAuth.data.indexOf('", "', startTag); 235 | 236 | const authcode = csOAuth.data.substr(startTag + messyTagPrefix.length, endTag - startTag - messyTagPrefix.length); 237 | 238 | let cookies = cookieJar.getCookiesSync("https://auth.cloudshell." + awsregion + ".aws.amazon.com/"); 239 | 240 | let keybase = ''; 241 | for (let cookie of cookies) { 242 | if (cookie.key == "aws-userInfo") { 243 | keybase = JSON.parse(decodeURIComponent(cookie.value))['keybase']; 244 | } 245 | } 246 | 247 | awsreq = aws4.sign({ 248 | service: 'cloudshell', 249 | region: awsregion, 250 | method: 'POST', 251 | path: '/redeemCode', 252 | headers: {}, 253 | body: JSON.stringify({ 254 | AuthCode: authcode, 255 | CodeVerifier: "cfd87ed2-16b3-432e-8278-e3afdfc6b235c1a6b90c-33e3-43a6-9801-02d742274b9c", 256 | EnvironmentId: csEnvironment.data.EnvironmentId, 257 | KeyBase: keybase, 258 | RedirectUri: "https://auth.cloudshell." + awsregion + ".aws.amazon.com/callback.js?state=1" 259 | }) 260 | }, aws_creds); 261 | 262 | const csRedeem = await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 263 | headers: awsreq.headers 264 | }); 265 | 266 | console.log("redeemCode response:"); 267 | console.log(csRedeem.data); 268 | 269 | awsreq = aws4.sign({ 270 | service: 'cloudshell', 271 | region: awsregion, 272 | method: 'POST', 273 | path: '/putCredentials', 274 | headers: {}, 275 | body: JSON.stringify({ 276 | EnvironmentId: csEnvironment.data.EnvironmentId, 277 | KeyBase: keybase, 278 | RefreshToken: csRedeem.data.RefreshToken 279 | }) 280 | }, aws_creds); 281 | 282 | await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 283 | headers: awsreq.headers 284 | }); 285 | } catch(err) { 286 | console.log(err.response); 287 | console.log(err.data); 288 | console.log(err); 289 | vscode.window.showWarningMessage("Could not apply AWS credentials to environment"); 290 | } 291 | 292 | // 293 | 294 | const terminal = vscode.window.createTerminal("AWS CloudShell", getSessionManagerPath(), [JSON.stringify(csSession.data), awsregion, "StartSession"]); 295 | 296 | session.setTerminal(terminal); 297 | sessionProvider.refresh(); 298 | 299 | terminal.show(); 300 | 301 | statusBar.dispose(); 302 | 303 | session.setConnected(); 304 | sessionProvider.refresh(); 305 | 306 | vscode.window.setStatusBarMessage("$(globe) Connected to AWS CloudShell", 3000); 307 | } 308 | 309 | async function startCsSession(csEnvironment, session) { 310 | try { 311 | while (csEnvironment.data.Status != "RUNNING") { 312 | try { 313 | let awsreq = aws4.sign({ 314 | service: 'cloudshell', 315 | region: session.region, 316 | method: 'POST', 317 | path: '/startEnvironment', 318 | headers: {}, 319 | body: JSON.stringify({ 320 | EnvironmentId: csEnvironment.data.EnvironmentId 321 | }) 322 | }, session.creds); 323 | 324 | const csEnvironmentStart = await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 325 | headers: awsreq.headers 326 | }); 327 | 328 | console.log("startEnvironment response:"); 329 | console.log(csEnvironmentStart.data); 330 | 331 | let environmentStatus = "RESUMING"; 332 | while (environmentStatus == "RESUMING") { 333 | await new Promise(resolve => setTimeout(resolve, 2000)); 334 | 335 | awsreq = aws4.sign({ 336 | service: 'cloudshell', 337 | region: session.region, 338 | method: 'POST', 339 | path: '/getEnvironmentStatus', 340 | headers: {}, 341 | body: JSON.stringify({ 342 | EnvironmentId: csEnvironment.data.EnvironmentId 343 | }) 344 | }, session.creds); 345 | 346 | csEnvironment = await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 347 | headers: awsreq.headers 348 | }); 349 | 350 | console.log("getEnvironmentStatus response:"); 351 | console.log(csEnvironment.data); 352 | 353 | environmentStatus = csEnvironment.data.Status; 354 | } 355 | } catch(err) { 356 | await new Promise(resolve => setTimeout(resolve, 3000)); 357 | } 358 | } 359 | 360 | let awsreq = aws4.sign({ 361 | service: 'cloudshell', 362 | region: session.region, 363 | method: 'POST', 364 | path: '/createSession', 365 | headers: {}, 366 | body: JSON.stringify({ 367 | 'EnvironmentId': csEnvironment.data.EnvironmentId 368 | }) 369 | }, session.creds); 370 | 371 | const csSession = await axios.post("https://" + awsreq.hostname + awsreq.path, awsreq.body, { 372 | headers: awsreq.headers 373 | }); 374 | 375 | console.log("createSession response:"); 376 | console.log(csSession.data); 377 | 378 | return csSession; 379 | } catch(err) { 380 | console.log(err.response); 381 | console.log(err.data); 382 | console.log(err); 383 | throw err; 384 | } 385 | } 386 | --------------------------------------------------------------------------------