CloudShell.ps1
# Starts Azure Cloud Shell session # Sep 8th 2020 function Start-CloudShell { <# .SYNOPSIS Starts an Azure Cloud Shell session. .DESCRIPTION Starts an Azure Cloud Shell session for the given user. Note: Does not work with VSCode or ISE. .Parameter AccessToken The access token used to start the session. .EXAMPLE Get-AADIntAccessTokenForCloudShell -SaveToCache PS\:>Start-AADIntCloudShell #> [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [String]$AccessToken, [ValidateSet('PowerShell','Bash')] [String]$Shell="PowerShell", [Parameter(Mandatory=$False)] [guid]$SubscriptionId, [Parameter(Mandatory=$False)] [String]$ResourceGroup, [Parameter(Mandatory=$False)] [String]$StorageAccount, [Parameter(Mandatory=$False)] [String]$FileShareName ) Process { # Get from cache if not provided $AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "00000006-0000-0ff1-ce00-000000000000" if(!$host.UI.SupportsVirtualTerminal) { Write-Error "PowerShell ISE or VSCode not supported!" return } try { # Check the user settings $userSettings = Get-UserCloudShellSettings -AccessToken $AccessToken if(!$userSettings) { # User has no settings, we need storage account and fileshare information to create new settings if([string]::IsNullOrEmpty($StorageAccount) -or [string]::IsNullOrEmpty($ResourceGroup) -or [string]::IsNullOrEmpty($FileShareName) -or ($SubscriptionId -eq $null)) { Write-Warning "User has no cloud shell settings. If connection fails, please provide Storage Account and FileShare details." } else { # Let's try to create setting $StorageAccountId = "/subscriptions/$SubscriptionId/resourcegroups/$ResourceGroup/providers/Microsoft.Storage/storageAccounts/$StorageAccount" $userSettings = Set-UserCloudShellSettings -AccessToken $AccessToken -StorageAccountId $StorageAccountId -fileShareName $fileShareName } } else { Write-Verbose "User settings received!" } # Get the shell info $shellInfo = New-CloudShell -AccessToken $AccessToken Write-Verbose "Created shell $($shellInfo.uri)" # Get the authorization code $authToken = Get-CloudShellAuthToken -AccessToken $AccessToken -Url $shellInfo.uri Write-Verbose "Received auth-token $authToken" # Get the settings $settings = Get-CloudShellSettings -AccessToken $AccessToken -Url $shellInfo.uri -Shell $Shell Write-Verbose "Received cloud shell settings" } catch { Write-Error "Failed to connect to Cloud Shell $($_.Message)" return } # Save the current setting for Ctrl+C $CtrlC = [console]::TreatControlCAsInput Try { $url = $settings.socketUri # Create the socket and keep alive $socket = New-Object System.Net.WebSockets.ClientWebSocket # Set the cookies $cookiec = [System.Net.CookieContainer]::new(1) $cookie = [System.Net.Cookie]::new("auth-token", $authToken) $cookie.Domain = ".console.azure.com" $cookiec.Add($cookie) $socket.Options.Cookies = $cookiec # Create the token and open the connection $token = New-Object System.Threading.CancellationToken $connection = $socket.ConnectAsync($url, $token) Write-Verbose "Connecting to socket $($settings.socketUri)" # Wait 'till the connection is completed While (!$connection.IsCompleted) { Start-Sleep -Milliseconds 100 } if($connection.IsFaulted -eq "True") { Write-Error $connection.Exception return } Write-Verbose "Connected to socket." # Buffer for the content $buffer = New-Object Byte[] 1024 $socket_in = $Socket.ReceiveAsync($buffer, $Token) # Clear the console and set the Ctlr+C to be used as an input (so that we can stop things running in cloud) [console]::TreatControlCAsInput = $true [console]::Clear() # The main loop do { # If the read is completed, print it to console and start another read if($socket_in.IsCompleted) { $retVal = $buffer[0..$($socket_in.Result.Count-1)] $text = [text.encoding]::UTF8.GetString($retVal) [console]::Write($text) $socket_in = $Socket.ReceiveAsync($buffer, $Token) } # Read the key if available if([console]::KeyAvailable) { $key = [console]::ReadKey($True) # https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences switch($key.Key) { "Insert" { $keyBytes = [byte[]]@(27,91,50,126) } "Delete" { $keyBytes = [byte[]]@(27,91,51,126) } "PageUp" { $keyBytes = [byte[]]@(27,91,53,126) } "PageDown" { $keyBytes = [byte[]]@(27,91,54,126) } "UpArrow" { $keyBytes = [byte[]]@(27,79,65) } "DownArrow" { $keyBytes = [byte[]]@(27,79,66) } "RightArrow" { $keyBytes = [byte[]]@(27,79,67) } "LeftArrow" { $keyBytes = [byte[]]@(27,79,68) } "Home" { $keyBytes = [byte[]]@(27,79,72) } "End" { $keyBytes = [byte[]]@(27,79,70) } default { $keyBytes = [text.encoding]::UTF8.GetBytes($key.KeyChar) } } SendToSocket -Socket $socket -Token $token -Bytes $keyBytes } } Until (!$connection -or $socket_in.IsFaulted -eq "True") } Catch { Write-Error $_ } Finally { # Return the original Ctrl+C [console]::TreatControlCAsInput = $CtrlC If ($socket) { Write-Verbose "Closing websocket" $socket.Dispose() } } } } |