From b0fe0ba00642d52838aa92d7b8d00c612636954b Mon Sep 17 00:00:00 2001 From: dritter Date: Wed, 15 Oct 2025 12:48:44 +0200 Subject: [PATCH] first commit --- .gitignore | 4 ++ Downloader/Script.ps1 | 31 +++++++++ Downloader/config.json | 67 +++++++++++++++++++ Downloader/functions/autoload.ps1 | 37 ++++++++++ .../ConvertFrom-ResponseHeaders.ps1 | 11 +++ .../private/features/Get-SoftwareUrl.ps1 | 61 +++++++++++++++++ .../private/features/Get-SoftwareVersion.ps1 | 56 ++++++++++++++++ .../private/features/Test-IfUpdated.ps1 | 27 ++++++++ .../private/features/Test-Signature.ps1 | 27 ++++++++ .../private/http/Invoke-FileDownload.ps1 | 42 ++++++++++++ .../private/http/Invoke-GetRequest.ps1 | 21 ++++++ .../private/http/Invoke-HeadRequest.ps1 | 21 ++++++ .../private/http/Invoke-RestRequest.ps1 | 28 ++++++++ .../functions/private/utils/Add-Items.ps1 | 26 +++++++ .../functions/private/utils/Get-JsonPath.ps1 | 17 +++++ .../functions/private/utils/Write-Log.ps1 | 37 ++++++++++ .../public/Find-SoftwareDownloadUrl.ps1 | 21 ++++++ .../public/Find-SoftwareDownloadVersion.ps1 | 24 +++++++ .../public/Get-SoftwareOutputItem.ps1 | 28 ++++++++ .../public/Import-SoftwareDownload.ps1 | 17 +++++ .../public/Invoke-SoftwareDownload.ps1 | 27 ++++++++ .../public/Set-SoftwareDownloadCache.ps1 | 29 ++++++++ .../public/Skip-SoftwareDownload.ps1 | 27 ++++++++ .../public/Start-SoftwareDownload.ps1 | 43 ++++++++++++ .../public/Test-SoftwareDownloadSignature.ps1 | 19 ++++++ Downloader/template/Untitled-2.html | 65 ++++++++++++++++++ install.ps1 | 59 ++++++++++++++++ 27 files changed, 872 insertions(+) create mode 100644 .gitignore create mode 100644 Downloader/Script.ps1 create mode 100644 Downloader/config.json create mode 100644 Downloader/functions/autoload.ps1 create mode 100644 Downloader/functions/private/converters/ConvertFrom-ResponseHeaders.ps1 create mode 100644 Downloader/functions/private/features/Get-SoftwareUrl.ps1 create mode 100644 Downloader/functions/private/features/Get-SoftwareVersion.ps1 create mode 100644 Downloader/functions/private/features/Test-IfUpdated.ps1 create mode 100644 Downloader/functions/private/features/Test-Signature.ps1 create mode 100644 Downloader/functions/private/http/Invoke-FileDownload.ps1 create mode 100644 Downloader/functions/private/http/Invoke-GetRequest.ps1 create mode 100644 Downloader/functions/private/http/Invoke-HeadRequest.ps1 create mode 100644 Downloader/functions/private/http/Invoke-RestRequest.ps1 create mode 100644 Downloader/functions/private/utils/Add-Items.ps1 create mode 100644 Downloader/functions/private/utils/Get-JsonPath.ps1 create mode 100644 Downloader/functions/private/utils/Write-Log.ps1 create mode 100644 Downloader/functions/public/Find-SoftwareDownloadUrl.ps1 create mode 100644 Downloader/functions/public/Find-SoftwareDownloadVersion.ps1 create mode 100644 Downloader/functions/public/Get-SoftwareOutputItem.ps1 create mode 100644 Downloader/functions/public/Import-SoftwareDownload.ps1 create mode 100644 Downloader/functions/public/Invoke-SoftwareDownload.ps1 create mode 100644 Downloader/functions/public/Set-SoftwareDownloadCache.ps1 create mode 100644 Downloader/functions/public/Skip-SoftwareDownload.ps1 create mode 100644 Downloader/functions/public/Start-SoftwareDownload.ps1 create mode 100644 Downloader/functions/public/Test-SoftwareDownloadSignature.ps1 create mode 100644 Downloader/template/Untitled-2.html create mode 100644 install.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45525f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +cache/ +downloads/ +logs/ +temp/ \ No newline at end of file diff --git a/Downloader/Script.ps1 b/Downloader/Script.ps1 new file mode 100644 index 0000000..4a70b10 --- /dev/null +++ b/Downloader/Script.ps1 @@ -0,0 +1,31 @@ +# If not running as file, ensure to be in cwd +$global:ScriptDirectory = if ([string]::IsNullOrWhiteSpace(($PSScriptRoot))) { Get-Location } else { $PSScriptRoot } + +. $ScriptDirectory\functions\autoload.ps1 + +# Getting all items +$allItems = Get-Content -Path $ScriptDirectory\config.json | ConvertFrom-Json + +# Main loop +foreach ($cfg in $allItems) { + + $software = Start-SoftwareDownload -cfg $cfg + if($software.CanContinue){ + continue; + } + + + Test-SoftwareDownloadSignature $software + if($software.CanContinue){ + continue; + } + + + Import-SoftwareDownlaod $software +} + + +Write-Log "Items updated: $($updatedItems.Count)" +Write-Log "Items checked: $($checkedItems.Count)" +Write-Log "Items failed: $($failedItems.Count)" +Write-Log "Items total: $($allItems.Count)" \ No newline at end of file diff --git a/Downloader/config.json b/Downloader/config.json new file mode 100644 index 0000000..aad420f --- /dev/null +++ b/Downloader/config.json @@ -0,0 +1,67 @@ +[ + { + "Name": "NotepadPlusPlus", + "Mode": "GitHubRelease", + "Source": "notepad-plus-plus/notepad-plus-plus", + "Asset": "npp.(?.+).Installer.x64.exe$", + "Target": "NotepadPlusPlus-Setup.exe", + "SigCN": null, + "Version": { + "Mode": "ReleaseRegex", + "Source": null, + "Asset": null + } + }, + { + "Name": "GoogleChrome", + "Mode": "StaticUrl", + "Source": "https://dl.google.com/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi", + "Asset": null, + "Target": "Chrome-Setup.msi", + "SigCN": "CN=Google LLC, O=Google LLC, L=Mountain View, S=California, C=US, SERIALNUMBER=3582691, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US", + "Version": { + "Mode": "RestRequest", + "Source": "https://versionhistory.googleapis.com/v1/chrome/platforms/win64/channels/stable/versions", + "Asset": "versions[0].version" + } + }, + { + "Name": "Firefox", + "Mode": "StaticUrl", + "Source": "https://download.mozilla.org/?product=firefox-esr-latest-ssl&os=win64&lang=de", + "Asset": null, + "Target": "Firefox-Setup.exe", + "SigCN": "CN=Mozilla Corporation, OU=Firefox Engineering Operations, O=Mozilla Corporation, L=San Francisco, S=California, C=US", + "Version": { + "Mode": "RestRequest", + "Source": "https://product-details.mozilla.org/1.0/firefox_versions.json", + "Asset": "FIREFOX_ESR" + } + }, + { + "Name": "7zip", + "Mode": "Regex", + "Source": "https://7-zip.de/download.html", + "Asset": "https.*\\/a/7z[0-9]+-x64\\.exe", + "Target": "7zip-Setup.exe", + "SigCN": null, + "Version": { + "Mode": "Regex", + "Source": "https://www.7-zip.org/download.html", + "Asset": "Zip\\s(?[^ ]+)\\s*\\(" + } + }, + { + "Name": "WinSCP", + "Mode": "SourceforgeBestRelease", + "Source": "https://sourceforge.net/projects/winscp/best_release.json", + "Asset": "windows", + "Target": "WinSCP-Setup.exe", + "SigCN": "CN=Martin Prikryl, O=Martin Prikryl, L=Prague, C=CZ, SERIALNUMBER=87331519, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.3=CZ", + "Version": { + "Mode": "Regex", + "Source": "https://winscp.net/eng/downloads.php", + "Asset": "/download/WinSCP-(?(.+))-Setup.exe/download" + } + } +] \ No newline at end of file diff --git a/Downloader/functions/autoload.ps1 b/Downloader/functions/autoload.ps1 new file mode 100644 index 0000000..c6bfa3f --- /dev/null +++ b/Downloader/functions/autoload.ps1 @@ -0,0 +1,37 @@ +. $ScriptDirectory\functions\private\utils\Write-Log.ps1 + +# Setting up da logga +Initialize-Log -LogDirectory ([IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\logs")).FullName) + +# Loading our functions dot-sourced +. $ScriptDirectory\functions\private\converters\ConvertFrom-ResponseHeaders.ps1 +. $ScriptDirectory\functions\private\http\Invoke-FileDownload.ps1 +. $ScriptDirectory\functions\private\http\Invoke-GetRequest.ps1 +. $ScriptDirectory\functions\private\http\Invoke-HeadRequest.ps1 +. $ScriptDirectory\functions\private\http\Invoke-RestRequest.ps1 +. $ScriptDirectory\functions\private\features\Get-SoftwareUrl.ps1 +. $ScriptDirectory\functions\private\features\Get-SoftwareVersion.ps1 +. $ScriptDirectory\functions\private\features\Test-IfUpdated.ps1 +. $ScriptDirectory\functions\private\features\Test-Signature.ps1 +. $ScriptDirectory\functions\private\utils\Add-Items.ps1 +. $ScriptDirectory\functions\private\utils\Get-JsonPath.ps1 + + +. $ScriptDirectory\functions\public\Get-SoftwareOutputItem.ps1 +. $ScriptDirectory\functions\public\Find-SoftwareDownloadUrl.ps1 +. $ScriptDirectory\functions\public\Skip-SoftwareDownload.ps1 +. $ScriptDirectory\functions\public\Invoke-SoftwareDownload.ps1 +. $ScriptDirectory\functions\public\Find-SoftwareDownloadVersion.ps1 +. $ScriptDirectory\functions\public\Set-SoftwareDownloadCache.ps1 +. $ScriptDirectory\functions\public\Start-SoftwareDownload.ps1 + + +# Create and define necessary directories +$global:outputDirectory = [IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\downloads")).FullName +$global:cacheDirectory = [IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\cache")).FullName +$global:tempDirectory = [IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\temp")).FullName + +# Setting up da logga +$global:updatedItems = @{} +$global:checkedItems = @{} +$global:failedItems = @{} diff --git a/Downloader/functions/private/converters/ConvertFrom-ResponseHeaders.ps1 b/Downloader/functions/private/converters/ConvertFrom-ResponseHeaders.ps1 new file mode 100644 index 0000000..e9de3d5 --- /dev/null +++ b/Downloader/functions/private/converters/ConvertFrom-ResponseHeaders.ps1 @@ -0,0 +1,11 @@ +function ConvertFrom-ResponseHeaders { + param( + $Response + ) + $headers = @{} + + foreach ($h in $response.Headers) { $headers[$h.Key] = $h.Value -join ', ' } + foreach ($h in $response.Content.Headers) { $headers[$h.Key] = $h.Value -join ', ' } + + return $headers +} \ No newline at end of file diff --git a/Downloader/functions/private/features/Get-SoftwareUrl.ps1 b/Downloader/functions/private/features/Get-SoftwareUrl.ps1 new file mode 100644 index 0000000..e864807 --- /dev/null +++ b/Downloader/functions/private/features/Get-SoftwareUrl.ps1 @@ -0,0 +1,61 @@ +function Get-SoftwareUrl { + param( + $cfg + ) + + switch ($cfg.Mode) { + 'GitHubRelease' { $url = Get-GitHubAssetUrl -Repo $cfg.Source -AssetPattern $cfg.Asset } + 'Regex' { $url = Get-RegexMatchUrl -Url $cfg.Source -Pattern $cfg.Asset } + 'SourceforgeBestRelease' { $url = Get-SourceforgeBestReleaseUrl -Url $cfg.Source -Plattform $cfg.Asset } + 'StaticUrl' { $url = $cfg.Source } + default { throw "Unknown mode: $($cfg.Mode) in $($cfg.Name)" } + } + + return $url +} + +function Get-GithubAssetUrl { + param( + [string]$Repo, + [string]$AssetPattern + ) + + $result = Invoke-RestRequest -Uri "https://api.github.com/repos/$Repo/releases/latest" ` + -Headers @{ Accept = 'application/json' + "User-Agent" = "PowerShell/Agent"} + + return $result.assets | Where-Object { $_.name -match $AssetPattern } | Select-Object -ExpandProperty browser_download_url +} + +function Get-RegexMatchUrl +{ + param( + [string]$Url, + [string]$Pattern + ) + + $MatchInfo = Invoke-GetRequest -Uri $Url | Select-String -Pattern $Pattern + $Value = $MatchInfo.Matches | Select-Object -First 1 -ExpandProperty Value + + if(!$Value){ + Write-Warning "Can not find pattern '$Pattern' on $Url" + return + } + + if($Value.StartsWith("/")){ + $Uri = [Uri]::new($Url) + return ("{0}://{1}{2}" -f $Uri.Scheme, $Uri.Host, $Value) + } + + return $Value +} + +function Get-SourceforgeBestReleaseUrl { + param( + [string]$Url, + [string]$Plattform + ) + + $Releases = Invoke-RestRequest -Uri $Url + return $Releases.platform_releases.$Plattform.url +} \ No newline at end of file diff --git a/Downloader/functions/private/features/Get-SoftwareVersion.ps1 b/Downloader/functions/private/features/Get-SoftwareVersion.ps1 new file mode 100644 index 0000000..d31fb54 --- /dev/null +++ b/Downloader/functions/private/features/Get-SoftwareVersion.ps1 @@ -0,0 +1,56 @@ +function Get-SoftwareVersion { + param( + $cfg, + $releaseUrl = "" + ) + + switch ($cfg.Version.Mode) { + 'ReleaseRegex' { $version = Get-SoftwareVersionReleaseRegex -cfg $cfg -ReleaseUrl $releaseUrl } + 'Regex' { $version = Get-SoftwareVersionRegex -cfg $cfg } + 'RestRequest' { $version = Get-SoftwareVersionRestRequest -cfg $cfg } + default { throw "Unknown mode: $($cfg.Version.Mode) in $($cfg.Name)" } + } + + return $version +} + +function Get-SoftwareVersionReleaseRegex { + param( + $cfg, + $ReleaseUrl + ) + + $Result = $ReleaseUrl | Select-String -Pattern $cfg.Asset + + if($Result.Matches){ + $FirstMatch = $Result.Matches | Where-Object { $_.Groups["Version"] } | Select-Object -First 1 + return $FirstMatch.Groups["Version"].Value + } + + return $null +} + +function Get-SoftwareVersionRegex { + param( + $cfg + ) + + $plain = Invoke-GetRequest -Uri $cfg.Version.Source + $Result = $plain | Select-String -Pattern $cfg.Version.Asset + + if($Result.Matches){ + $FirstMatch = $Result.Matches | Where-Object { $_.Groups["Version"] } | Select-Object -First 1 + return $FirstMatch.Groups["Version"].Value + } + + return $null +} + +function Get-SoftwareVersionRestRequest { + param( + $cfg + ) + + $json = Invoke-RestRequest -Uri $cfg.Version.Source + return Get-JsonPath -Json $json -Path $cfg.Version.Asset +} \ No newline at end of file diff --git a/Downloader/functions/private/features/Test-IfUpdated.ps1 b/Downloader/functions/private/features/Test-IfUpdated.ps1 new file mode 100644 index 0000000..6f1f390 --- /dev/null +++ b/Downloader/functions/private/features/Test-IfUpdated.ps1 @@ -0,0 +1,27 @@ +function Test-IfUpdated { + param( + [string]$Url, + [string]$CacheFile + ) + + if(-not (Test-Path -Path $CacheFile)){ + return $true + } + + $Cache = Get-Content $CacheFile | ConvertFrom-Json + $Head = Invoke-HeadRequest -Url $Url + + if($Head.Keys -contains "ETag" ` + -and $Cache.PSObject.Properties.Name -contains "ETag" ` + -and !($null -eq $Cache.ETag -or [string]::IsNullOrWhiteSpace($Cache.ETag))){ + return -not ($Head["ETag"] -eq $Cache.ETag) + } + + if($Head.Keys -contains "Last-Modified" ` + -and $Cache.PSObject.Properties.Name -contains "LastModified" ` + -and !($null -eq $Cache.LastModified -or [string]::IsNullOrWhiteSpace($Cache.LastModified))){ + return -not ($Head["Last-Modified"] -eq $Cache.LastModified) + } + + return $true +} \ No newline at end of file diff --git a/Downloader/functions/private/features/Test-Signature.ps1 b/Downloader/functions/private/features/Test-Signature.ps1 new file mode 100644 index 0000000..57e18e8 --- /dev/null +++ b/Downloader/functions/private/features/Test-Signature.ps1 @@ -0,0 +1,27 @@ +function Test-Signature{ + param( + [string]$FilePath, + [string]$ExpectedCN + ) + + try{ + $signature = Get-AuthenticodeSignature -FilePath $FilePath + + if(-not ($signature.SignerCertificate.Verify())){ + Write-Warning "Verify of $FilePath failed." + return $false + } + + $match = $signature.SignerCertificate.Subject -eq $ExpectedCN + + if(-not $match){ + Write-Warning "Signature verification of $FilePath failed. Signature mismatch. Expected: >$ExpectedCN< Got: $($signature.SignerCertificate.Subject)" + } + + return $match + } + catch{ + return $false + } + +} \ No newline at end of file diff --git a/Downloader/functions/private/http/Invoke-FileDownload.ps1 b/Downloader/functions/private/http/Invoke-FileDownload.ps1 new file mode 100644 index 0000000..6724eac --- /dev/null +++ b/Downloader/functions/private/http/Invoke-FileDownload.ps1 @@ -0,0 +1,42 @@ +function Invoke-FileDownload { + param ( + [Parameter(Mandatory = $true)] + [string]$Url, + + # Ziel-Ordner, in dem die Datei abgelegt wird + [Parameter(Mandatory = $true)] + [string]$FilePath + ) + + # HttpClient einmalig anlegen + $client = [System.Net.Http.HttpClient]::new() + + try { + # Nur Header laden, um sofort an Dateinamen zu kommen + $response = $client.GetAsync( + $Url, + [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead + ).Result + + $response.EnsureSuccessStatusCode() | Out-Null + + # Download-Stream in Datei schreiben + $inStream = $response.Content.ReadAsStreamAsync().Result + $outStream = [System.IO.File]::Create($FilePath) + $inStream.CopyToAsync($outStream).Wait() + + # Aufräumen + $outStream.Dispose() + $inStream.Dispose() + + Write-Host "Datei erfolgreich heruntergeladen: $FilePath" + return $response + } + catch { + Write-Error "Fehler beim Herunterladen: $_" + throw + } + finally { + $client.Dispose() + } +} diff --git a/Downloader/functions/private/http/Invoke-GetRequest.ps1 b/Downloader/functions/private/http/Invoke-GetRequest.ps1 new file mode 100644 index 0000000..a5f03e1 --- /dev/null +++ b/Downloader/functions/private/http/Invoke-GetRequest.ps1 @@ -0,0 +1,21 @@ +function Invoke-GetRequest { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string]$Uri + ) + + # .NET-Klasse laden (falls noch nicht im AppDomain) + Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue + + $client = [System.Net.Http.HttpClient]::new() + + # GET ausführen (synchron gewartet) + $response = $client.GetAsync($Uri).Result + if (-not $response.IsSuccessStatusCode) { + throw "Request failed: $($response.StatusCode) - $($response.ReasonPhrase)" + } + + # Rückgabe: reiner String-Content + return $response.Content.ReadAsStringAsync().Result +} diff --git a/Downloader/functions/private/http/Invoke-HeadRequest.ps1 b/Downloader/functions/private/http/Invoke-HeadRequest.ps1 new file mode 100644 index 0000000..c171ccb --- /dev/null +++ b/Downloader/functions/private/http/Invoke-HeadRequest.ps1 @@ -0,0 +1,21 @@ +function Invoke-HeadRequest { + param ( + [Parameter(Mandatory = $true)] + [string]$Url + ) + + # HttpClient in using-Block sorgt für sauberes Dispose + $client = [System.Net.Http.HttpClient]::new() + $request = [System.Net.Http.HttpRequestMessage]::new( + [System.Net.Http.HttpMethod]::Head, + $Url) + + # Antwort abholen + $response = $client.SendAsync($request).GetAwaiter().GetResult() + try { + return ConvertFrom-ResponseHeaders -Response $response + } + finally { + $response.Dispose() + } +} \ No newline at end of file diff --git a/Downloader/functions/private/http/Invoke-RestRequest.ps1 b/Downloader/functions/private/http/Invoke-RestRequest.ps1 new file mode 100644 index 0000000..4ade1cf --- /dev/null +++ b/Downloader/functions/private/http/Invoke-RestRequest.ps1 @@ -0,0 +1,28 @@ +function Invoke-RestRequest { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string]$Uri, + + [hashtable]$Headers = @{} + ) + + # .NET-Klasse laden und einmaligen HttpClient erzeugen + Add-Type -AssemblyName System.Net.Http + $client = [System.Net.Http.HttpClient]::new() + + # optionale Header setzen + foreach ($key in $Headers.Keys) { + $client.DefaultRequestHeaders.Add($key, $Headers[$key]) + } + + # synchronen GET ausführen (für asynchron siehe *.GetAsync().ConfigureAwait()) + $response = $client.GetAsync($Uri).Result + if (-not $response.IsSuccessStatusCode) { + throw "Request failed: $($response.StatusCode) - $($response.ReasonPhrase)" + } + + # Inhalt lesen + JSON in PSCustomObject umwandeln + $json = $response.Content.ReadAsStringAsync().Result + return $json | ConvertFrom-Json +} diff --git a/Downloader/functions/private/utils/Add-Items.ps1 b/Downloader/functions/private/utils/Add-Items.ps1 new file mode 100644 index 0000000..037b2fb --- /dev/null +++ b/Downloader/functions/private/utils/Add-Items.ps1 @@ -0,0 +1,26 @@ +function Add-FailedItems{ + param( + $output + ) + + Write-Log "$($output.InputObject.Name) : $($output.Message)" -Severity "Error"; + $failedItems[$output.InputObject.Name] = $output +} + +function Add-CheckedItems{ + param( + $output + ) + + Write-Log "$($output.InputObject.Name) : $($output.Message)" + $checkedItems[$output.InputObject.Name] = $output +} + +function Add-UpdatedItems{ + param( + $output + ) + + Write-Log "$($output.InputObject.Name) : $($output.Message)" + $updatedItems[$output.InputObject.Name] = $output +} \ No newline at end of file diff --git a/Downloader/functions/private/utils/Get-JsonPath.ps1 b/Downloader/functions/private/utils/Get-JsonPath.ps1 new file mode 100644 index 0000000..c5379cf --- /dev/null +++ b/Downloader/functions/private/utils/Get-JsonPath.ps1 @@ -0,0 +1,17 @@ +function Get-JsonPath { + param( + [Parameter(Mandatory=$true)][object]$Json, + [Parameter(Mandatory=$true)][string]$Path + ) + + $PathParts = $Path -split '\.' + $value = $Json + foreach ($part in $PathParts) { + if ($part -match '(.+)\[(\d+)\]') { + $value = $value.($matches[1])[[int]$matches[2]] + } else { + $value = $value.$part + } + } + return $value +} diff --git a/Downloader/functions/private/utils/Write-Log.ps1 b/Downloader/functions/private/utils/Write-Log.ps1 new file mode 100644 index 0000000..37acc5b --- /dev/null +++ b/Downloader/functions/private/utils/Write-Log.ps1 @@ -0,0 +1,37 @@ +function Initialize-Log { + param( + [string]$LogDirectory + ) + $LogFileName = "log_$(Get-Date -Format "yyyy-MM-dd_HHmmss").log" + $global:LogFilePath = Join-Path -Path $LogDirectory -ChildPath $LogFileName +} + +function Write-Log { + param( + [string]$Message, + [string]$Severity = "Info" + ) + + $Date = Get-Date -Format "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK" + $Prefix = "[$Date]:`t$($Severity.ToUpper())`t`t" + + Write-Host -Object $Message + "$Prefix$Message" | Out-File -FilePath $global:LogFilePath -Encoding utf8 -Append -Force +} + +function Set-StringLength { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string]$InputString, + + [Parameter(Mandatory)] + [int]$TotalLength + ) + + if ($InputString.Length -ge $TotalLength) { + return $InputString.Substring(0, $TotalLength) + } + + return $InputString + (" " * ($TotalLength - $InputString.Length)) +} \ No newline at end of file diff --git a/Downloader/functions/public/Find-SoftwareDownloadUrl.ps1 b/Downloader/functions/public/Find-SoftwareDownloadUrl.ps1 new file mode 100644 index 0000000..936a6b4 --- /dev/null +++ b/Downloader/functions/public/Find-SoftwareDownloadUrl.ps1 @@ -0,0 +1,21 @@ +function Find-SoftwareDownloadUrl{ + param( + $output + ) + + try { + $output.DownloadUrl = Get-SoftwareUrl -cfg $cfg + + $paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22 + Write-Log "$paddedName Extracted url: $($output.DownloadUrl)" + return $output + } + catch { + $output.Message = "Failed to find software url. Exception: $_" + $output.Exception = $_ + + Add-FailedItems $output + + return $output; + } +} \ No newline at end of file diff --git a/Downloader/functions/public/Find-SoftwareDownloadVersion.ps1 b/Downloader/functions/public/Find-SoftwareDownloadVersion.ps1 new file mode 100644 index 0000000..20841b3 --- /dev/null +++ b/Downloader/functions/public/Find-SoftwareDownloadVersion.ps1 @@ -0,0 +1,24 @@ +function Find-SoftwareDownloadVersion { + param( + $output + ) + + $paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22 + + Write-Log "$paddedName Try getting current product version" + try { + + $output.CurrentVersion = Get-SoftwareVersion -cfg $output.InputObject -releaseUrl $output.DownloadUrl + Write-Log "$paddedName Current Version -> $($output.CurrentVersion)" + + return $output + } + catch { + $output.Message = "Error getting current product version. Exception: $_" + $output.Exception = $_ + + Add-FailedItems $output + + return $output; + } +} \ No newline at end of file diff --git a/Downloader/functions/public/Get-SoftwareOutputItem.ps1 b/Downloader/functions/public/Get-SoftwareOutputItem.ps1 new file mode 100644 index 0000000..ad6b300 --- /dev/null +++ b/Downloader/functions/public/Get-SoftwareOutputItem.ps1 @@ -0,0 +1,28 @@ +function Get-SoftwareOutputItem{ + param( + $cfg + ) + + $output = [PSCustomObject] @{ + "InputObject" = $cfg + "CanContinue" = $true + "DownloadChanged" = $false + "SignatureVerified" = $false + "DownloadUrl" = $null + "DownloadHeaders" = $null + "DownloadResponse" = $null + "CacheFile" = $null + "TargetFile" = $null + "TempFile" = $null + "Exception" = $null + "Message" = $null + "CurrentVersion" = $null + "HasSignatureCheck" = (($null -ne $cfg.SigCN) -and ![string]::IsNullOrWhiteSpace($cfg.SigCN)) + } + + $output.CacheFile = Join-Path -Path $cacheDirectory -ChildPath "$($cfg.Name).cache.json" + $output.TargetFile = Join-Path -Path $outputDirectory -ChildPath $cfg.Target + $output.TempFile = Join-Path -Path $tempDirectory -ChildPath $cfg.Target + + return $output +} \ No newline at end of file diff --git a/Downloader/functions/public/Import-SoftwareDownload.ps1 b/Downloader/functions/public/Import-SoftwareDownload.ps1 new file mode 100644 index 0000000..293f4c3 --- /dev/null +++ b/Downloader/functions/public/Import-SoftwareDownload.ps1 @@ -0,0 +1,17 @@ +function Import-SoftwareDownlaod{ + param( + $output + ) + + $paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22 + + if ([IO.File]::Exists($output.TargetFile)) { + Write-Log "$paddedName Target already exists. Deleting: $($output.TargetFile)" + Remove-Item -Path $output.TargetFile -Force -Confirm:$false + } + + Write-Log "$paddedName Moving item $($output.TempFile) -> $($output.TargetFile)" + Move-Item -Path $output.TempFile -Destination $output.TargetFile -Force -Confirm:$false + + $updatedItems[$cfg.Name] = $target +} \ No newline at end of file diff --git a/Downloader/functions/public/Invoke-SoftwareDownload.ps1 b/Downloader/functions/public/Invoke-SoftwareDownload.ps1 new file mode 100644 index 0000000..5fa1686 --- /dev/null +++ b/Downloader/functions/public/Invoke-SoftwareDownload.ps1 @@ -0,0 +1,27 @@ +function Invoke-SoftwareDownload{ + param( + $output + ) + + $paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22 + Write-Log "$paddedName Remote file changed or local doesn't exist." + + try { + Write-Log "$paddedName Starting download $($output.DownloadUrl) -> $($output.TempFile)" + + $output.DownloadResponse = Invoke-FileDownload -Url $output.DownloadUrl -FilePath $output.TempFile + $output.DownloadHeaders = ConvertFrom-ResponseHeaders -Response $output.DownloadResponse + + Write-Log "$paddedName Download finished $($output.DownloadUrl) -> $($output.TempFile)" + + return $output + } + catch { + $output.Message = "Download failed $($output.DownloadUrl) -> $($output.TempFile). Exception: $_" + $output.Exception = $_ + + Add-FailedItems $output + + return $output; + } +} \ No newline at end of file diff --git a/Downloader/functions/public/Set-SoftwareDownloadCache.ps1 b/Downloader/functions/public/Set-SoftwareDownloadCache.ps1 new file mode 100644 index 0000000..a12c19d --- /dev/null +++ b/Downloader/functions/public/Set-SoftwareDownloadCache.ps1 @@ -0,0 +1,29 @@ +function Set-SoftwareDownloadCache { + param( + $output + ) + + $paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22 + Write-Log "$paddedName Writing cache file to $($output.CacheFile)" + + try { + @{ + "ETag" = $output.DownloadHeaders["ETag"] + "LastModified" = $output.DownloadHeaders["Last-Modified"] + "CurrentVersion" = $output.CurrentVersion + "DownloadUrl" = $output.DownloadUrl + "VersionSource" = $output.InputObject.Version + } | ConvertTo-Json | Set-Content -Path $output.CacheFile -Force -Confirm:$false + + return $output + } + catch { + $output.Message = "Error writing cache file. Exception: $_" + $output.Exception = $_ + + Add-FailedItems $output + + return $output; + } + +} \ No newline at end of file diff --git a/Downloader/functions/public/Skip-SoftwareDownload.ps1 b/Downloader/functions/public/Skip-SoftwareDownload.ps1 new file mode 100644 index 0000000..49cacb1 --- /dev/null +++ b/Downloader/functions/public/Skip-SoftwareDownload.ps1 @@ -0,0 +1,27 @@ +function Skip-SoftwareDownload { + param( + $output + ) + + try { + $downloadChanged = Test-IfUpdated $output.DownloadUrl $output.CacheFile + $localExists = Test-Path -Path $output.TargetFile + + $output.DownloadChanged = ($downloadChanged -or !$localExists) + } + catch { + $output.Message = "Failed when check if remote has changed. Exception: $_" + $output.Exception = $_ + + $output.DownloadChanged = $false + + Add-FailedItems $output + } + + if (-not $output.DownloadChanged) { + $output.Message = "Remote file hasn't changed." + Add-CheckedItems $output + } + + return $output +} \ No newline at end of file diff --git a/Downloader/functions/public/Start-SoftwareDownload.ps1 b/Downloader/functions/public/Start-SoftwareDownload.ps1 new file mode 100644 index 0000000..954ea90 --- /dev/null +++ b/Downloader/functions/public/Start-SoftwareDownload.ps1 @@ -0,0 +1,43 @@ +function Start-SoftwareDownload { + param( + $cfg + ) + + $output = Get-SoftwareOutputItem -cfg $cfg + $paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22 + + Write-Log "$paddedName Start processing item." + Write-Log "$paddedName Cache File='$($output.CacheFile)', Target File='$($output.TargetFile)', Temp File='$($output.TempFile)'" + + ## -- STEP 1: + # Find the softwares download url + Find-SoftwareDownloadUrl $output + if($output.Exception){ return $output } + + + ## -- STEP 2: + # Skip if remote not changed and local exists + Skip-SoftwareDownload $output + if($output.Exception){ return $output } + + ## -- STEP 3: + # Invoke download to temp folder + Invoke-SoftwareDownload $output + if($output.Exception -and !$output.DownloadChanged){ return $output } + + + ## -- STEP 4: + ## Find current version + Find-SoftwareDownloadVersion $output + if($output.Exception){ return $output } + + + ## -- STEP 5: + # Set cache file + Set-SoftwareDownloadCache $output + if($output.Exception){ return $output } + + $output.CanContinue = $false + + return $output +} \ No newline at end of file diff --git a/Downloader/functions/public/Test-SoftwareDownloadSignature.ps1 b/Downloader/functions/public/Test-SoftwareDownloadSignature.ps1 new file mode 100644 index 0000000..dc9971a --- /dev/null +++ b/Downloader/functions/public/Test-SoftwareDownloadSignature.ps1 @@ -0,0 +1,19 @@ +function Test-SoftwareDownloadSignature { + param( + $output + ) + + if($output.HasSignatureCheck){ + + $output.SignatureVerified = Test-Signature -FilePath $output.TempFile -ExpectedCN $output.InputObject.SigCN + + if(-not $output.SignatureVerified){ + Add-FailedItems $output + $output.CanContinue = $true + } + + } + + return $output + +} \ No newline at end of file diff --git a/Downloader/template/Untitled-2.html b/Downloader/template/Untitled-2.html new file mode 100644 index 0000000..3099901 --- /dev/null +++ b/Downloader/template/Untitled-2.html @@ -0,0 +1,65 @@ + + + + + + +

Check for Common Software Report

+

Kunde: $customerPrefix
+Server: $computerName
+Datum: $(Get-Date -Format 'dd.MM.yyyy HH:mm')

+ +

Zusammenfassung

+ + + + +
Geprüfte Software-Pakete$($SmartResults.TotalChecked)
Durchgeführte Updates$($SmartResults.TotalUpdates)
Gesamtgröße Downloads$TotalSizeMB MB
+ +

Aktualisierte Software

+ + + + + + + + + + + + + + +
ProduktVorherige VersionNeue VersionDateiStatus
$($res.Software)$($res.UpdateCheck.LocalVersion)$($res.UpdateCheck.OnlineVersion)$fileCellUpdate durchgeführt
$($res.Software)$($res.UpdateCheck.LocalVersion)$($res.UpdateCheck.OnlineVersion)$fileCellUpdate durchgeführt
+

Aktueller Software-Bestand

+ + + + + + + +
StatusProduktVersionDateiLetzter Check
$status$($res.Software)$($info.Version)$($info.FileName) ($([math]::Round([double]$info.FileSizeMB,1)) MB)$($info.LastCheck)
+

Nicht verfügbare Ordner

" + ($SkippedFolders -join ', ') + "

+

Systeminfo

+ + + + + +
Server$computerName
Kunde$customerPrefix
Ausführungszeit$(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')
Basis-Pfad$BasePath
+

Automatisch generiert von Fremdsoftware Download Script v2.0
+Schleupen SE – ApplicationService EWW
+Hinweis: E-Mail wird nur bei tatsächlichen Updates versendet.

+ \ No newline at end of file diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..12c522e --- /dev/null +++ b/install.ps1 @@ -0,0 +1,59 @@ +$ScriptDirectory = if ([string]::IsNullOrWhiteSpace(($PSScriptRoot))) { Get-Location } else { $PSScriptRoot } + +$DestinationDirectory = [IO.Directory]::CreateDirectory("C:\Scripts").FullName + +Copy-Item -Path ([IO.Path]::Combine($ScriptDirectory, "Downloader")) ` + -Destination $DestinationDirectory ` + -Recurse ` + -Force ` + -Confirm:$false + +# -------------------- Variablen -------------------- +$TaskName = "Download latest application software" +$ScriptPath = [IO.Path]::Combine($DestinationDirectory, "Downloader\Script.ps1") + +# Optional: Task Beschreibung +$TaskDescription = "Startet das PowerShell-Projekt automatisch beim Systemstart." + +# -------------------- Existiert der Task schon? -------------------- +$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + +if ($existingTask) { + Write-Host "Task '$TaskName' existiert bereits. Aktualisiere..." -ForegroundColor Yellow + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false +} + +# -------------------- Task Action -------------------- +# PowerShell-Executable +$pwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue).Source +if (-not $pwshPath) { $pwshPath = (Get-Command powershell).Source } + +$Action = New-ScheduledTaskAction -Execute $pwshPath -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`"" + +# -------------------- Task Trigger -------------------- +# Beispiel: Beim Systemstart (kannst du ändern zu -AtLogon, -Daily etc.) +$Trigger = New-ScheduledTaskTrigger -Daily -At 20:00 + +# -------------------- Task Einstellungen -------------------- +$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable + +# -------------------- Task Principal -------------------- +# Führt mit höchsten Privilegien unter SYSTEM aus +$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest + +# -------------------- Task Registrierung -------------------- +try{ + Register-ScheduledTask ` + -TaskName $TaskName ` + -Description $TaskDescription ` + -Action $Action ` + -Trigger $Trigger ` + -Principal $Principal ` + -Settings $Settings + + Write-Host "Task '$TaskName' wurde erfolgreich registriert!" -ForegroundColor Green + Write-Host "Pfad: $ScriptPath" +} +catch{ + Write-Host "Fehler beim registrieren von Task '$TaskName'. $($_.Exception.Message)" -ForegroundColor Red +} \ No newline at end of file