diff --git a/src/Ssh/Ssh.AlcWrapper/Ssh.AlcWrapper.csproj b/src/Ssh/Ssh.AlcWrapper/Ssh.AlcWrapper.csproj
new file mode 100644
index 000000000000..87b66684a620
--- /dev/null
+++ b/src/Ssh/Ssh.AlcWrapper/Ssh.AlcWrapper.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Ssh
+ true
+ true
+
+
+
+
+
+ $(LegacyAssemblyPrefix)$(PsModuleName)
+ $(LegacyAssemblyPrefix)$(PsModuleName).AlcWrapper
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ssh/Ssh.AlcWrapper/SshServices.cs b/src/Ssh/Ssh.AlcWrapper/SshServices.cs
new file mode 100644
index 000000000000..c8a986e18716
--- /dev/null
+++ b/src/Ssh/Ssh.AlcWrapper/SshServices.cs
@@ -0,0 +1,328 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using Microsoft.Azure.Management.Compute;
+using Microsoft.Azure.Management.Compute.Models;
+using Microsoft.Azure.Management.Network;
+using Microsoft.Azure.Management.HybridCompute;
+using Microsoft.Azure.Management.HybridCompute.Models;
+using Microsoft.Azure.Management.Internal.Resources.Utilities.Models;
+using Microsoft.Azure.Commands.Common.Authentication;
+using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
+using Microsoft.Azure.Commands.Common.Exceptions;
+using System.Linq;
+using Microsoft.Rest.Azure;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Azure.Commands.Ssh
+{
+ ///
+ /// Client to make API calls to Azure Compute Resource Provider.
+ ///
+ internal class ComputeClient
+ {
+ public IComputeManagementClient ComputeManagementClient { get; private set; }
+
+ public ComputeClient(IAzureContext context)
+ : this(AzureSession.Instance.ClientFactory.CreateArmClient(
+ context, AzureEnvironment.Endpoint.ResourceManager))
+ {
+ }
+
+ public ComputeClient(IComputeManagementClient computeManagementClient)
+ {
+ this.ComputeManagementClient = computeManagementClient;
+ }
+ }
+
+ ///
+ /// Client to make API calls to Azure Compute Resource Provider.
+ ///
+ internal class NetworkClient
+ {
+ public INetworkManagementClient NetworkManagementClient { get; private set; }
+
+ public NetworkClient(IAzureContext context)
+ : this(AzureSession.Instance.ClientFactory.CreateArmClient(
+ context, AzureEnvironment.Endpoint.ResourceManager))
+ {
+ }
+
+ public NetworkClient(INetworkManagementClient networkManagementClient)
+ {
+ this.NetworkManagementClient = networkManagementClient;
+ }
+ }
+
+ ///
+ /// Client to make API calls to Azure Hybrid Compute Resource Provider.
+ ///
+ internal class HybridComputeClient
+ {
+ public IHybridComputeManagementClient HybridComputeManagementClient { get; private set; }
+
+ public HybridComputeClient(IAzureContext context)
+ : this(AzureSession.Instance.ClientFactory.CreateArmClient(
+ context, AzureEnvironment.Endpoint.ResourceManager))
+ {
+ }
+
+ public HybridComputeClient(IHybridComputeManagementClient hybridComputeManagementClient)
+ {
+ this.HybridComputeManagementClient = hybridComputeManagementClient;
+ }
+ }
+
+ ///
+ /// Class that provides utility methods that rely on external Azure Services.
+ ///
+ public sealed class SshAzureUtils
+ {
+ private ComputeClient _computeClient;
+ private NetworkClient _networkClient;
+ private HybridComputeClient _hybridClient;
+ private IAzureContext context;
+
+ public SshAzureUtils(IAzureContext azureContext)
+ {
+ context = azureContext;
+ }
+
+ private ComputeClient ComputeClient
+ {
+ get
+ {
+ if (_computeClient == null)
+ {
+ _computeClient = new ComputeClient(context);
+ }
+
+ return _computeClient;
+ }
+
+ set { _computeClient = value; }
+ }
+
+ private NetworkClient NetworkClient
+ {
+ get
+ {
+ if (_networkClient == null)
+ {
+ _networkClient = new NetworkClient(context);
+ }
+ return _networkClient;
+ }
+
+ set { _networkClient = value; }
+ }
+
+ private HybridComputeClient HybridClient
+ {
+ get
+ {
+ if (_hybridClient == null)
+ {
+ _hybridClient = new HybridComputeClient(context);
+ }
+ return _hybridClient;
+ }
+
+ set { _hybridClient = value; }
+ }
+
+ private IVirtualMachinesOperations VirtualMachineClient
+ {
+ get
+ {
+ return ComputeClient.ComputeManagementClient.VirtualMachines;
+ }
+ }
+
+ private IMachinesOperations ArcMachineClient
+ {
+ get
+ {
+ return HybridClient.HybridComputeManagementClient.Machines;
+ }
+ }
+
+ ///
+ /// Gets the first public IP of an Azure Virtual Machine.
+ ///
+ /// Azure Virtual Machine Name
+ /// Resource Group Name
+ /// Virtual Machine Public IP
+ public string GetFirstPublicIp(string vmName, string rgName)
+ {
+ var result = this.VirtualMachineClient.GetWithHttpMessagesAsync(
+ rgName, vmName).GetAwaiter().GetResult();
+
+ VirtualMachine vm = result.Body;
+
+ string publicIpAddress = null;
+
+ foreach (var nicReference in vm.NetworkProfile.NetworkInterfaces)
+ {
+ ResourceIdentifier parsedNicId = new ResourceIdentifier(nicReference.Id);
+ string nicRg = parsedNicId.ResourceGroupName;
+ string nicName = parsedNicId.ResourceName;
+
+ var nic = this.NetworkClient.NetworkManagementClient.NetworkInterfaces.GetWithHttpMessagesAsync(
+ nicRg, nicName).GetAwaiter().GetResult();
+
+ var publicIps = nic.Body.IpConfigurations.Where(ipconfig => ipconfig.PublicIPAddress != null).Select(ipconfig => ipconfig.PublicIPAddress);
+ foreach (var ip in publicIps)
+ {
+ ResourceIdentifier parsedIpId = new ResourceIdentifier(ip.Id);
+ var ipRg = parsedIpId.ResourceGroupName;
+ var ipName = parsedIpId.ResourceName;
+ var ipAddress = this.NetworkClient.NetworkManagementClient.PublicIPAddresses.GetWithHttpMessagesAsync(
+ ipRg, ipName).GetAwaiter().GetResult().Body;
+
+ publicIpAddress = ipAddress.IpAddress;
+
+ if (!string.IsNullOrEmpty(publicIpAddress))
+ {
+ break;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(publicIpAddress))
+ {
+ break;
+ }
+ }
+ return publicIpAddress;
+ }
+
+ ///
+ /// Makes call to Azure to confirm the type of the target and if the resource
+ /// really exists in this context.
+ ///
+ /// Resource Name
+ /// Resource Group Name
+ ///
+ /// Either "Microsoft.HybridCompute/machine" or
+ /// "Microsoft.Compute/virtualMachines"
+ ///
+ ///
+ /// Either "Microsoft.HybridCompute/machine" or "Microsoft.Compute/virtualMachines".
+ /// If type of the target can't be confirmed, then throw a terminating error.
+ ///
+ public string DecideResourceType(
+ string vmName,
+ string rgName,
+ string ResourceType)
+ {
+
+ string hybridExceptionMessage;
+ string computeExceptionMessage;
+
+ if (ResourceType != null)
+ {
+ if (ResourceType.Equals("Microsoft.HybridCompute/machines"))
+ {
+ if (TryArcServer(vmName, rgName, out hybridExceptionMessage))
+ {
+ return "Microsoft.HybridCompute/machines";
+ }
+
+ throw new AzPSCloudException("Failed to get Azure Arc Server. Error: " + hybridExceptionMessage);
+ }
+ else if (ResourceType.Equals("Microsoft.Compute/virtualMachines"))
+ {
+ if (TryAzureVM(vmName, rgName, out computeExceptionMessage))
+ {
+ return "Microsoft.Compute/virtualMachines";
+ }
+
+ throw new AzPSCloudException("Failed to get Azure Arc Server. Error: " + computeExceptionMessage);
+ }
+ }
+
+ bool isArc = TryArcServer(vmName, rgName, out hybridExceptionMessage);
+ bool isAzVM = TryAzureVM(vmName, rgName, out computeExceptionMessage);
+
+ if (isArc && isAzVM)
+ {
+ throw new AzPSCloudException("A arc server and a azure vm with the same name. Please provide -ResourceType argument.");
+ }
+ else if (!isArc && !isAzVM)
+ {
+ throw new AzPSCloudException("Unable to determine the target machine type as azure vm or arc server. Errors: \n" + hybridExceptionMessage + "\n" + computeExceptionMessage);
+ }
+ else if (isArc)
+ {
+ return "Microsoft.HybridCompute/machines";
+ }
+
+ return "Microsoft.Compute/virtualMachines";
+ }
+
+ private bool TryAzureVM(
+ string vmName,
+ string rgName,
+ out string azexceptionMessage)
+ {
+ azexceptionMessage = null;
+ try
+ {
+ var result = this.VirtualMachineClient.GetWithHttpMessagesAsync(
+ rgName, vmName).GetAwaiter().GetResult();
+ }
+ catch (CloudException exception)
+ {
+ if (exception.Response.StatusCode == System.Net.HttpStatusCode.NotFound || exception.Response.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ JObject json = JObject.Parse(exception.Response.Content);
+ azexceptionMessage = (string)json["error"]["message"];
+ return false;
+ }
+
+ // Unexpected exception we can't handle.
+ throw;
+ }
+
+ return true;
+ }
+
+ private bool TryArcServer(
+ string vmName,
+ string rgName,
+ out string azexceptionMessage)
+ {
+ azexceptionMessage = null;
+ try
+ {
+ var result = this.ArcMachineClient.GetWithHttpMessagesAsync(rgName, vmName).GetAwaiter().GetResult();
+ }
+ catch (ErrorResponseException exception)
+ {
+ if (exception.Response.StatusCode == System.Net.HttpStatusCode.NotFound || exception.Response.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ JObject json = JObject.Parse(exception.Response.Content);
+ azexceptionMessage = (string)json["error"]["message"];
+ return false;
+ }
+
+ // Unexpected exception we can't handle.
+ throw;
+ }
+
+ return true;
+ }
+ }
+
+}
diff --git a/src/Ssh/Ssh.Test/Properties/AssemblyInfo.cs b/src/Ssh/Ssh.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000000..44f614f84f04
--- /dev/null
+++ b/src/Ssh/Ssh.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System.Reflection;
+using System.Runtime.InteropServices;
+using Xunit;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Microsoft.Azure.Commands.Ssh.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Microsoft.Azure.Commands.Ssh.Test")]
+[assembly: AssemblyCopyright(Microsoft.WindowsAzure.Commands.Common.AzurePowerShell.AssemblyCopyright)]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("822d5889-438f-4c18-9c06-f5db728d417d")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("0.0.1")]
+[assembly: AssemblyFileVersion("0.0.1")]
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
diff --git a/src/Ssh/Ssh.Test/Resources/DummyKey.txt b/src/Ssh/Ssh.Test/Resources/DummyKey.txt
new file mode 100644
index 000000000000..365f098c9617
--- /dev/null
+++ b/src/Ssh/Ssh.Test/Resources/DummyKey.txt
@@ -0,0 +1 @@
+AAAAB3NzaC1yc2EAAAADAQABAAABAQCvAtoiFV59d8kaHUh82Uy5bso5g6/XWQku6++zfWNon5mpy8SLWT2QKiTUeWuM86wOmBhcFbO/8Powov8bWzZRkgNKnwyPOL3Pp3uyq9maGbYcGeOS757hZgdmJMuly/o7tGnJu0w2Efp8eZdGmz1FZOi4p+s5+EWAL8jOCFwXEuZm+mjsCIrJVlfZ6ArvunQ/NS+2ld4c8xsxH/smAJHw34wXI143dytjOmQLN8AbGhD927CnNLcFdJ2edqTBC5U7Rw/hy5hac9xVt/McK00bqAVOhzxIcxk6JiyaMSaCgaILw1s6IiRiJcF/vCH2FYIhzRFPbOtYFBc1SH73rMz3
\ No newline at end of file
diff --git a/src/Ssh/Ssh.Test/Ssh.Test.csproj b/src/Ssh/Ssh.Test/Ssh.Test.csproj
new file mode 100644
index 000000000000..04b94fa27f26
--- /dev/null
+++ b/src/Ssh/Ssh.Test/Ssh.Test.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Cdn
+
+
+
+
+
+ $(LegacyAssemblyPrefix)$(PsModuleName)$(AzTestAssemblySuffix).ScenarioTests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ssh/Ssh.sln b/src/Ssh/Ssh.sln
new file mode 100644
index 000000000000..f73b58db9136
--- /dev/null
+++ b/src/Ssh/Ssh.sln
@@ -0,0 +1,78 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31702.278
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssh", "Ssh\Ssh.csproj", "{42656543-77AD-4968-BA4B-BE0778705625}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssh.Test", "Ssh.Test\Ssh.Test.csproj", "{5E5BBB82-2D69-4A12-93AA-E5753F87AF03}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Accounts", "..\Accounts\Accounts\Accounts.csproj", "{142D7B0B-388A-4CEB-A228-7F6D423C5C2E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Authentication", "..\Accounts\Authentication\Authentication.csproj", "{FF81DC73-B8EC-4082-8841-4FBF2B16E7CE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Authenticators", "..\Accounts\Authenticators\Authenticators.csproj", "{F12822FD-CE14-4C35-80CC-5094C82955C9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Authentication.ResourceManager", "..\Accounts\Authentication.ResourceManager\Authentication.ResourceManager.csproj", "{3E016018-D65D-4336-9F64-17DA97783AD0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScenarioTest.ResourceManager", "..\..\tools\ScenarioTest.ResourceManager\ScenarioTest.ResourceManager.csproj", "{F83FBA8D-732D-437C-A0E2-02E45B01E123}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFx", "..\..\tools\TestFx\TestFx.csproj", "{BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthenticationAssemblyLoadContext", "..\Accounts\AuthenticationAssemblyLoadContext\AuthenticationAssemblyLoadContext.csproj", "{9CB02F6D-A509-49F1-BE2F-11F537A6CABE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssh.AlcWrapper", "Ssh.AlcWrapper\Ssh.AlcWrapper.csproj", "{104DF33D-4EC3-4C64-A0E7-FB4DEB7EFC98}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {42656543-77AD-4968-BA4B-BE0778705625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {42656543-77AD-4968-BA4B-BE0778705625}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {42656543-77AD-4968-BA4B-BE0778705625}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {42656543-77AD-4968-BA4B-BE0778705625}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5E5BBB82-2D69-4A12-93AA-E5753F87AF03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5E5BBB82-2D69-4A12-93AA-E5753F87AF03}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5E5BBB82-2D69-4A12-93AA-E5753F87AF03}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5E5BBB82-2D69-4A12-93AA-E5753F87AF03}.Release|Any CPU.Build.0 = Release|Any CPU
+ {142D7B0B-388A-4CEB-A228-7F6D423C5C2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {142D7B0B-388A-4CEB-A228-7F6D423C5C2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {142D7B0B-388A-4CEB-A228-7F6D423C5C2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {142D7B0B-388A-4CEB-A228-7F6D423C5C2E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF81DC73-B8EC-4082-8841-4FBF2B16E7CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF81DC73-B8EC-4082-8841-4FBF2B16E7CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF81DC73-B8EC-4082-8841-4FBF2B16E7CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF81DC73-B8EC-4082-8841-4FBF2B16E7CE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F12822FD-CE14-4C35-80CC-5094C82955C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F12822FD-CE14-4C35-80CC-5094C82955C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F12822FD-CE14-4C35-80CC-5094C82955C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F12822FD-CE14-4C35-80CC-5094C82955C9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3E016018-D65D-4336-9F64-17DA97783AD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3E016018-D65D-4336-9F64-17DA97783AD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3E016018-D65D-4336-9F64-17DA97783AD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3E016018-D65D-4336-9F64-17DA97783AD0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F83FBA8D-732D-437C-A0E2-02E45B01E123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F83FBA8D-732D-437C-A0E2-02E45B01E123}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F83FBA8D-732D-437C-A0E2-02E45B01E123}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F83FBA8D-732D-437C-A0E2-02E45B01E123}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9CB02F6D-A509-49F1-BE2F-11F537A6CABE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9CB02F6D-A509-49F1-BE2F-11F537A6CABE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9CB02F6D-A509-49F1-BE2F-11F537A6CABE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9CB02F6D-A509-49F1-BE2F-11F537A6CABE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {104DF33D-4EC3-4C64-A0E7-FB4DEB7EFC98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {104DF33D-4EC3-4C64-A0E7-FB4DEB7EFC98}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {104DF33D-4EC3-4C64-A0E7-FB4DEB7EFC98}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {104DF33D-4EC3-4C64-A0E7-FB4DEB7EFC98}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F9B3D96E-9680-40BE-A917-02EE655D6030}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Ssh/Ssh/Az.Ssh.psd1 b/src/Ssh/Ssh/Az.Ssh.psd1
new file mode 100644
index 000000000000..3e1fe84400fe
--- /dev/null
+++ b/src/Ssh/Ssh/Az.Ssh.psd1
@@ -0,0 +1,131 @@
+#
+# Module manifest for module 'Az.Ssh'
+#
+# Generated by: Microsoft Corporation
+#
+# Generated on: 8/22/2019
+#
+
+@{
+
+# Script module or binary module file associated with this manifest.
+# RootModule = ''
+
+# Version number of this module.
+ModuleVersion = '0.0.1'
+
+# Supported PSEditions
+CompatiblePSEditions = 'Core', 'Desktop'
+
+# ID used to uniquely identify this module
+GUID = '91832aaa-dc11-4583-8239-bce5fd531604'
+
+# Author of this module
+Author = 'Microsoft Corporation'
+
+# Company or vendor of this module
+CompanyName = 'Microsoft Corporation'
+
+# Copyright statement for this module
+Copyright = 'Microsoft Corporation. All rights reserved.'
+
+# Description of the functionality provided by this module
+Description = 'Microsoft Azure PowerShell - cmdlets for connecting to Azure VMs using SSH in Windows PowerShell and PowerShell Core.'
+
+# Minimum version of the PowerShell engine required by this module
+PowerShellVersion = '5.1'
+
+# Name of the PowerShell host required by this module
+# PowerShellHostName = ''
+
+# Minimum version of the PowerShell host required by this module
+# PowerShellHostVersion = ''
+
+# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+DotNetFrameworkVersion = '4.7.2'
+
+# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+# CLRVersion = ''
+
+# Processor architecture (None, X86, Amd64) required by this module
+# ProcessorArchitecture = ''
+
+# Modules that must be imported into the global environment prior to importing this module
+RequiredModules = @(@{ModuleName = 'Az.Accounts'; ModuleVersion = '2.7.1'; })
+
+# Assemblies that must be loaded prior to importing this module
+#RequiredAssemblies = '.\Microsoft.Azure.Management.Compute.dll', '.\Microsoft.Azure.Management.Network.dll', '.\Microsoft.Azure.Management.HybridCompute.dll'
+
+# Script files (.ps1) that are run in the caller's environment prior to importing this module.
+# ScriptsToProcess = @()
+
+# Type files (.ps1xml) to be loaded when importing this module
+# TypesToProcess = @()
+
+# Format files (.ps1xml) to be loaded when importing this module
+# FormatsToProcess = @()
+
+# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+NestedModules = @('.\Microsoft.Azure.PowerShell.Cmdlets.Ssh.dll')
+
+# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+FunctionsToExport = @()
+
+# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+CmdletsToExport = 'Export-AzSshConfig', 'Enter-AzVM'
+# Variables to export from this module
+# VariablesToExport = @()
+
+# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+AliasesToExport = @()
+
+# DSC resources to export from this module
+# DscResourcesToExport = @()
+
+# List of all modules packaged with this module
+# ModuleList = @()
+
+# List of all files packaged with this module
+# FileList = @()
+
+# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+PrivateData = @{
+
+ PSData = @{
+
+ # Tags applied to this module. These help with module discovery in online galleries.
+ Tags = 'Azure','ResourceManager','ARM','VM', 'SSH'
+
+ # A URL to the license for this module.
+ LicenseUri = 'https://aka.ms/azps-license'
+
+ # A URL to the main website for this project.
+ ProjectUri = 'https://github.com/Azure/azure-powershell'
+
+ # A URL to an icon representing this module.
+ # IconUri = ''
+
+ # ReleaseNotes of this module
+ ReleaseNotes = ''
+
+ # Prerelease string of this module
+ # Prerelease = ''
+
+ # Flag to indicate whether the module requires explicit user acceptance for install/update/save
+ # RequireLicenseAcceptance = $false
+
+ # External dependent modules of this module
+ # ExternalModuleDependencies = @()
+
+ } # End of PSData hashtable
+
+ } # End of PrivateData hashtable
+
+# HelpInfo URI of this module
+# HelpInfoURI = ''
+
+# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
+# DefaultCommandPrefix = ''
+
+}
+
diff --git a/src/Ssh/Ssh/ChangeLog.md b/src/Ssh/Ssh/ChangeLog.md
new file mode 100644
index 000000000000..19fa7feeca76
--- /dev/null
+++ b/src/Ssh/Ssh/ChangeLog.md
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/src/Ssh/Ssh/Common/RSACertGenerator.cs b/src/Ssh/Ssh/Common/RSACertGenerator.cs
new file mode 100644
index 000000000000..a5f494b21f23
--- /dev/null
+++ b/src/Ssh/Ssh/Common/RSACertGenerator.cs
@@ -0,0 +1,47 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace Microsoft.Azure.Commands.Ssh
+{
+ internal class RSACertGenerator
+ {
+ private const string RsaOpenSshPrefix = "ssh-rsa-cert-v01@openssh.com";
+
+ private string certBytes;
+ private string certFileContents;
+
+ public string CertificateContents
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(certFileContents))
+ {
+ certFileContents = GetContents(certBytes);
+ }
+
+ return certFileContents;
+ }
+ }
+
+ public RSACertGenerator(string certBytes)
+ {
+ this.certBytes = certBytes;
+ }
+
+ private string GetContents(string certBytes)
+ {
+ return string.Format("{0} {1}", RsaOpenSshPrefix, certBytes);
+ }
+ }
+}
diff --git a/src/Ssh/Ssh/Common/RSAParser.cs b/src/Ssh/Ssh/Common/RSAParser.cs
new file mode 100644
index 000000000000..16bef24b5fc0
--- /dev/null
+++ b/src/Ssh/Ssh/Common/RSAParser.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Azure.Commands.Ssh
+{
+ internal class RSAParser
+ {
+ public const string KeyType = "RSA";
+
+ private const string KeyTypeKey = "kty";
+ private const string ModulusKey = "n";
+ private const string ExponentKey = "e";
+
+ private string keyId;
+
+ public string Modulus { get; private set; }
+ public string Exponent { get; private set; }
+
+ public string KeyId
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(keyId))
+ {
+ keyId = GetKeyId(Modulus);
+ }
+
+ return keyId;
+ }
+ }
+
+ public Dictionary Jwk
+ {
+ get
+ {
+ return new Dictionary
+ {
+ { KeyTypeKey, KeyType },
+ { ModulusKey, Modulus },
+ { ExponentKey, Exponent }
+ };
+ }
+ }
+
+ public RSAParser(string publicKeyFileContents)
+ {
+ ParseKey(publicKeyFileContents);
+ }
+
+ private void ParseKey(string publicKey)
+ {
+ string[] keyParts = publicKey.Split(' ');
+
+ if (keyParts.Length != 3)
+ {
+ throw new ArgumentException();
+ }
+
+ Span keyBytes = Convert.FromBase64String(keyParts[1]);
+
+ int read = 0;
+ Span firstLengthBytes = keyBytes.Slice(read, 4);
+ firstLengthBytes.Reverse();
+ int length = Convert.ToInt32(BitConverter.ToUInt32(firstLengthBytes.ToArray(), 0));
+
+ read += 4;
+ Span algorithm = keyBytes.Slice(read, Convert.ToInt32(length));
+
+ read += length;
+ Span secondLengthBytes = keyBytes.Slice(read, 4);
+ secondLengthBytes.Reverse();
+ length = Convert.ToInt32(BitConverter.ToUInt32(secondLengthBytes.ToArray(), 0));
+
+ read += 4;
+ Span exponent = keyBytes.Slice(read, length);
+
+ read += length;
+ Span thirdLengthBytes = keyBytes.Slice(read, 4);
+ thirdLengthBytes.Reverse();
+ length = Convert.ToInt32(BitConverter.ToUInt32(thirdLengthBytes.ToArray(), 0));
+
+ read += 4;
+ Span modulus = keyBytes.Slice(read, length);
+
+ Exponent = Convert.ToBase64String(exponent.ToArray());
+ Modulus = Convert.ToBase64String(modulus.ToArray());
+ }
+
+ private string GetKeyId(string modulus)
+ {
+ return modulus.GetHashCode().ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ssh/Ssh/Common/SshBaseCmdlet.cs b/src/Ssh/Ssh/Common/SshBaseCmdlet.cs
new file mode 100644
index 000000000000..6f918e46b61b
--- /dev/null
+++ b/src/Ssh/Ssh/Common/SshBaseCmdlet.cs
@@ -0,0 +1,813 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using Microsoft.Azure.Commands.Common.Authentication.Models;
+using Microsoft.Azure.Commands.ResourceManager.Common;
+using System.Management.Automation;
+using Microsoft.WindowsAzure.Commands.Utilities.Common;
+using Microsoft.Azure.Commands.Common.Authentication;
+using System.Text.RegularExpressions;
+using System.IO;
+using System.Text;
+using System.Runtime.InteropServices;
+using System;
+using System.Security.Cryptography;
+using System.Diagnostics;
+using System.Net;
+using Microsoft.Azure.Commands.Common.Exceptions;
+using System.Collections.Generic;
+using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
+using Microsoft.Azure.Commands.Common.Authentication.Factories;
+using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Models;
+using Microsoft.Azure.PowerShell.Cmdlets.Ssh.Common;
+using Newtonsoft.Json;
+using Azure.ResourceManager.HybridConnectivity.Models;
+using Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters;
+using Microsoft.Azure.Management.Internal.Resources.Utilities.Models;
+
+
+namespace Microsoft.Azure.Commands.Ssh
+{
+ public abstract class SshBaseCmdlet : AzureRMCmdlet
+ {
+
+ #region Constants
+ private const string clientProxyStorageUrl = "https://sshproxysa.blob.core.windows.net";
+ private const string clientProxyRelease = "release01-11-21";
+ private const string clientProxyVersion = "1.3.017634";
+
+ protected internal const string InteractiveParameterSet = "Interactive";
+ protected internal const string ResourceIdParameterSet = "ResourceId";
+ protected internal const string IpAddressParameterSet = "IpAddress";
+ #endregion
+
+ #region Fields
+ protected internal bool deleteKeys;
+ protected internal bool deleteCert;
+ protected internal string proxyPath;
+ protected internal string relayInfo;
+ protected internal DateTime relayInfoExpiration;
+ #endregion
+
+ #region Properties
+ internal SshAzureUtils AzureUtils
+ {
+ get
+ {
+ if (azureUtils == null)
+ {
+ azureUtils = new SshAzureUtils(DefaultProfile.DefaultContext);
+ }
+
+ return azureUtils;
+ }
+
+ set { azureUtils = value; }
+ }
+ private SshAzureUtils azureUtils;
+
+ internal RMProfileClient ProfileClient
+ {
+ get
+ {
+ if (profileClient == null)
+ {
+ profileClient = new RMProfileClient(DefaultProfile as AzureRmProfile);
+ }
+ return profileClient;
+ }
+
+ set { profileClient = value; }
+ }
+ private RMProfileClient profileClient;
+ #endregion
+
+ #region Parameters
+ ///
+ /// The name of the resource group. The name is case insensitive.
+ ///
+ [Parameter(
+ ParameterSetName = InteractiveParameterSet,
+ Mandatory = true,
+ ValueFromPipelineByPropertyName = true)]
+ [ResourceGroupCompleter]
+ [ValidateNotNullOrEmpty]
+ public string ResourceGroupName { get; set; }
+
+ ///
+ /// The name of the target Azure Virtual Machine or Arc Server.
+ ///
+ [Parameter(
+ ParameterSetName = InteractiveParameterSet,
+ Mandatory = true,
+ ValueFromPipelineByPropertyName = true)]
+ [SshResourceNameCompleter(new string[] { "Microsoft.Compute/virtualMachines", "Microsoft.HybridCompute/machines" }, "ResourceGroupName")]
+ [ValidateNotNullOrEmpty]
+ public string Name { get; set; }
+
+ ///
+ /// The IP address of the target Azure Virtual Machine.
+ ///
+ [Parameter(
+ ParameterSetName = IpAddressParameterSet,
+ Mandatory = true)]
+ [ValidateNotNullOrEmpty]
+ public string Ip { get; set; }
+
+ ///
+ /// Id of the target Azure Virtual Machine or Arc Server.
+ ///
+ [Parameter(
+ ParameterSetName = ResourceIdParameterSet,
+ Mandatory = true,
+ ValueFromPipeline = true,
+ ValueFromPipelineByPropertyName = true)]
+ [ValidateNotNullOrEmpty]
+ [SshResourceIdCompleter(new string[] { "Microsoft.HybridCompute/machines", "Microsoft.Compute/virtualMachines" })]
+ public string ResourceId { get; set; }
+
+ ///
+ /// Path to the generated config file.
+ ///
+ [Parameter(
+ ParameterSetName = InteractiveParameterSet,
+ Mandatory = true)]
+ [Parameter(
+ ParameterSetName = IpAddressParameterSet,
+ Mandatory = true)]
+ [Parameter(
+ ParameterSetName = ResourceIdParameterSet,
+ Mandatory = true)]
+ [ValidateNotNullOrEmpty]
+ public virtual string ConfigFilePath { get; set; }
+
+ ///
+ /// Path to a Public Key File that will be signed by AAD to create certificate for AAD login.
+ /// Work on this.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string PublicKeyFile { get; set; }
+
+ ///
+ /// Path to a Private Key File.
+ /// Can be used for key based authentication when connecting to a LocalUser
+ /// Or it can be used in conjuction with a signed public key for AAD Login
+ /// Work on this as well
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string PrivateKeyFile { get; set; }
+
+ ///
+ /// Gets or sets a flag that uses a Private Ip when connecting to an Azure Virtual Machine.
+ /// User must have a line of sight to the Private Ip.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public SwitchParameter UsePrivateIp { get; set; }
+
+ ///
+ /// Name of a local user in the target machine to connect to.
+ /// To connect using AAD certificates, don't use this argument.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string LocalUser { get; set; }
+
+ ///
+ /// Port number to connect to on the remote target host.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string Port { get; set; }
+
+ ///
+ /// Path to a local folder that contains the OpenSSH executables to be used by this cmdlet.
+ /// Defaults to pre-installed bits. In Windows: C:\Windows\System32\OpenSSH.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string SshClientFolder { get; set; }
+
+ ///
+ /// The Type of the target Azure Resource.
+ /// Either Microsoft.Compute/virtualMachines or Microsoft.HybridCompute/machines.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [PSArgumentCompleter("Microsoft.Compute/virtualMachines", "Microsoft.HybridCompute/machines")]
+ [ValidateNotNullOrEmpty]
+ public string ResourceType { get; set; }
+
+ ///
+ /// Folder to install the SSH Proxy if connecting to an Arc Server.
+ /// Defaults to a .clientsshproxy folder created in the user's directory.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string SshProxyFolder { get; set; }
+
+ ///
+ /// Certificate File for authentication when connecting to a Local User account.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string CertificateFile { get; set; }
+
+ ///
+ /// Additional SSH arguments.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet, ValueFromRemainingArguments = true)]
+ [Parameter(ParameterSetName = IpAddressParameterSet, ValueFromRemainingArguments = true)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet, ValueFromRemainingArguments = true)]
+ [ValidateNotNullOrEmpty]
+ public virtual string[] SshArguments { get; set; }
+
+ ///
+ /// Overwrite existing ConfigFile
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public virtual SwitchParameter Overwrite { get; set; }
+
+ ///
+ /// Folder where generated keys will be saved.
+ ///
+ [Parameter(ParameterSetName = InteractiveParameterSet)]
+ [Parameter(ParameterSetName = IpAddressParameterSet)]
+ [Parameter(ParameterSetName = ResourceIdParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public virtual string KeysDestinationFolder { get; set; }
+
+ [Parameter(Mandatory = false)]
+ public SwitchParameter PassThru { get; set; }
+
+ #endregion
+
+ #region Protected Internal Methods
+
+ protected internal void BeforeExecution()
+ {
+ string a = getProxyPath();
+ switch (ParameterSetName)
+ {
+ case IpAddressParameterSet:
+ ResourceType = "Microsoft.Compute/virtualMachines";
+ break;
+ case ResourceIdParameterSet:
+ ResourceIdentifier parsedId = new ResourceIdentifier(ResourceId);
+ Name = parsedId.ResourceName;
+ ResourceGroupName = parsedId.ResourceGroupName;
+ ResourceType = AzureUtils.DecideResourceType(Name, ResourceGroupName, parsedId.ResourceType);
+ break;
+ case InteractiveParameterSet:
+ ResourceType = AzureUtils.DecideResourceType(Name, ResourceGroupName, ResourceType);
+ break;
+ }
+
+ ValidateParameters();
+ }
+
+ protected internal void UpdateProgressBar(
+ ProgressRecord record,
+ string statusMessage,
+ int percentComplete)
+ {
+ record.PercentComplete = percentComplete;
+ record.StatusDescription = statusMessage;
+ WriteProgress(record);
+ }
+
+ protected internal void GetRelayInformation()
+ {
+ Track2HybridConnectivityManagementClient myclient = new Track2HybridConnectivityManagementClient(AzureSession.Instance.ClientFactory,
+ DefaultProfile.DefaultContext);
+ TargetResourceEndpointAccess responseValue = null;
+ switch (ParameterSetName)
+ {
+ case InteractiveParameterSet:
+ responseValue = myclient.GetRelayInformationString(ResourceGroupName, Name, "default");
+ break;
+ case ResourceIdParameterSet:
+ responseValue = myclient.GetRelayInformationString(ResourceId, "default");
+ break;
+ }
+
+ if (responseValue != null)
+ {
+ relayInfoExpiration = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds((long)responseValue.ExpiresOn), TimeZoneInfo.Local).DateTime;
+ relayInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"relay\":" + JsonConvert.SerializeObject(responseValue) + "}"));
+ }
+ else
+ {
+ throw new AzPSInvalidOperationException("Unable to get Relay Information.");
+ }
+
+ }
+
+ protected internal void GetVmIpAddress()
+ {
+ Ip = AzureUtils.GetFirstPublicIp(Name, ResourceGroupName);
+ if (Ip == null)
+ {
+ string errorMessage = "Couldn't determine the IP address of " + Name + "in the Resource Group " + ResourceGroupName;
+ throw new AzPSResourceNotFoundCloudException(errorMessage);
+ }
+ }
+
+ protected internal void PrepareAadCredentials(string credentialFolder = null)
+ {
+ deleteCert = true;
+ deleteKeys = CheckOrCreatePublicAndPrivateKeyFile(credentialFolder);
+ CertificateFile = GetAndWriteCertificate(PublicKeyFile);
+ LocalUser = GetSSHCertPrincipals(CertificateFile)[0];
+ }
+
+ protected internal string GetCertificateExpirationTimes()
+ {
+ string[] certificateInfo = GetSSHCertInfo(this.CertificateFile);
+ foreach (string line in certificateInfo)
+ {
+ if (line.Contains("Valid:"))
+ {
+ var validity = Regex.Split(line.Trim().Replace("Valid: from ", ""), " to ");
+ DateTime endDate = DateTime.Parse(validity[1]);
+ return endDate.ToString();
+ }
+
+ }
+ return null;
+ }
+
+ protected internal string GetSSHClientPath(string sshCommand)
+ {
+ string sshPath;
+ string commandExecutable = sshCommand;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ commandExecutable += ".exe";
+ }
+ if (SshClientFolder != null)
+ {
+ sshPath = Path.Combine(SshClientFolder, commandExecutable);
+ if (File.Exists(sshPath))
+ {
+ WriteDebug("Attempting to run " + commandExecutable + " from path " + sshPath + ".");
+ return sshPath;
+ }
+ WriteWarning("Could not find " + sshPath + ". " + "Attempting to get pre-installed OpenSSH bits.");
+ }
+
+ sshPath = commandExecutable;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ string systemDir = Environment.SystemDirectory;
+ sshPath = Path.Combine(systemDir, "openSSH", commandExecutable);
+ if (!File.Exists(sshPath))
+ {
+ throw new AzPSFileNotFoundException("Couldn't find " + sshPath + " .\nMake sure OpenSSH is installed correctly: https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse . Or use -SshClientFolder to provide folder path with ssh executables. ", sshPath);
+ }
+ }
+ return sshPath;
+ }
+
+ protected internal string GetClientSideProxy()
+ {
+ string proxyPath = null;
+ string oldProxyPattern = null;
+ string requestUrl = null;
+
+ GetProxyUrlAndFilename(ref proxyPath, ref oldProxyPattern, ref requestUrl);
+
+ if (!File.Exists(proxyPath))
+ {
+ string proxyDir = Path.GetDirectoryName(proxyPath);
+ Console.WriteLine(proxyDir);
+
+ if (!Directory.Exists(proxyDir))
+ {
+ Directory.CreateDirectory(proxyDir);
+ }
+ else
+ {
+ var files = Directory.GetFiles(proxyDir, oldProxyPattern);
+ foreach (string file in files)
+ {
+ try
+ {
+ File.Delete(file);
+ }
+ catch (Exception exception)
+ {
+ WriteWarning("Couldn't delete old version of the Proxy File: " + file + ". Error: " + exception.Message);
+ }
+ }
+ }
+
+ try
+ {
+ WebClient wc = new WebClient();
+ wc.DownloadFile(new Uri(requestUrl), proxyPath);
+ }
+ catch (Exception exception)
+ {
+ string errorMessage = "Failed to download client proxy executable from " + requestUrl + ". Error: " + exception.Message;
+ throw new AzPSApplicationException(errorMessage);
+ }
+ }
+ return proxyPath;
+ }
+
+ protected internal void DeleteFile(string fileName, string warningMessage = null)
+ {
+ if (File.Exists(fileName))
+ {
+ try
+ {
+ File.Delete(fileName);
+ }
+ catch (Exception e)
+ {
+ if (warningMessage != null)
+ {
+ WriteWarning(warningMessage + " Error: " + e.Message);
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+ }
+
+ protected internal void DeleteDirectory(string dirPath, string warningMessage = null)
+ {
+ if (Directory.Exists(dirPath))
+ {
+ try
+ {
+ Directory.Delete(dirPath);
+ }
+ catch (Exception e)
+ {
+ if (warningMessage != null)
+ {
+ WriteWarning(warningMessage + " Error: " + e.Message);
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+ }
+
+ protected internal bool IsArc()
+ {
+ if (ResourceType.Equals("Microsoft.HybridCompute/machines"))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ #endregion
+
+
+ #region Private Methods
+ private void ValidateParameters()
+ {
+ if (CertificateFile != null && LocalUser != null)
+ {
+ WriteWarning("To authenticate with a cartificate you must provide a LocalUser. The certificate will be ignored.");
+ }
+
+ if (PrivateKeyFile != null && !File.Exists(PrivateKeyFile))
+ {
+ throw new AzPSArgumentException("Provided private key file doesn't exist.", nameof(PrivateKeyFile));
+ }
+
+ if (PublicKeyFile != null && !File.Exists(PublicKeyFile))
+ {
+ throw new AzPSArgumentException("Provided private key file doesn't exist.", nameof(PublicKeyFile));
+ }
+
+ if (CertificateFile != null && !File.Exists(CertificateFile))
+ {
+ throw new AzPSArgumentException("Provided private key file doesn't exist.", nameof(CertificateFile));
+ }
+
+ if (ConfigFilePath != null)
+ {
+ string configFolder = Path.GetDirectoryName(ConfigFilePath);
+ if (!Directory.Exists(configFolder))
+ {
+ throw new AzPSArgumentException("Config file destination folder " + configFolder + " does not exist.", nameof(ConfigFilePath));
+ }
+ }
+
+ if (KeysDestinationFolder != null && (PrivateKeyFile != null || PublicKeyFile != null))
+ {
+ throw new AzPSArgumentException("KeysDestinationFolder can't be used in conjunction with PublicKeyFile or PrivateKeyFile. All generated keys are saved in the same directory as provided keys.", nameof(KeysDestinationFolder));
+ }
+
+ if (!ResourceType.Equals("Microsoft.HybridCompute/machines") && SshProxyFolder != null)
+ {
+ WriteWarning("-SshProxyFolder is only used when connecting to Arc Servers. This argument will be ignored.");
+ }
+ }
+
+ private string GetAndWriteCertificate(string publicKeyFile)
+ {
+ SshCredential certificate = GetAccessToken(publicKeyFile);
+ string token = certificate.Credential;
+ string keyDir = Path.GetDirectoryName(publicKeyFile);
+ string certpath = Path.Combine(keyDir, "id_rsa.pub-aadcert.pub");
+ string cert_contents = "ssh-rsa-cert-v01@openssh.com " + token;
+
+ File.WriteAllText(certpath, cert_contents);
+
+ return certpath;
+ }
+
+ private SshCredential GetAccessToken(string publicKeyFile)
+ {
+ string publicKeyText = File.ReadAllText(publicKeyFile);
+ RSAParser parser = new RSAParser(publicKeyText);
+ var context = DefaultProfile.DefaultContext;
+ RSAParameters parameters = new RSAParameters
+ {
+ Exponent = Base64UrlHelper.DecodeToBytes(parser.Exponent),
+ Modulus = Base64UrlHelper.DecodeToBytes(parser.Modulus)
+ };
+ ISshCredentialFactory factory = new SshCredentialFactory();
+ AzureSession.Instance.TryGetComponent(nameof(ISshCredentialFactory), out factory);
+ var token = factory.GetSshCredential(context, parameters);
+ return token;
+ }
+
+ private List GetSSHCertPrincipals(string certFile)
+ {
+ string[] certInfo = GetSSHCertInfo(certFile);
+ List principals = new List();
+ bool inPrincipals = false;
+
+ foreach (string line in certInfo)
+ {
+ if (line.Contains(":"))
+ {
+ inPrincipals = false;
+ }
+ if (line.Contains("Principals: "))
+ {
+ inPrincipals = true;
+ continue;
+ }
+ if (inPrincipals)
+ {
+ principals.Add(line.Trim());
+ }
+ }
+ return principals;
+ }
+
+ private string[] GetSSHCertInfo(string certFile)
+ {
+ string sshKeygenPath = GetSSHClientPath("ssh-keygen");
+ string args = "-L -f " + certFile;
+ WriteDebug("Runnung ssh-keygen command: " + sshKeygenPath + " " + args);
+ Process keygen = new Process();
+ keygen.StartInfo.FileName = sshKeygenPath;
+ keygen.StartInfo.Arguments = args;
+ keygen.StartInfo.UseShellExecute = false;
+ keygen.StartInfo.RedirectStandardOutput = true;
+ keygen.Start();
+ string output = keygen.StandardOutput.ReadToEnd();
+ keygen.WaitForExit();
+
+ string[] certInfo = output.Split('\n');
+
+ return certInfo;
+ }
+
+ private bool CheckOrCreatePublicAndPrivateKeyFile(string credentialFolder=null)
+ {
+ bool deleteKeys = false;
+ if (PublicKeyFile == null && PrivateKeyFile == null)
+ {
+ deleteKeys = true;
+ if (credentialFolder == null)
+ {
+ credentialFolder = CreateTempFolder();
+ }
+ else
+ {
+ //create all directories in the path unless they already exist
+ Directory.CreateDirectory(credentialFolder);
+ }
+
+ PublicKeyFile = Path.Combine(credentialFolder, "id_rsa.pub");
+ PrivateKeyFile = Path.Combine(credentialFolder, "id_rsa");
+ CreateSSHKeyfile(PrivateKeyFile);
+ }
+
+ if (PublicKeyFile == null)
+ {
+ if (PrivateKeyFile != null)
+ {
+ PublicKeyFile = PrivateKeyFile + ".pub";
+ }
+ else
+ {
+ throw new AzPSArgumentNullException("Public key file not specified.", "PublicKeyFile");
+ }
+ }
+
+ if (!File.Exists(PublicKeyFile))
+ {
+ throw new AzPSFileNotFoundException("Public key file not found", PublicKeyFile);
+ }
+
+ // The private key is not required as the user may be using a keypair stored in ssh-agent
+ if (PrivateKeyFile != null && !File.Exists(PrivateKeyFile))
+ {
+ throw new AzPSFileNotFoundException("Private key file not found", PrivateKeyFile);
+ }
+
+ return deleteKeys;
+ }
+
+ private void CreateSSHKeyfile(string privateKeyFile)
+ {
+ string args = "-f " + privateKeyFile + " -t rsa -q -N \"\"";
+ Process keygen = Process.Start(GetSSHClientPath("ssh-keygen"), args);
+ keygen.WaitForExit();
+ }
+
+
+ private string CreateTempFolder()
+ {
+ //mix in some numbers as well?
+ //worry about the length of the name?
+ //should we be using get random filename
+ string prefix = "aadsshcert";
+ var dirnameBuilder = new StringBuilder();
+ Random random = new Random();
+ string dirname;
+ do
+ {
+ dirnameBuilder.Clear();
+ dirnameBuilder.Append(prefix);
+ for (int i = 0; i < 8; i++)
+ {
+ char randChar = (char)random.Next('a', 'a' + 26);
+ dirnameBuilder.Append(randChar);
+ }
+ dirname = Path.Combine(Path.GetTempPath(), dirnameBuilder.ToString());
+ } while (Directory.Exists(dirname));
+
+ Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), dirnameBuilder.ToString()));
+
+ return dirname;
+ }
+
+ private string getProxyPath()
+ {
+ string os;
+ string architecture;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ os = "windows";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ os = "linux";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ os = "darwin";
+ }
+ else
+ {
+ throw new AzPSApplicationException("Operating System not supported.");
+ }
+
+ if (Environment.Is64BitProcess)
+ {
+ architecture = "amd64";
+ }
+ else
+ {
+ architecture = "386";
+ }
+
+ string proxyName = "sshProxy_" + os + "_" + architecture;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ proxyName = proxyName + ".exe";
+ }
+
+ string assemblyPath = this.GetType().Assembly.Location;
+ string assemblyDirectory = Path.GetDirectoryName(assemblyPath);
+ string proxyPath = Path.Combine(assemblyDirectory, proxyName);
+
+ if (File.Exists(proxyPath))
+ {
+ return proxyPath;
+ }
+ throw new AzPSApplicationException("Unable to find proxy. Reinstall extension?");
+
+ //Check for a hash to make sure the assembly is for real before returning
+ }
+
+ private void GetProxyUrlAndFilename(
+ ref string proxyPath,
+ ref string oldProxyPattern,
+ ref string requestUrl)
+ {
+ string os;
+ string architecture;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ os = "windows";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ os = "linux";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ os = "darwin";
+ }
+ else
+ {
+ throw new AzPSApplicationException("Operating System not supported.");
+ }
+
+ if (Environment.Is64BitProcess)
+ {
+ architecture = "amd64";
+ }
+ else
+ {
+ architecture = "386";
+ }
+
+ string proxyName = "sshProxy_" + os + "_" + architecture;
+ requestUrl = clientProxyStorageUrl + "/" + clientProxyRelease + "/" + proxyName + "_" + clientProxyVersion;
+
+ string installPath = proxyName + "_" + clientProxyVersion.Replace('.', '_');
+ oldProxyPattern = proxyName + "*";
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ requestUrl = requestUrl + ".exe";
+ installPath = installPath + ".exe";
+ oldProxyPattern = oldProxyPattern + ".exe";
+ }
+ if (SshProxyFolder == null)
+ {
+ proxyPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Path.Combine(".clientsshproxy", installPath));
+ }
+ else
+ {
+ proxyPath = Path.Combine(SshProxyFolder, installPath);
+ }
+
+ }
+ #endregion
+
+ }
+
+}
diff --git a/src/Ssh/Ssh/Common/SshResourceIdCompleterAttribute.cs b/src/Ssh/Ssh/Common/SshResourceIdCompleterAttribute.cs
new file mode 100644
index 000000000000..96e7ed376aa2
--- /dev/null
+++ b/src/Ssh/Ssh/Common/SshResourceIdCompleterAttribute.cs
@@ -0,0 +1,25 @@
+using System.Management.Automation;
+
+namespace Microsoft.Azure.PowerShell.Cmdlets.Ssh.Common
+{
+ internal class SshResourceIdCompleterAttribute : ArgumentCompleterAttribute
+ {
+ public SshResourceIdCompleterAttribute(string [] resourceTypes) : base(CreateScriptBlock(resourceTypes))
+ {
+ }
+
+ public static ScriptBlock CreateScriptBlock(string [] resourceTypes)
+ {
+ string script = "param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)\n";
+ script += "$resourceIds = @()\n";
+ foreach (var resourceType in resourceTypes)
+ {
+ script += $"$resourceType = \"{resourceType}\"\n" +
+ "$resourceIds = $resourceIds + [Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters.ResourceIdCompleterAttribute]::GetResourceIds($resourceType)\n";
+ }
+ script += "$resourceIds | Where-Object { $_ -Like \"*$wordToComplete*\" } | Sort-Object | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }";
+ var scriptBlock = ScriptBlock.Create(script);
+ return scriptBlock;
+ }
+ }
+}
diff --git a/src/Ssh/Ssh/Common/SshResourceNameCompleterAttribute.cs b/src/Ssh/Ssh/Common/SshResourceNameCompleterAttribute.cs
new file mode 100644
index 000000000000..7042e0d75082
--- /dev/null
+++ b/src/Ssh/Ssh/Common/SshResourceNameCompleterAttribute.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Management.Automation;
+
+namespace Microsoft.Azure.PowerShell.Cmdlets.Ssh.Common
+{
+ internal class SshResourceNameCompleterAttribute : ArgumentCompleterAttribute
+ {
+ public SshResourceNameCompleterAttribute(string [] resourceTypes, params string [] parentResourceParameterNames): base(CreateScriptBlock(resourceTypes, parentResourceParameterNames))
+ {
+ }
+
+ public static ScriptBlock CreateScriptBlock(string [] resourceTypes, string [] parentResourceNames)
+ {
+ string script = "param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)\n" +
+ "$parentResources = @()\n";
+ foreach (var parentResourceName in parentResourceNames)
+ {
+ script += String.Format("$parentResources += $fakeBoundParameter[\"{0}\"]\n", parentResourceName);
+ }
+ script += "$resources = @()\n";
+ foreach (var resourceType in resourceTypes)
+ {
+ script += String.Format("$resourceType = \"{0}\"\n", resourceType) +
+ "$resources = $resources + [Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters.ResourceNameCompleterAttribute]::FindResources($resourceType, $parentResources)\n";
+ }
+ script += "$resources | Where-Object { $_ -Like \"$wordToComplete*\" } | Sort-Object | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }";
+ ScriptBlock scriptBlock = ScriptBlock.Create(script);
+ return scriptBlock;
+ }
+ }
+}
diff --git a/src/Ssh/Ssh/Common/Track2HybridConnectivityManagementClient.cs b/src/Ssh/Ssh/Common/Track2HybridConnectivityManagementClient.cs
new file mode 100644
index 000000000000..329d3b6d3733
--- /dev/null
+++ b/src/Ssh/Ssh/Common/Track2HybridConnectivityManagementClient.cs
@@ -0,0 +1,64 @@
+using Azure.ResourceManager.HybridConnectivity;
+using Azure.ResourceManager.HybridConnectivity.Models;
+using Azure.ResourceManager;
+using Azure;
+using Azure.ResourceManager.Resources;
+using Microsoft.Azure.Commands.Common.Authentication;
+using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
+using Microsoft.Azure.Management.Internal.Resources.Utilities.Models;
+
+namespace Microsoft.Azure.PowerShell.Cmdlets.Ssh.Common
+{
+ internal class Track2HybridConnectivityManagementClient
+ {
+ private ArmClient _armClient;
+ private string _subscription;
+ private IClientFactory _clientFactory;
+
+ public Track2HybridConnectivityManagementClient(IClientFactory clientFactory, IAzureContext context)
+ {
+ _clientFactory = clientFactory;
+ _armClient = _clientFactory.CreateArmClient(context, AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId);
+ _subscription = context.Subscription.Id;
+ }
+
+ private ResourceGroupResource GetResourceGroup(string resourceGroupName) =>
+ _armClient.GetResourceGroupResource(ResourceGroupResource.CreateResourceIdentifier(_subscription, resourceGroupName));
+
+ private EndpointResource CreateEndpoint(string resourceGroup, EndpointResourceData data)
+ {
+ ResourceGroupResource rg = GetResourceGroup(resourceGroup);
+ EndpointResourceCollection endpresources = rg.GetEndpointResources();
+ EndpointResource a = endpresources.CreateOrUpdate(WaitUntil.Completed, "default", data).WaitForCompletion();
+ return a;
+ }
+
+ public TargetResourceEndpointAccess GetRelayInformationString(string resourceGroup, string resourceName, string endpoint)
+ {
+ return GetRelayInformationString($"/subscriptions/{_subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.HybridCompute/machines/{resourceName}", endpoint);
+ }
+
+ public TargetResourceEndpointAccess GetRelayInformationString(string ResourceId, string endpoint)
+ {
+
+ EndpointResource myEndpoint = _armClient.GetEndpointResource(EndpointResource.CreateResourceIdentifier(ResourceId, endpoint));
+ try
+ {
+ var myCred = myEndpoint.GetCredentials(3600);
+ return myCred.Value;
+ }
+ catch (RequestFailedException)
+ {
+ EndpointResourceData data = new EndpointResourceData();
+ data.EndpointType = EndpointType.Default;
+ //data.ResourceId = EndpointResource.CreateResourceIdentifier(ResourceId, endpoint);
+ data.ResourceId = ResourceId;
+ ResourceIdentifier parsedId = new ResourceIdentifier(ResourceId);
+ CreateEndpoint(parsedId.ResourceGroupName, data);
+ var myCred = myEndpoint.GetCredentials(3600);
+ return myCred.Value;
+ }
+ }
+
+ }
+}
diff --git a/src/Ssh/Ssh/Models/PSSshConfigEntry.cs b/src/Ssh/Ssh/Models/PSSshConfigEntry.cs
new file mode 100644
index 000000000000..0b7249f2f346
--- /dev/null
+++ b/src/Ssh/Ssh/Models/PSSshConfigEntry.cs
@@ -0,0 +1,141 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System.Text;
+using System.Collections.Generic;
+
+namespace Microsoft.Azure.Commands.Ssh.Models
+{
+ ///
+ /// Class that represents an Ssh Configuration file to connect to Azure Resources.
+ ///
+ internal class PSSshConfigEntry
+ {
+ ///
+ /// Alias of the host in the config file.
+ /// If we know Resource Name and Resource Group, host will be "{rg}-{name}"
+ /// If we only know the Ip, host will be "{Ip}"
+ ///
+ public string Host { get; set; }
+
+ ///
+ /// Actual host name. Ip address for Azure VMs and resource name for arc servers.
+ ///
+ public string HostName { get; set; }
+
+ ///
+ /// Username.
+ ///
+ public string User { get; set; }
+
+ ///
+ /// Path to certificate file.
+ ///
+ public string CertificateFile { get; set; }
+
+ ///
+ /// Path to private key file.
+ ///
+ public string IdentityFile { get; set; }
+
+ ///
+ /// Microsoft.HybridCompute/machines or Microsoft.Compute/virtualMachines.
+ ///
+ public string ResourceType { get; set; }
+
+ ///
+ /// Command to connect to host via Arc Connectivity Proxy.
+ ///
+ public string ProxyCommand { get; set; }
+
+ ///
+ /// Ssh Port.
+ ///
+ public string Port { get; set; }
+
+ ///
+ /// Either AAD or LocalUser
+ ///
+ public string LoginType { get; set; }
+
+ ///
+ /// String built from the value of this object's properties.
+ /// Note 1: If LocalUser login, "-{localusername}" is appended to host.
+ /// Note 2: Azure VMs have two entries if resource name and group are known.
+ /// One with {rg}-{name} as host, and one with {ip} as host.
+ ///
+ public string ConfigString
+ {
+ get
+ {
+ StringBuilder builder = new StringBuilder();
+ if (LoginType.Equals("AAD")) { builder.AppendLine($"Host {this.Host}"); }
+ else { builder.AppendLine($"Host {this.Host}-{this.User}"); }
+
+ if (!HostName.Equals(Host))
+ {
+ builder.AppendLine($"\tHostName {this.HostName}");
+ }
+ builder.AppendLine(string.Format("\tUser {0}", this.User));
+ this.AppendKeyValuePairToStringBuilderIfNotValueNull("CertificateFile", this.CertificateFile, builder);
+ this.AppendKeyValuePairToStringBuilderIfNotValueNull("IdentityFile", this.IdentityFile, builder);
+ this.AppendKeyValuePairToStringBuilderIfNotValueNull("Port", this.Port, builder);
+ this.AppendKeyValuePairToStringBuilderIfNotValueNull("ProxyCommand", this.ProxyCommand, builder);
+
+ if (ResourceType.Equals("Microsoft.Compute/virtualMachines") && !HostName.Equals(Host))
+ {
+ if (LoginType.Equals("AAD")) { builder.AppendLine($"\nHost {this.HostName}"); }
+ else { builder.AppendLine($"\nHost {this.HostName}-{this.User}"); }
+
+ builder.AppendLine(string.Format("\tUser {0}", this.User));
+ this.AppendKeyValuePairToStringBuilderIfNotValueNull("CertificateFile", this.CertificateFile, builder);
+ this.AppendKeyValuePairToStringBuilderIfNotValueNull("IdentityFile", this.IdentityFile, builder);
+ this.AppendKeyValuePairToStringBuilderIfNotValueNull("Port", this.Port, builder);
+ }
+
+ return builder.ToString();
+ }
+ }
+
+ public PSSshConfigEntry(Dictionary configEntry)
+ {
+ this.Host = GetPropertyValueFromConfigDictionary(configEntry, "Host");
+ this.HostName = GetPropertyValueFromConfigDictionary(configEntry, "HostName");
+ this.ProxyCommand = GetPropertyValueFromConfigDictionary(configEntry, "ProxyCommand");
+ this.Port = GetPropertyValueFromConfigDictionary(configEntry, "Port");
+ this.User = GetPropertyValueFromConfigDictionary(configEntry, "User");
+ this.IdentityFile = GetPropertyValueFromConfigDictionary(configEntry, "IdentityFile");
+ this.CertificateFile = GetPropertyValueFromConfigDictionary(configEntry, "CertificateFile");
+ this.ResourceType = GetPropertyValueFromConfigDictionary(configEntry, "ResourceType");
+ this.LoginType = GetPropertyValueFromConfigDictionary(configEntry, "LoginType");
+ }
+
+ private string GetPropertyValueFromConfigDictionary(Dictionary configEntry, string KeyName)
+ {
+ if (configEntry.ContainsKey(KeyName))
+ {
+ return configEntry[KeyName];
+ }
+ return null;
+ }
+
+ private void AppendKeyValuePairToStringBuilderIfNotValueNull(string key, string value, StringBuilder builder)
+ {
+ if (value != null)
+ {
+ builder.AppendLine($"\t{key} {value}");
+ }
+ }
+ }
+}
diff --git a/src/Ssh/Ssh/Properties/AssemblyInfo.cs b/src/Ssh/Ssh/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000000..223eab3734c5
--- /dev/null
+++ b/src/Ssh/Ssh/Properties/AssemblyInfo.cs
@@ -0,0 +1,29 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("Microsoft Azure Powershell - SSH")]
+[assembly: AssemblyCompany(Microsoft.WindowsAzure.Commands.Common.AzurePowerShell.AssemblyCompany)]
+[assembly: AssemblyProduct(Microsoft.WindowsAzure.Commands.Common.AzurePowerShell.AssemblyProduct)]
+[assembly: AssemblyCopyright(Microsoft.WindowsAzure.Commands.Common.AzurePowerShell.AssemblyCopyright)]
+
+[assembly: ComVisible(false)]
+[assembly: CLSCompliant(false)]
+[assembly: Guid("d90791a2-8102-47fc-8150-de25ae796eb1")]
+[assembly: AssemblyVersion("0.0.1")]
+[assembly: AssemblyFileVersion("0.0.1")]
diff --git a/src/Ssh/Ssh/Properties/Resources.Designer.cs b/src/Ssh/Ssh/Properties/Resources.Designer.cs
new file mode 100644
index 000000000000..ea3ebba3b740
--- /dev/null
+++ b/src/Ssh/Ssh/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Azure.PowerShell.Cmdlets.Ssh.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Azure.PowerShell.Cmdlets.Ssh.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/src/Ssh/Ssh/Properties/Resources.resx b/src/Ssh/Ssh/Properties/Resources.resx
new file mode 100644
index 000000000000..1af7de150c99
--- /dev/null
+++ b/src/Ssh/Ssh/Properties/Resources.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Ssh/Ssh/Ssh.csproj b/src/Ssh/Ssh/Ssh.csproj
new file mode 100644
index 000000000000..c0196a6b6861
--- /dev/null
+++ b/src/Ssh/Ssh/Ssh.csproj
@@ -0,0 +1,62 @@
+
+
+
+ Ssh
+
+
+
+
+ Microsoft.Azure.Commands.Ssh.AlcWrapper
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ runtime
+
+
+
+
+
+ ..\..\..\..\azure-sdk-for-net\artifacts\bin\Azure.ResourceManager.HybridConnectivity\Release\netstandard2.0\Azure.ResourceManager.HybridConnectivity.dll
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ssh/Ssh/SshCommands/EnterAzVMCommand.cs b/src/Ssh/Ssh/SshCommands/EnterAzVMCommand.cs
new file mode 100644
index 000000000000..e36d4cd4868d
--- /dev/null
+++ b/src/Ssh/Ssh/SshCommands/EnterAzVMCommand.cs
@@ -0,0 +1,233 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+using System.Management.Automation;
+using Microsoft.Azure.Commands.Common.Exceptions;
+using System.Diagnostics;
+using System.Collections.Generic;
+
+
+namespace Microsoft.Azure.Commands.Ssh
+{
+ [Cmdlet(
+ VerbsCommon.Enter,
+ ResourceManager.Common.AzureRMConstants.AzureRMPrefix + "VM",
+ DefaultParameterSetName = InteractiveParameterSet)]
+ [OutputType(typeof(bool))]
+ [Alias("Enter-AzArcServer")]
+ public sealed class EnterAzVMCommand : SshBaseCmdlet
+ {
+ #region Supress Export-AzSshConfig Parameters
+
+ public override string ConfigFilePath
+ {
+ get { return null; }
+ }
+ public override SwitchParameter Overwrite
+ {
+ get { return false; }
+ }
+
+ public override string KeysDestinationFolder
+ {
+ get { return null; }
+ }
+
+ #endregion
+
+ public override void ExecuteCmdlet()
+ {
+ base.ExecuteCmdlet();
+
+ BeforeExecution();
+
+ ProgressRecord record = new ProgressRecord(0, "Prepare for starting SSH connection", "Start Preparing");
+ UpdateProgressBar(record, "Start preparing", 0);
+
+ if (!IsArc() && !ParameterSetName.Equals(IpAddressParameterSet))
+ {
+ GetVmIpAddress();
+ UpdateProgressBar(record, "Retrieved the IP address of the target VM", 50);
+ }
+ if (IsArc())
+ {
+ proxyPath = GetClientSideProxy();
+ UpdateProgressBar(record, "Dowloaded SSH Proxy, saved to " + proxyPath, 25);
+ GetRelayInformation();
+ UpdateProgressBar(record, "Retrieved Relay Information" + proxyPath, 50);
+ }
+ try
+ {
+ if (LocalUser == null)
+ {
+ PrepareAadCredentials();
+ }
+
+ record.RecordType = ProgressRecordType.Completed;
+ UpdateProgressBar(record, "Ready to start SSH connection.", 100);
+
+ int sshStatus = StartSSHConnection();
+ if (this.PassThru.IsPresent)
+ {
+ WriteObject(sshStatus == 0);
+ }
+ }
+ finally
+ {
+ DoCleanup();
+ }
+ }
+
+ #region Private Methods
+
+ private bool IsDebugMode()
+ {
+ bool debug;
+ bool containsDebug = MyInvocation.BoundParameters.ContainsKey("Debug");
+ if (containsDebug)
+ debug = ((SwitchParameter)MyInvocation.BoundParameters["Debug"]).ToBool();
+ else
+ debug = (ActionPreference)GetVariableValue("DebugPreference") != ActionPreference.SilentlyContinue;
+ return debug;
+ }
+
+ private int StartSSHConnection()
+ {
+ string sshClient = GetSSHClientPath("ssh");
+ // Process is not accepting ArgumentList instead of Arguments. .NET framework too old?
+ string command = GetHost() + " " + BuildArgs();
+
+ Process sshProcess = new Process();
+ WriteDebug("Running SSH command: " + sshClient + " " + command);
+
+ if (IsArc())
+ sshProcess.StartInfo.EnvironmentVariables["SSHPROXY_RELAY_INFO"] = relayInfo;
+ sshProcess.StartInfo.FileName = sshClient;
+ sshProcess.StartInfo.Arguments = command;
+ sshProcess.StartInfo.RedirectStandardError = true;
+ sshProcess.StartInfo.UseShellExecute = false;
+ sshProcess.Start();
+
+ bool writeLogs = false;
+ var stderr = sshProcess.StandardError;
+
+ if (SshArguments != null &&
+ (Array.Exists(SshArguments, x => x == "-v") ||
+ Array.Exists(SshArguments, x => x == "-vv") ||
+ Array.Exists(SshArguments, x => x == "-vvv")) ||
+ IsDebugMode())
+ {
+ writeLogs = true;
+ }
+
+ string line;
+ while ((line = stderr.ReadLine()) != null)
+ {
+ if (writeLogs || (!line.Contains("debug1: ") && !line.Contains("debug2: ") && !line.Contains("debug3: ")))
+ {
+ // We have the option of filtering out some of the logs that we don't want printed here.
+ // Console.Error.WriteLine(line);
+ // Logs are written to StdErr on OpenSSH
+ Host.UI.WriteLine(line);
+ }
+ if (line.Contains("debug1: Entering interactive session."))
+ {
+ DoCleanup();
+ }
+ //check for well known errors: azcmagent config not set to listen to port 22, OpenSSH too old error
+ }
+
+ sshProcess.WaitForExit();
+
+ return sshProcess.ExitCode;
+ }
+
+ private string GetHost()
+ {
+ if (ResourceType == "Microsoft.HybridCompute/machines" && LocalUser != null && Name != null)
+ {
+ return LocalUser + "@" + Name;
+ } else if (ResourceType == "Microsoft.Compute/virtualMachines" && LocalUser != null && Ip != null)
+ {
+ return LocalUser + "@" + Ip;
+ }
+ throw new AzPSInvalidOperationException("Unable to determine target host.");
+ }
+
+
+ private string BuildArgs()
+ {
+ List argList = new List();
+
+ if (PrivateKeyFile != null) { argList.Add("-i \"" + PrivateKeyFile + "\""); }
+
+ if (CertificateFile != null) { argList.Add("-o CertificateFile=\"" + CertificateFile + "\""); }
+
+ if (IsArc())
+ {
+ string pcommand = "ProxyCommand=\"" + proxyPath + "\"";
+ if (Port != null)
+ {
+ pcommand = "ProxyCommand=\"" + proxyPath + " -p " + Port + "\"";
+ }
+ argList.Add("-o " + pcommand);
+ } else if (Port != null)
+ {
+ argList.Add("-p " + Port);
+ }
+
+ if (SshArguments == null ||
+ (!Array.Exists(SshArguments, x => x == "-v") &&
+ !Array.Exists(SshArguments, x => x == "-vv") &&
+ !Array.Exists(SshArguments, x => x == "-vvv")))
+ {
+ if (IsDebugMode())
+ argList.Add("-vvv");
+ else
+ argList.Add("-v");
+ }
+
+ if (SshArguments != null)
+ {
+ Array.ForEach(SshArguments, item => argList.Add(item));
+ }
+
+ return string.Join(" ", argList.ToArray());
+ }
+
+ private void DoCleanup()
+ {
+ if (deleteKeys && PrivateKeyFile != null)
+ {
+ DeleteFile(PrivateKeyFile, "Couldn't delete Private Key file " + PrivateKeyFile + ".");
+ }
+ if (deleteKeys && PublicKeyFile != null)
+ {
+ DeleteFile(PublicKeyFile, "Couldn't delete Public Key file " + PublicKeyFile + ".");
+ }
+ if (deleteCert && CertificateFile != null)
+ {
+ DeleteFile(CertificateFile, "Couldn't delete Certificate File " + CertificateFile + ".");
+ }
+ if (deleteKeys)
+ {
+ DeleteDirectory(Directory.GetParent(CertificateFile).ToString());
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Ssh/Ssh/SshCommands/ExportAzSshConfig.cs b/src/Ssh/Ssh/SshCommands/ExportAzSshConfig.cs
new file mode 100644
index 000000000000..0d19ed39b0de
--- /dev/null
+++ b/src/Ssh/Ssh/SshCommands/ExportAzSshConfig.cs
@@ -0,0 +1,158 @@
+using System.IO;
+using Microsoft.Azure.Commands.Ssh.Models;
+using System.Management.Automation;
+using Microsoft.Azure.Commands.Common.Exceptions;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using System.Collections.Generic;
+
+namespace Microsoft.Azure.Commands.Ssh
+{
+ [Cmdlet("Export",
+ ResourceManager.Common.AzureRMConstants.AzureRMPrefix + "SshConfig")]
+ //[OutputType(typeof(PSSshConfigEntry))]
+ [OutputType(typeof(bool))]
+ public sealed class ExportAzSshConfig : SshBaseCmdlet
+ {
+ #region Supress Enter-AzVM Parameters
+ public override string[] SshArguments
+ {
+ get
+ {
+ return null;
+ }
+ }
+ #endregion
+
+ #region Properties
+ internal string RelayInfoPath { get; set; }
+ #endregion
+
+ public override void ExecuteCmdlet()
+ {
+ base.ExecuteCmdlet();
+
+ BeforeExecution();
+
+ ConfigFilePath = Path.GetFullPath(ConfigFilePath);
+
+ ProgressRecord record = new ProgressRecord(0, "Creating SSH Config", "Start Preparing");
+ UpdateProgressBar(record, "Start Preparing", 0);
+
+ if (!IsArc() && !ParameterSetName.Equals(IpAddressParameterSet))
+ {
+ GetVmIpAddress();
+ UpdateProgressBar(record, "Retrieved target IP address", 50);
+ }
+ if (IsArc())
+ {
+ proxyPath = GetClientSideProxy();
+ UpdateProgressBar(record, "Downloaded proxy to " + proxyPath, 25);
+ GetRelayInformation();
+ UpdateProgressBar(record, "Completed retrieving relay information", 50);
+ CreateRelayInfoFile();
+ UpdateProgressBar(record, "Created file containing relay information", 65);
+ }
+ if (LocalUser == null)
+ {
+ PrepareAadCredentials(GetKeysDestinationFolder());
+ UpdateProgressBar(record, "Generated Certificate File", 90);
+ // This is not exactly a warning. But I couldn't make WriteInformation or WriteObject work.
+ WriteWarning($"Generated AAD Certificate {CertificateFile} is valid until {GetCertificateExpirationTimes()} in local time.");
+ }
+
+ PSSshConfigEntry entry =
+ new PSSshConfigEntry(CreateConfigDict());
+
+ StreamWriter configSW = new StreamWriter(ConfigFilePath, !Overwrite);
+ configSW.WriteLine(entry.ConfigString);
+ configSW.Close();
+
+ record.RecordType = ProgressRecordType.Completed;
+ UpdateProgressBar(record, "Successfully wrote config file", 100);
+ }
+
+ #region Private Methods
+
+ private Dictionary CreateConfigDict()
+ {
+ Dictionary Config = new Dictionary();
+ if (ResourceGroupName != null && Name != null) { Config.Add("Host", $"{ResourceGroupName}-{Name}"); }
+ else { Config.Add("Host", Ip); }
+
+ if (IsArc())
+ {
+ Config.Add("HostName", Name);
+ Config.Add("ProxyCommand", $"\"{proxyPath}\" -r \"{RelayInfoPath}\"");
+ if (Port != null)
+ Config["ProxyCommand"] = Config["ProxyCommand"] + $" -p {Port}";
+ }
+ else
+ {
+ Config.Add("Port", Port);
+ if (Ip != null) { Config.Add("HostName", Ip); }
+ else { Config.Add("HostName", "*"); }
+ }
+
+ Config.Add("User", LocalUser);
+ Config.Add("CertificateFile", CertificateFile);
+ Config.Add("IdentityFile", PrivateKeyFile);
+ Config.Add("ResourceType", ResourceType);
+
+ if (deleteCert) { Config.Add("LoginType", "AAD"); }
+ else { Config.Add("LoginType", "LocalUser"); }
+
+ return Config;
+ }
+
+ private void CreateRelayInfoFile()
+ {
+ string relayInfoDir = GetKeysDestinationFolder();
+ Directory.CreateDirectory(relayInfoDir);
+
+ string relayInfoFilename = ResourceGroupName + "-" + Name + "-relay_info";
+ RelayInfoPath = Path.Combine(relayInfoDir, relayInfoFilename);
+ DeleteFile(RelayInfoPath);
+ StreamWriter relaySW = new StreamWriter(RelayInfoPath);
+ relaySW.WriteLine(relayInfo);
+ relaySW.Close();
+
+ // This is not exactly a warning. But I couldn't make WriteInformation or WriteObject work.
+ WriteWarning($"Generated relay information file {RelayInfoPath} is valid until {relayInfoExpiration} in local time.");
+ }
+
+ private string GetKeysDestinationFolder()
+ {
+ if (KeysDestinationFolder == null)
+ {
+ string configFolder = Path.GetDirectoryName(ConfigFilePath);
+ string keysFolderName = Ip;
+ if (ResourceGroupName != null && Name != null)
+ {
+ keysFolderName = ResourceGroupName + "-" + Name;
+ }
+
+ if (keysFolderName.Equals("*"))
+ {
+ // If the user provides -Ip *, that would not be a valid name for Windows. Treat it as a special case.
+ keysFolderName = "all_ips";
+ }
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ //Make sure that the folder name doesn't have illegal characters
+ string regexString = "[" + Regex.Escape(new string(Path.GetInvalidFileNameChars())) + "]";
+ Regex containsInvalidCharacter = new Regex(regexString);
+ if (containsInvalidCharacter.IsMatch(keysFolderName))
+ {
+ throw new AzPSInvalidOperationException("Unable to create default keys destination folder. Resource contain invalid characters. Please provide -KeysDestinationFolder.");
+ }
+ }
+
+ return Path.Combine(configFolder, "az_ssh_config", keysFolderName);
+ }
+ return KeysDestinationFolder;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Ssh/Ssh/StartupScripts/sample.ps1 b/src/Ssh/Ssh/StartupScripts/sample.ps1
new file mode 100644
index 000000000000..50ac77403ab6
--- /dev/null
+++ b/src/Ssh/Ssh/StartupScripts/sample.ps1
@@ -0,0 +1 @@
+#Placeholder for future scripts: Please delete this file and uncomment Always within the block in the .csproj file.
\ No newline at end of file
diff --git a/src/Ssh/documentation/current-breaking-changes.md b/src/Ssh/documentation/current-breaking-changes.md
new file mode 100644
index 000000000000..d38f925e6892
--- /dev/null
+++ b/src/Ssh/documentation/current-breaking-changes.md
@@ -0,0 +1,39 @@
+
\ No newline at end of file
diff --git a/src/Ssh/documentation/upcoming-breaking-changes.md b/src/Ssh/documentation/upcoming-breaking-changes.md
new file mode 100644
index 000000000000..0e389879299c
--- /dev/null
+++ b/src/Ssh/documentation/upcoming-breaking-changes.md
@@ -0,0 +1,26 @@
+
\ No newline at end of file