Public/Connect-DattoApi.ps1
|
function Connect-DattoApi { <# .SYNOPSIS Creates an authenticated Datto REST API connection. .DESCRIPTION Creates a named in-memory connection to https://api.datto.com by default. The public key is stored as the PSCredential username and the secret key remains a SecureString while at rest in module memory. Credentials are never written to disk by this module. .PARAMETER Credential A PSCredential whose username is the Datto public key and whose password is the Datto secret key. .PARAMETER PublicKey The Datto API public key. .PARAMETER SecretKey The Datto API secret key as a SecureString. .PARAMETER Name A unique friendly name for the connection. .PARAMETER BaseUri The Datto API origin. HTTPS is required unless -AllowInsecureHttp is explicitly used for a local test server. .PARAMETER TimeoutSeconds The HTTP request timeout in seconds. .PARAMETER MaxRetryCount The maximum number of retries for idempotent requests after transient transport failures or HTTP 408, 429, 500, 502, 503, or 504 responses. .PARAMETER MaxRetryDelaySeconds The upper bound for each retry delay. .PARAMETER SetDefault Makes this connection the default connection. .PARAMETER Force Replaces an existing connection with the same name. .PARAMETER AllowInsecureHttp Allows an HTTP base URI. This is intended only for isolated local mock testing and should not be used with production credentials. .EXAMPLE $credential = Get-Credential -UserName 'PUBLIC_KEY' Connect-DattoApi -Credential $credential .EXAMPLE $secret = Read-Host 'Datto secret key' -AsSecureString Connect-DattoApi -PublicKey 'PUBLIC_KEY' -SecretKey $secret -Name 'Production' -SetDefault .OUTPUTS DattoBackupApi.Connection #> [CmdletBinding(DefaultParameterSetName = 'Credential', SupportsShouldProcess)] param( [Parameter(Mandatory, ParameterSetName = 'Credential')] [ValidateNotNull()] [pscredential]$Credential, [Parameter(Mandatory, ParameterSetName = 'Keys')] [ValidateNotNullOrEmpty()] [string]$PublicKey, [Parameter(Mandatory, ParameterSetName = 'Keys')] [ValidateNotNull()] [securestring]$SecretKey, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Name = 'Default', [Parameter()] [ValidateNotNull()] [uri]$BaseUri = 'https://api.datto.com', [Parameter()] [ValidateRange(1, 3600)] [int]$TimeoutSeconds = 100, [Parameter()] [ValidateRange(0, 10)] [int]$MaxRetryCount = 3, [Parameter()] [ValidateRange(1, 300)] [int]$MaxRetryDelaySeconds = 60, [Parameter()] [switch]$SetDefault, [Parameter()] [switch]$Force, [Parameter(DontShow)] [switch]$AllowInsecureHttp ) if ($PSCmdlet.ParameterSetName -eq 'Keys') { $Credential = [pscredential]::new($PublicKey, $SecretKey) } if ([string]::IsNullOrWhiteSpace($Name)) { throw [System.ArgumentException]::new('Connection Name cannot be empty or whitespace.') } if ([string]::IsNullOrWhiteSpace($Credential.UserName)) { throw [System.ArgumentException]::new('The PSCredential username must contain the Datto public key.') } if ($Credential.UserName.Contains(':')) { throw [System.ArgumentException]::new('The Datto public key cannot contain a colon because it is used as the HTTP Basic username.') } if ($Credential.Password.Length -eq 0) { throw [System.ArgumentException]::new('The Datto secret key cannot be empty.') } if (-not $BaseUri.IsAbsoluteUri) { throw [System.ArgumentException]::new('BaseUri must be an absolute URI.') } if ($BaseUri.Scheme -ne 'https' -and -not ($AllowInsecureHttp -and $BaseUri.Scheme -eq 'http')) { throw [System.ArgumentException]::new('BaseUri must use HTTPS. Use -AllowInsecureHttp only with an isolated local mock server.') } if (-not [string]::IsNullOrEmpty($BaseUri.UserInfo)) { throw [System.ArgumentException]::new('BaseUri must not contain embedded credentials.') } if (-not [string]::IsNullOrEmpty($BaseUri.Query) -or -not [string]::IsNullOrEmpty($BaseUri.Fragment)) { throw [System.ArgumentException]::new('BaseUri must not contain a query string or fragment.') } if ($BaseUri.AbsolutePath -ne '/') { throw [System.ArgumentException]::new('BaseUri must identify an API origin and cannot contain a path.') } $baseHost = $BaseUri.DnsSafeHost.Trim([char[]]'[]') if ($AllowInsecureHttp -and $BaseUri.Scheme -eq 'http' -and -not ($baseHost -in @('localhost', '127.0.0.1', '::1'))) { throw [System.ArgumentException]::new('Insecure HTTP connections are restricted to localhost addresses.') } $existing = Find-DattoApiConnectionStateByName -Name $Name if ($null -ne $existing -and -not $Force) { throw [System.InvalidOperationException]::new("A Datto API connection named '$Name' already exists. Use -Force to replace it.") } if (-not $PSCmdlet.ShouldProcess("Datto API connection '$Name'", 'Create or replace in-memory connection')) { return } $handler = $null $client = $null try { $handler = [System.Net.Http.HttpClientHandler]::new() $handler.AllowAutoRedirect = $false $handler.AutomaticDecompression = [Net.DecompressionMethods]::GZip -bor [Net.DecompressionMethods]::Deflate $client = [System.Net.Http.HttpClient]::new($handler, $true) $client.Timeout = [TimeSpan]::FromSeconds($TimeoutSeconds) $null = $client.DefaultRequestHeaders.UserAgent.TryParseAdd(('DattoBackupApi/{0}' -f $script:DattoApiModuleVersion)) $null = $client.DefaultRequestHeaders.UserAgent.TryParseAdd(('PowerShell/{0}' -f $PSVersionTable.PSVersion)) $id = [guid]::NewGuid().ToString() $normalisedBaseUri = [uri]($BaseUri.AbsoluteUri.TrimEnd('/')) $state = [ordered]@{ Id = $id Name = $Name BaseUri = $normalisedBaseUri Credential = $Credential Client = $client Handler = $handler CreatedAtUtc = [datetimeoffset]::UtcNow LastUsedAtUtc = $null TimeoutSeconds = $TimeoutSeconds MaxRetryCount = $MaxRetryCount MaxRetryDelaySeconds = $MaxRetryDelaySeconds RateLimit = [ordered]@{ Remaining = $null ResetEpoch = $null ResetAtUtc = $null Cost = $null ObservedAtUtc = $null } } } catch { if ($null -ne $client) { try { $client.Dispose() } catch { Write-Debug ('HttpClient cleanup failed: {0}' -f $_.Exception.Message) } } elseif ($null -ne $handler) { try { $handler.Dispose() } catch { Write-Debug ('HttpClientHandler cleanup failed: {0}' -f $_.Exception.Message) } } throw } # Register the fully constructed replacement before disposing the previous # connection so a construction failure cannot destroy a working session. $script:DattoApiState.Connections[$id] = $state $wasDefault = $false if ($null -ne $existing) { $wasDefault = ([string]$script:DattoApiState.DefaultConnectionId -eq [string]$existing.Id) Remove-DattoApiConnectionInternal -ConnectionState $existing $null = $script:DattoApiState.Connections.Remove([string]$existing.Id) } if ($SetDefault -or $wasDefault -or [string]::IsNullOrWhiteSpace([string]$script:DattoApiState.DefaultConnectionId)) { $script:DattoApiState.DefaultConnectionId = $id } return (Get-DattoApiConnectionView -ConnectionState $state) } |