# Copyright (c) 2026, R. W. Rodolico # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # upload_http.ps1 # Windows port of transports/upload_http: POSTs the report to 'URL' as # multipart/form-data. Loaded by sysinfo_client.ps1 (dot-sourced in a fresh # script block), which calls Send-SysinfoReport with the transport's config # keys merged with the client globals (Spec 3.5). # # Same parameter keys, POST field names and return codes as the Perl script: # 1 sent (HTTP 2xx) # 4 request failed or a non-2xx answer (redirects are not followed, as LWP # does not follow them for POST) # 5-9 no 'key for report' / 'key for date' / 'key for hostname' / # 'key for client' / 'key for serial number' # 10 System.Net.Http could not be loaded # A missing URL throws, as the Perl script dies. # # Unlike the Perl script no backup copy of the report is written to a temp # file; the client writes unsent.json when every transport fails. # # Must stay Windows PowerShell 5.1 compatible and pure ASCII. # # Version 1.0.0 20261018 Claude # Initial version (bug 211, stage W3); a URL without http:// or https:// now fails with a clear message (found at M1) ####################################################### # # Write-UploadHttpError( $Message ) # # To the client log when loaded by the client, else to # STDERR (where the Perl script writes). # ####################################################### function Write-UploadHttpError { param([string]$Message) if (Get-Command -Name 'Write-SysinfoLog' -ErrorAction SilentlyContinue) { Write-SysinfoLog 0 "upload_http: $Message" } else { [Console]::Error.WriteLine("upload_http: $Message") } } ####################################################### # # Get-UploadHttpValue( $Params, $Key ) # # String value of $Key, '' when absent or null. Works for # hashtables, ordered and case-sensitive maps alike. # ####################################################### function Get-UploadHttpValue { param($Params, [string]$Key) if ($null -eq $Params -or -not $Params.Contains($Key)) { return '' } if ($null -eq $Params[$Key]) { return '' } return [string]$Params[$Key] } ####################################################### # # Get-UploadHttpFileName( $Params ) # # Port of makeFileName: # ___.sysinfo[.] # with every run of whitespace replaced by one underscore. # ####################################################### function Get-UploadHttpFileName { param($Params) $name = (@('report date', 'client name', 'host name', 'serial number') | ForEach-Object { Get-UploadHttpValue $Params $_ }) -join '_' $name += '.sysinfo' $fileType = Get-UploadHttpValue $Params 'fileType' if ($fileType -ne '') { $name += '.' + $fileType } return ($name -replace '\s+', '_') } ####################################################### # # Get-UploadHttpFields( $Params ) # # The form fields in Perl doit's order, or the int return # code (5-9) for the first missing 'key for ...'. The # report part itself is added by the caller under # 'ReportKey'. # ####################################################### function Get-UploadHttpFields { param($Params) $reportKey = Get-UploadHttpValue $Params 'key for report' if ($reportKey -eq '') { return 5 } $fields = New-Object System.Collections.ArrayList $checks = @( @('key for date', 'report date', 6), @('key for hostname', 'host name', 7), @('key for client', 'client name', 8), @('key for serial number', 'serial number', 9) ) foreach ($check in $checks) { $fieldName = Get-UploadHttpValue $Params $check[0] if ($fieldName -eq '') { return [int]$check[2] } [void]$fields.Add(@($fieldName, (Get-UploadHttpValue $Params $check[1]))) } $uploadType = Get-UploadHttpValue $Params 'upload_type' if ($uploadType -ne '') { [void]$fields.Add(@('upload_type', $uploadType)) } [void]$fields.Add(@('filename', (Get-UploadHttpFileName $Params))) return @{ 'ReportKey' = $reportKey; 'Fields' = $fields } } ####################################################### # # New-UploadHttpDisposition( $Name, $FileName ) # # form-data Content-Disposition with quoted name (and # filename), as browsers and LWP send them. # ####################################################### function New-UploadHttpDisposition { param([string]$Name, [string]$FileName = '') $disposition = New-Object System.Net.Http.Headers.ContentDispositionHeaderValue 'form-data' $disposition.Name = '"' + $Name.Replace('"', '') + '"' if ($FileName -ne '') { $disposition.FileName = '"' + $FileName.Replace('"', '') + '"' } return $disposition } ####################################################### # # Send-SysinfoReport( $Params, $Report ) # # Transport entry point (Spec 3.5); returns the int code. # ####################################################### function Send-SysinfoReport($Params, [string]$Report) { $url = Get-UploadHttpValue $Params 'URL' if ($url -eq '') { throw 'Could not find URL to send to' } if ($url -notmatch '^https?://') { Write-UploadHttpError "URL must start with http:// or https:// (got: $url); fix the URL in the config file" return 4 } $plan = Get-UploadHttpFields $Params if ($plan -is [int]) { return $plan } try { Add-Type -AssemblyName System.Net.Http -ErrorAction Stop } catch { Write-UploadHttpError "cannot load System.Net.Http: $($_.Exception.Message)" return 10 } # Server 2016 / .NET Framework 4.6 defaults lack TLS 1.2 try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { # not settable on this runtime (PowerShell 7 ignores it anyway) } $handler = $null $client = $null $content = $null $response = $null try { $content = New-Object System.Net.Http.MultipartFormDataContent # report as a file part named temp.report, raw UTF-8 bytes, as the Perl script $reportPart = New-Object System.Net.Http.ByteArrayContent (, ((New-Object System.Text.UTF8Encoding $false).GetBytes($Report))) $reportPart.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue 'application/x-www-form-urlencoded' $reportPart.Headers.ContentDisposition = New-UploadHttpDisposition $plan['ReportKey'] 'temp.report' $content.Add($reportPart) foreach ($field in $plan['Fields']) { $part = New-Object System.Net.Http.ByteArrayContent (, ((New-Object System.Text.UTF8Encoding $false).GetBytes([string]$field[1]))) $part.Headers.ContentDisposition = New-UploadHttpDisposition ([string]$field[0]) $content.Add($part) } $handler = New-Object System.Net.Http.HttpClientHandler $handler.AllowAutoRedirect = $false $client = New-Object System.Net.Http.HttpClient $handler $client.Timeout = [TimeSpan]::FromSeconds(180) $response = $client.PostAsync($url, $content).GetAwaiter().GetResult() if ($response.IsSuccessStatusCode) { return 1 } $body = '' try { $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() } catch { $body = '' } Write-UploadHttpError "URL did not work. Full URI sent follows`n$url`nReturn was $([int]$response.StatusCode) $($response.ReasonPhrase) $body" return 4 } catch { $inner = $_.Exception while ($null -ne $inner.InnerException) { $inner = $inner.InnerException } Write-UploadHttpError "URL did not work: $url ($($inner.Message))" return 4 } finally { if ($null -ne $response) { $response.Dispose() } if ($null -ne $client) { $client.Dispose() } elseif ($null -ne $handler) { $handler.Dispose() } if ($null -ne $content) { $content.Dispose() } } }