diff --git a/internal/templates/userdata/gitea_windows_userdata.tmpl b/internal/templates/userdata/gitea_windows_userdata.tmpl index e0146b09c..8b0c261f0 100644 --- a/internal/templates/userdata/gitea_windows_userdata.tmpl +++ b/internal/templates/userdata/gitea_windows_userdata.tmpl @@ -7,82 +7,82 @@ Param( $ErrorActionPreference="Stop" function Start-ExecuteWithRetry { - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] - [ScriptBlock]$ScriptBlock, - [int]$MaxRetryCount=10, - [int]$RetryInterval=3, - [string]$RetryMessage, - [array]$ArgumentList=@() - ) - PROCESS { - $currentErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - $retryCount = 0 - while ($true) { - try { - $res = Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList - $ErrorActionPreference = $currentErrorActionPreference - return $res - } catch [System.Exception] { - $retryCount++ - - if ($_.Exception -is [System.Net.WebException]) { - $webResponse = $_.Exception.Response + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ScriptBlock]$ScriptBlock, + [int]$MaxRetryCount=10, + [int]$RetryInterval=3, + [string]$RetryMessage, + [array]$ArgumentList=@() + ) + PROCESS { + $currentErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $retryCount = 0 + while ($true) { + try { + $res = Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList + $ErrorActionPreference = $currentErrorActionPreference + return $res + } catch [System.Exception] { + $retryCount++ + + if ($_.Exception -is [System.Net.WebException]) { + $webResponse = $_.Exception.Response # Skip retry on Error: 4XX (e.g. 401 Unauthorized, 404 Not Found etc.) - if ($webResponse -and $webResponse.StatusCode -ge 400 -and $webResponse.StatusCode -lt 500) { - # Skip retry on 4xx errors - Write-Output "Encountered non-retryable error (4xx): $($_.Exception.Message)" - $ErrorActionPreference = $currentErrorActionPreference - throw - } - } - - if ($retryCount -gt $MaxRetryCount) { - $ErrorActionPreference = $currentErrorActionPreference - throw - } else { - if ($RetryMessage) { - Write-Output $RetryMessage - } elseif ($_) { - Write-Output $_ - } - Start-Sleep -Seconds $RetryInterval - } - } - } - } + if ($webResponse -and $webResponse.StatusCode -ge 400 -and $webResponse.StatusCode -lt 500) { + # Skip retry on 4xx errors + Write-Output "Encountered non-retryable error (4xx): $($_.Exception.Message)" + $ErrorActionPreference = $currentErrorActionPreference + throw + } + } + + if ($retryCount -gt $MaxRetryCount) { + $ErrorActionPreference = $currentErrorActionPreference + throw + } else { + if ($RetryMessage) { + Write-Output $RetryMessage + } elseif ($_) { + Write-Output $_ + } + Start-Sleep -Seconds $RetryInterval + } + } + } + } } function Get-RandomString { - [CmdletBinding()] - Param( - [int]$Length=13 - ) - PROCESS { - if($Length -lt 6) { - $Length = 6 - } - $special = @(44, 45, 46, 64) - $numeric = 48..57 - $upper = 65..90 - $lower = 97..122 - - $passwd = [System.Collections.Generic.List[object]](New-object "System.Collections.Generic.List[object]") - for($i=0; $i -lt $Length-4; $i++){ - $c = get-random -input ($special + $numeric + $upper + $lower) - $passwd.Add([char]$c) - } - - $passwd.Add([char](get-random -input $numeric)) - $passwd.Add([char](get-random -input $special)) - $passwd.Add([char](get-random -input $upper)) - $passwd.Add([char](get-random -input $lower)) - - $Random = New-Object Random - return [string]::join("",($passwd|Sort-Object {$Random.Next()})) - } + [CmdletBinding()] + Param( + [int]$Length=13 + ) + PROCESS { + if($Length -lt 6) { + $Length = 6 + } + $special = @(44, 45, 46, 64) + $numeric = 48..57 + $upper = 65..90 + $lower = 97..122 + + $passwd = [System.Collections.Generic.List[object]](New-object "System.Collections.Generic.List[object]") + for($i=0; $i -lt $Length-4; $i++){ + $c = get-random -input ($special + $numeric + $upper + $lower) + $passwd.Add([char]$c) + } + + $passwd.Add([char](get-random -input $numeric)) + $passwd.Add([char](get-random -input $special)) + $passwd.Add([char](get-random -input $upper)) + $passwd.Add([char](get-random -input $lower)) + + $Random = New-Object Random + return [string]::join("",($passwd|Sort-Object {$Random.Next()})) + } } Add-Type -TypeDefinition @" @@ -92,87 +92,87 @@ using System.Text; public class GrantSysPrivileges { - [StructLayout(LayoutKind.Sequential)] - public struct LSA_UNICODE_STRING - { - public ushort Length; - public ushort MaximumLength; - public IntPtr Buffer; - } - - [StructLayout(LayoutKind.Sequential)] - public struct LSA_OBJECT_ATTRIBUTES - { - public int Length; - public IntPtr RootDirectory; - public IntPtr ObjectName; - public uint Attributes; - public IntPtr SecurityDescriptor; - public IntPtr SecurityQualityOfService; - } - - [DllImport("advapi32.dll", SetLastError=true)] - public static extern uint LsaOpenPolicy( - ref LSA_UNICODE_STRING SystemName, - ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, - uint DesiredAccess, - out IntPtr PolicyHandle - ); - - [DllImport("advapi32.dll", SetLastError=true)] - public static extern uint LsaAddAccountRights( - IntPtr PolicyHandle, - IntPtr AccountSid, - LSA_UNICODE_STRING[] UserRights, - uint CountOfRights - ); - - [DllImport("advapi32.dll")] - public static extern uint LsaClose(IntPtr PolicyHandle); - - [DllImport("advapi32.dll")] - public static extern uint LsaNtStatusToWinError(uint status); - - public const uint POLICY_ALL_ACCESS = 0x00F0FFF; - - public static uint GrantPrivilege(byte[] sid, string[] rights) - { - LSA_OBJECT_ATTRIBUTES loa = new LSA_OBJECT_ATTRIBUTES(); - LSA_UNICODE_STRING systemName = new LSA_UNICODE_STRING(); - - IntPtr policyHandle; - uint result = LsaOpenPolicy(ref systemName, ref loa, POLICY_ALL_ACCESS, out policyHandle); - if (result != 0) - { - return LsaNtStatusToWinError(result); - } - - LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[rights.Length]; - for (int i = 0; i < rights.Length; i++) - { - byte[] bytes = Encoding.Unicode.GetBytes(rights[i]); - IntPtr ptr = Marshal.AllocHGlobal(bytes.Length); - Marshal.Copy(bytes, 0, ptr, bytes.Length); - - userRights[i].Buffer = ptr; - userRights[i].Length = (ushort)bytes.Length; - userRights[i].MaximumLength = (ushort)(bytes.Length); - } - - IntPtr sidPtr = Marshal.AllocHGlobal(sid.Length); - Marshal.Copy(sid, 0, sidPtr, sid.Length); - - result = LsaAddAccountRights(policyHandle, sidPtr, userRights, (uint)rights.Length); - LsaClose(policyHandle); - - foreach (var right in userRights) - { - Marshal.FreeHGlobal(right.Buffer); - } - Marshal.FreeHGlobal(sidPtr); - - return LsaNtStatusToWinError(result); - } + [StructLayout(LayoutKind.Sequential)] + public struct LSA_UNICODE_STRING + { + public ushort Length; + public ushort MaximumLength; + public IntPtr Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LSA_OBJECT_ATTRIBUTES + { + public int Length; + public IntPtr RootDirectory; + public IntPtr ObjectName; + public uint Attributes; + public IntPtr SecurityDescriptor; + public IntPtr SecurityQualityOfService; + } + + [DllImport("advapi32.dll", SetLastError=true)] + public static extern uint LsaOpenPolicy( + ref LSA_UNICODE_STRING SystemName, + ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, + uint DesiredAccess, + out IntPtr PolicyHandle + ); + + [DllImport("advapi32.dll", SetLastError=true)] + public static extern uint LsaAddAccountRights( + IntPtr PolicyHandle, + IntPtr AccountSid, + LSA_UNICODE_STRING[] UserRights, + uint CountOfRights + ); + + [DllImport("advapi32.dll")] + public static extern uint LsaClose(IntPtr PolicyHandle); + + [DllImport("advapi32.dll")] + public static extern uint LsaNtStatusToWinError(uint status); + + public const uint POLICY_ALL_ACCESS = 0x00F0FFF; + + public static uint GrantPrivilege(byte[] sid, string[] rights) + { + LSA_OBJECT_ATTRIBUTES loa = new LSA_OBJECT_ATTRIBUTES(); + LSA_UNICODE_STRING systemName = new LSA_UNICODE_STRING(); + + IntPtr policyHandle; + uint result = LsaOpenPolicy(ref systemName, ref loa, POLICY_ALL_ACCESS, out policyHandle); + if (result != 0) + { + return LsaNtStatusToWinError(result); + } + + LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[rights.Length]; + for (int i = 0; i < rights.Length; i++) + { + byte[] bytes = Encoding.Unicode.GetBytes(rights[i]); + IntPtr ptr = Marshal.AllocHGlobal(bytes.Length); + Marshal.Copy(bytes, 0, ptr, bytes.Length); + + userRights[i].Buffer = ptr; + userRights[i].Length = (ushort)bytes.Length; + userRights[i].MaximumLength = (ushort)(bytes.Length); + } + + IntPtr sidPtr = Marshal.AllocHGlobal(sid.Length); + Marshal.Copy(sid, 0, sidPtr, sid.Length); + + result = LsaAddAccountRights(policyHandle, sidPtr, userRights, (uint)rights.Length); + LsaClose(policyHandle); + + foreach (var right in userRights) + { + Marshal.FreeHGlobal(right.Buffer); + } + Marshal.FreeHGlobal(sidPtr); + + return LsaNtStatusToWinError(result); + } } "@ -Language CSharp @@ -219,6 +219,9 @@ function Invoke-FastWebRequest { foreach ($k in $Headers.Keys){ $client.DefaultRequestHeaders.Add($k, $Headers[$k]) } + if (-not $Headers.ContainsKey('User-Agent')) { + $client.DefaultRequestHeaders.Add('User-Agent', 'Invoke-FastWebRequest/1.0') + } $task = $client.GetStreamAsync($Uri) $response = $task.Result if($task.IsFaulted) { @@ -335,46 +338,46 @@ function Invoke-GarmFailure() { } function Set-SystemInfo { - [CmdletBinding()] - param ( - [parameter(Mandatory=$true)] - [string]$CallbackURL, - [parameter(Mandatory=$true)] - [string]$RunnerDir, + [CmdletBinding()] + param ( [parameter(Mandatory=$true)] - [string]$BearerToken - ) - - # Construct the path to the .runner file - $agentInfoFile = Join-Path $RunnerDir ".runner" - - # Read and parse the JSON content from the .runner file - $agentInfo = ConvertFrom-Json (Get-Content -Raw -Path $agentInfoFile) - $AgentId = $agentInfo.agent_id - - # Retrieve OS information - $osInfo = Get-WmiObject -Class Win32_OperatingSystem - $osName = $osInfo.Caption - $osVersion = $osInfo.Version - - # Strip status from the callback URL - if ($CallbackUrl -match '^(.*)/status(/)?$') { - $CallbackUrl = $matches[1] - } - - $SysInfoUrl = "$CallbackUrl/system-info/" - $Payload = @{ - os_name = $OSName - os_version = $OSVersion - agent_id = $AgentId - } | ConvertTo-Json - - # Send the POST request - try { - Invoke-RestMethod -Uri $SysInfoUrl -Method Post -Body $Payload -ContentType 'application/json' -Headers @{ 'Authorization' = "Bearer $BearerToken" } -ErrorAction Stop - } catch { - Write-Output "Failed to send the system information." - } + [string]$CallbackURL, + [parameter(Mandatory=$true)] + [string]$RunnerDir, + [parameter(Mandatory=$true)] + [string]$BearerToken + ) + + # Construct the path to the .runner file + $agentInfoFile = Join-Path $RunnerDir ".runner" + + # Read and parse the JSON content from the .runner file + $agentInfo = ConvertFrom-Json (Get-Content -Raw -Path $agentInfoFile) + $AgentId = $agentInfo.agent_id + + # Retrieve OS information + $osInfo = Get-WmiObject -Class Win32_OperatingSystem + $osName = $osInfo.Caption + $osVersion = $osInfo.Version + + # Strip status from the callback URL + if ($CallbackUrl -match '^(.*)/status(/)?$') { + $CallbackUrl = $matches[1] + } + + $SysInfoUrl = "$CallbackUrl/system-info/" + $Payload = @{ + os_name = $OSName + os_version = $OSVersion + agent_id = $AgentId + } | ConvertTo-Json + + # Send the POST request + try { + Invoke-RestMethod -Uri $SysInfoUrl -Method Post -Body $Payload -ContentType 'application/json' -Headers @{ 'Authorization' = "Bearer $BearerToken" } -ErrorAction Stop + } catch { + Write-Output "Failed to send the system information." + } } $CallbackURL="{{.CallbackURL}}" @@ -383,46 +386,46 @@ if (!($CallbackURL -match "^(.*)/status(/)?$")) { } $GHRunnerGroup = "{{.GitHubRunnerGroup}}" function Get-IsAgentMode { - return {{ if .AgentMode }}$true{{ else }}$false{{ end }} + return {{ if .AgentMode }}$true{{ else }}$false{{ end }} } function Get-AgentURL { - return "{{ .AgentURL }}" + return "{{ .AgentURL }}" } function Get-AgentToken { - return "{{ .AgentToken }}" + return "{{ .AgentToken }}" } function Get-AgentDownloadURL { - return "{{ .AgentDownloadURL }}" + return "{{ .AgentDownloadURL }}" } function Get-AgentShellEnabled { - return "{{ .AgentShell }}" + return "{{ .AgentShell }}" } function Install-GarmAgent { - [CmdletBinding()] - param ( - [parameter(Mandatory=$true)] - [System.Management.Automation.PSCredential]$pscreds, - [parameter(Mandatory=$true)] - [string]$runnerExecutable - ) - Update-GarmStatus -Message "Agent mode is enabled" -CallbackURL $CallbackURL | Out-Null - $agentDir = "C:\garm-agent" - mkdir -Force $agentDir - $agentURL = Get-AgentURL - $agentDownloadURL = Get-AgentDownloadURL - $agentToken = Get-AgentToken - $agentDownloadHeaders=@{ - "Authorization"="Bearer $Token" - } - $shellEnabled = Get-AgentShellEnabled - $runnerExecutable = $runnerExecutable.Replace('\', '/') - # ' - Set-Content "$agentDir\garm-agent.toml" @" + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [System.Management.Automation.PSCredential]$pscreds, + [parameter(Mandatory=$true)] + [string]$runnerExecutable + ) + Update-GarmStatus -Message "Agent mode is enabled" -CallbackURL $CallbackURL | Out-Null + $agentDir = "C:\garm-agent" + mkdir -Force $agentDir + $agentURL = Get-AgentURL + $agentDownloadURL = Get-AgentDownloadURL + $agentToken = Get-AgentToken + $agentDownloadHeaders=@{ + "Authorization"="Bearer $Token" + } + $shellEnabled = Get-AgentShellEnabled + $runnerExecutable = $runnerExecutable.Replace('\', '/') + # ' + Set-Content "$agentDir\garm-agent.toml" @" server_url = "$agentURL" log_file = "C:/garm-agent/garm-agent.log" shell = "cmd.exe" @@ -433,38 +436,38 @@ runner_cmdline = ["$runnerExecutable", "daemon", "--once"] state_db_path = "C:/garm-agent/agent-state.db" "@ - Update-GarmStatus -Message "Downloading agent from: $agentDownloadURL" -CallbackURL $CallbackURL | Out-Null - Start-ExecuteWithRetry -ScriptBlock { - Invoke-FastWebRequest -Headers $agentDownloadHeaders -Uri "$agentDownloadURL" -OutFile $agentDir\garm-agent.exe - } -MaxRetryCount 5 -RetryInterval 5 -RetryMessage "Retrying download of runner..." - - try { - New-Service -Name garm-agent -BinaryPathName "$agentDir\garm-agent.exe daemon --config $agentDir\garm-agent.toml" -DisplayName "garm-agent" -Description "GARM agent" -StartupType Automatic -Credential $pscreds - } catch { - Invoke-GarmFailure -CallbackURL $CallbackURL -Message "failed to set up garm agent $_" - } - Start-Service garm-agent + Update-GarmStatus -Message "Downloading agent from: $agentDownloadURL" -CallbackURL $CallbackURL | Out-Null + Start-ExecuteWithRetry -ScriptBlock { + Invoke-FastWebRequest -Headers $agentDownloadHeaders -Uri "$agentDownloadURL" -OutFile $agentDir\garm-agent.exe + } -MaxRetryCount 5 -RetryInterval 5 -RetryMessage "Retrying download of runner..." + + try { + New-Service -Name garm-agent -BinaryPathName "$agentDir\garm-agent.exe daemon --config $agentDir\garm-agent.toml" -DisplayName "garm-agent" -Description "GARM agent" -StartupType Automatic -Credential $pscreds + } catch { + Invoke-GarmFailure -CallbackURL $CallbackURL -Message "failed to set up garm agent $_" + } + Start-Service garm-agent } function Install-NSSM { - [CmdletBinding()] - param ( - [parameter(Mandatory=$true)] - [string]$username, - [parameter(Mandatory=$true)] - [string]$password, - [parameter(Mandatory=$true)] - [string]$runnerDir - ) + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [string]$username, + [parameter(Mandatory=$true)] + [string]$password, + [parameter(Mandatory=$true)] + [string]$runnerDir + ) Start-ExecuteWithRetry -ScriptBlock { Invoke-FastWebRequest -Uri "https://nssm.cc/ci/nssm-2.24-103-gdee49fc.zip" -OutFile "$runnerDir/nssm.zip" } -MaxRetryCount 5 -RetryInterval 5 -RetryMessage "Retrying download of nssm..." Expand-Archive -Path "$runnerDir/nssm.zip" -DestinationPath "$runnerDir/nssm" -Force - mv "$runnerDir\nssm\nssm-2.24-103-gdee49fc\win64\nssm.exe" "$runnerDir/nssm.exe" - rm -Recurse -Force "$runnerDir\nssm" - rm -Force "$runnerDir\nssm.zip" - $nssm="$runnerDir/nssm.exe" + mv "$runnerDir\nssm\nssm-2.24-103-gdee49fc\win64\nssm.exe" "$runnerDir/nssm.exe" + rm -Recurse -Force "$runnerDir\nssm" + rm -Force "$runnerDir\nssm.zip" + $nssm="$runnerDir/nssm.exe" & $nssm install GiteaRunner "$runnerDir/gitea-runner.exe" & $nssm set GiteaRunner AppParameters daemon & $nssm set GiteaRunner AppStdout $runnerDir\stdout.log @@ -472,7 +475,7 @@ function Install-NSSM { & $nssm set GiteaRunner AppStopMethodSkip 6 & $nssm set GiteaRunner AppStopMethodConsole 1000 & $nssm set GiteaRunner AppThrottle 5000 - & $nssm set GiteaRunner ObjectName $username $password + & $nssm set GiteaRunner ObjectName $username $password & $nssm start GiteaRunner } @@ -493,8 +496,8 @@ function Install-Runner() { if($MetadataURL -eq ""){ Throw "missing metadata URL" } - $runnerDir = "C:\actions-runner" - $runnerExecutable = Join-Path $runnerDir "gitea-runner.exe" + $runnerDir = "C:\actions-runner" + $runnerExecutable = Join-Path $runnerDir "gitea-runner.exe" # Create user with administrator rights to run service as $userPasswd = Get-RandomString -Length 10 @@ -518,7 +521,7 @@ function Install-Runner() { $result = [GrantSysPrivileges]::GrantPrivilege($sidBytes, ("SeBatchLogonRight", "SeServiceLogonRight")) if ($result -ne 0) { - Throw "Failed to grant privileges" + Throw "Failed to grant privileges" } $bundle = wget -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/system/cert-bundle @@ -533,7 +536,7 @@ function Install-Runner() { if (-not (Test-Path $runnerDir)) { # No cached runner found, proceed to download and extract Update-GarmStatus -CallbackURL $CallbackURL -Message "downloading tools from {{ .DownloadURL }}" - mkdir $runnerDir + mkdir $runnerDir Start-ExecuteWithRetry -ScriptBlock { Invoke-FastWebRequest -Uri "{{ .DownloadURL }}" -OutFile $runnerExecutable @@ -545,7 +548,7 @@ function Install-Runner() { # Ensure runner has full access to actions-runner folder $runnerACL = Get-Acl $runnerDir $runnerACL.SetAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( - $userName, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + $userName, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" ))) Set-Acl -Path $runnerDir -AclObject $runnerAcl @@ -555,18 +558,18 @@ function Install-Runner() { $GithubRegistrationToken = Start-ExecuteWithRetry -ScriptBlock { Invoke-WebRequest -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/runner-registration-token/ } -MaxRetryCount 5 -RetryInterval 5 -RetryMessage "Retrying download of GitHub registration token..." - & $runnerExecutable register --ephemeral --no-interactive --instance "{{ .RepoURL }}" --token $GithubRegistrationToken --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" + & $runnerExecutable register --ephemeral --no-interactive --instance "{{ .RepoURL }}" --token $GithubRegistrationToken --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" if ($LASTEXITCODE) { Throw "Failed to configure runner. Err code $LASTEXITCODE" } $agentInfoFile = Join-Path $runnerDir ".runner" $agentInfo = ConvertFrom-Json (gc -raw $agentInfoFile) Set-SystemInfo -CallbackURL $CallbackURL -RunnerDir $runnerDir -BearerToken $Token - if ((Get-IsAgentMode)) { - Install-GarmAgent -pscreds $pscreds -runnerExecutable $runnerExecutable - } else { - Install-NSSM -username $userName -password $userPasswd -runnerDir $runnerDir - } + if ((Get-IsAgentMode)) { + Install-GarmAgent -pscreds $pscreds -runnerExecutable $runnerExecutable + } else { + Install-NSSM -username $userName -password $userPasswd -runnerDir $runnerDir + } Invoke-GarmSuccess -CallbackURL $CallbackURL -Message "runner successfully installed" -AgentID $agentInfo.id } catch { Invoke-GarmFailure -CallbackURL $CallbackURL -Message $_ diff --git a/internal/templates/userdata/github_windows_userdata.tmpl b/internal/templates/userdata/github_windows_userdata.tmpl index dd8aa3107..0d55e6bc6 100644 --- a/internal/templates/userdata/github_windows_userdata.tmpl +++ b/internal/templates/userdata/github_windows_userdata.tmpl @@ -7,82 +7,82 @@ Param( $ErrorActionPreference="Stop" function Start-ExecuteWithRetry { - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] - [ScriptBlock]$ScriptBlock, - [int]$MaxRetryCount=10, - [int]$RetryInterval=3, - [string]$RetryMessage, - [array]$ArgumentList=@() - ) - PROCESS { - $currentErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - $retryCount = 0 - while ($true) { - try { - $res = Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList - $ErrorActionPreference = $currentErrorActionPreference - return $res - } catch [System.Exception] { - $retryCount++ - - if ($_.Exception -is [System.Net.WebException]) { - $webResponse = $_.Exception.Response + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ScriptBlock]$ScriptBlock, + [int]$MaxRetryCount=10, + [int]$RetryInterval=3, + [string]$RetryMessage, + [array]$ArgumentList=@() + ) + PROCESS { + $currentErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $retryCount = 0 + while ($true) { + try { + $res = Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList + $ErrorActionPreference = $currentErrorActionPreference + return $res + } catch [System.Exception] { + $retryCount++ + + if ($_.Exception -is [System.Net.WebException]) { + $webResponse = $_.Exception.Response # Skip retry on Error: 4XX (e.g. 401 Unauthorized, 404 Not Found etc.) - if ($webResponse -and $webResponse.StatusCode -ge 400 -and $webResponse.StatusCode -lt 500) { - # Skip retry on 4xx errors - Write-Output "Encountered non-retryable error (4xx): $($_.Exception.Message)" - $ErrorActionPreference = $currentErrorActionPreference - throw - } - } - - if ($retryCount -gt $MaxRetryCount) { - $ErrorActionPreference = $currentErrorActionPreference - throw - } else { - if ($RetryMessage) { - Write-Output $RetryMessage - } elseif ($_) { - Write-Output $_ - } - Start-Sleep -Seconds $RetryInterval - } - } - } - } + if ($webResponse -and $webResponse.StatusCode -ge 400 -and $webResponse.StatusCode -lt 500) { + # Skip retry on 4xx errors + Write-Output "Encountered non-retryable error (4xx): $($_.Exception.Message)" + $ErrorActionPreference = $currentErrorActionPreference + throw + } + } + + if ($retryCount -gt $MaxRetryCount) { + $ErrorActionPreference = $currentErrorActionPreference + throw + } else { + if ($RetryMessage) { + Write-Output $RetryMessage + } elseif ($_) { + Write-Output $_ + } + Start-Sleep -Seconds $RetryInterval + } + } + } + } } function Get-RandomString { - [CmdletBinding()] - Param( - [int]$Length=13 - ) - PROCESS { - if($Length -lt 6) { - $Length = 6 - } - $special = @(44, 45, 46, 64) - $numeric = 48..57 - $upper = 65..90 - $lower = 97..122 - - $passwd = [System.Collections.Generic.List[object]](New-object "System.Collections.Generic.List[object]") - for($i=0; $i -lt $Length-4; $i++){ - $c = get-random -input ($special + $numeric + $upper + $lower) - $passwd.Add([char]$c) - } - - $passwd.Add([char](get-random -input $numeric)) - $passwd.Add([char](get-random -input $special)) - $passwd.Add([char](get-random -input $upper)) - $passwd.Add([char](get-random -input $lower)) - - $Random = New-Object Random - return [string]::join("",($passwd|Sort-Object {$Random.Next()})) - } + [CmdletBinding()] + Param( + [int]$Length=13 + ) + PROCESS { + if($Length -lt 6) { + $Length = 6 + } + $special = @(44, 45, 46, 64) + $numeric = 48..57 + $upper = 65..90 + $lower = 97..122 + + $passwd = [System.Collections.Generic.List[object]](New-object "System.Collections.Generic.List[object]") + for($i=0; $i -lt $Length-4; $i++){ + $c = get-random -input ($special + $numeric + $upper + $lower) + $passwd.Add([char]$c) + } + + $passwd.Add([char](get-random -input $numeric)) + $passwd.Add([char](get-random -input $special)) + $passwd.Add([char](get-random -input $upper)) + $passwd.Add([char](get-random -input $lower)) + + $Random = New-Object Random + return [string]::join("",($passwd|Sort-Object {$Random.Next()})) + } } Add-Type -TypeDefinition @" @@ -92,87 +92,87 @@ using System.Text; public class GrantSysPrivileges { - [StructLayout(LayoutKind.Sequential)] - public struct LSA_UNICODE_STRING - { - public ushort Length; - public ushort MaximumLength; - public IntPtr Buffer; - } - - [StructLayout(LayoutKind.Sequential)] - public struct LSA_OBJECT_ATTRIBUTES - { - public int Length; - public IntPtr RootDirectory; - public IntPtr ObjectName; - public uint Attributes; - public IntPtr SecurityDescriptor; - public IntPtr SecurityQualityOfService; - } - - [DllImport("advapi32.dll", SetLastError=true)] - public static extern uint LsaOpenPolicy( - ref LSA_UNICODE_STRING SystemName, - ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, - uint DesiredAccess, - out IntPtr PolicyHandle - ); - - [DllImport("advapi32.dll", SetLastError=true)] - public static extern uint LsaAddAccountRights( - IntPtr PolicyHandle, - IntPtr AccountSid, - LSA_UNICODE_STRING[] UserRights, - uint CountOfRights - ); - - [DllImport("advapi32.dll")] - public static extern uint LsaClose(IntPtr PolicyHandle); - - [DllImport("advapi32.dll")] - public static extern uint LsaNtStatusToWinError(uint status); - - public const uint POLICY_ALL_ACCESS = 0x00F0FFF; - - public static uint GrantPrivilege(byte[] sid, string[] rights) - { - LSA_OBJECT_ATTRIBUTES loa = new LSA_OBJECT_ATTRIBUTES(); - LSA_UNICODE_STRING systemName = new LSA_UNICODE_STRING(); - - IntPtr policyHandle; - uint result = LsaOpenPolicy(ref systemName, ref loa, POLICY_ALL_ACCESS, out policyHandle); - if (result != 0) - { - return LsaNtStatusToWinError(result); - } - - LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[rights.Length]; - for (int i = 0; i < rights.Length; i++) - { - byte[] bytes = Encoding.Unicode.GetBytes(rights[i]); - IntPtr ptr = Marshal.AllocHGlobal(bytes.Length); - Marshal.Copy(bytes, 0, ptr, bytes.Length); - - userRights[i].Buffer = ptr; - userRights[i].Length = (ushort)bytes.Length; - userRights[i].MaximumLength = (ushort)(bytes.Length); - } - - IntPtr sidPtr = Marshal.AllocHGlobal(sid.Length); - Marshal.Copy(sid, 0, sidPtr, sid.Length); - - result = LsaAddAccountRights(policyHandle, sidPtr, userRights, (uint)rights.Length); - LsaClose(policyHandle); - - foreach (var right in userRights) - { - Marshal.FreeHGlobal(right.Buffer); - } - Marshal.FreeHGlobal(sidPtr); - - return LsaNtStatusToWinError(result); - } + [StructLayout(LayoutKind.Sequential)] + public struct LSA_UNICODE_STRING + { + public ushort Length; + public ushort MaximumLength; + public IntPtr Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LSA_OBJECT_ATTRIBUTES + { + public int Length; + public IntPtr RootDirectory; + public IntPtr ObjectName; + public uint Attributes; + public IntPtr SecurityDescriptor; + public IntPtr SecurityQualityOfService; + } + + [DllImport("advapi32.dll", SetLastError=true)] + public static extern uint LsaOpenPolicy( + ref LSA_UNICODE_STRING SystemName, + ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, + uint DesiredAccess, + out IntPtr PolicyHandle + ); + + [DllImport("advapi32.dll", SetLastError=true)] + public static extern uint LsaAddAccountRights( + IntPtr PolicyHandle, + IntPtr AccountSid, + LSA_UNICODE_STRING[] UserRights, + uint CountOfRights + ); + + [DllImport("advapi32.dll")] + public static extern uint LsaClose(IntPtr PolicyHandle); + + [DllImport("advapi32.dll")] + public static extern uint LsaNtStatusToWinError(uint status); + + public const uint POLICY_ALL_ACCESS = 0x00F0FFF; + + public static uint GrantPrivilege(byte[] sid, string[] rights) + { + LSA_OBJECT_ATTRIBUTES loa = new LSA_OBJECT_ATTRIBUTES(); + LSA_UNICODE_STRING systemName = new LSA_UNICODE_STRING(); + + IntPtr policyHandle; + uint result = LsaOpenPolicy(ref systemName, ref loa, POLICY_ALL_ACCESS, out policyHandle); + if (result != 0) + { + return LsaNtStatusToWinError(result); + } + + LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[rights.Length]; + for (int i = 0; i < rights.Length; i++) + { + byte[] bytes = Encoding.Unicode.GetBytes(rights[i]); + IntPtr ptr = Marshal.AllocHGlobal(bytes.Length); + Marshal.Copy(bytes, 0, ptr, bytes.Length); + + userRights[i].Buffer = ptr; + userRights[i].Length = (ushort)bytes.Length; + userRights[i].MaximumLength = (ushort)(bytes.Length); + } + + IntPtr sidPtr = Marshal.AllocHGlobal(sid.Length); + Marshal.Copy(sid, 0, sidPtr, sid.Length); + + result = LsaAddAccountRights(policyHandle, sidPtr, userRights, (uint)rights.Length); + LsaClose(policyHandle); + + foreach (var right in userRights) + { + Marshal.FreeHGlobal(right.Buffer); + } + Marshal.FreeHGlobal(sidPtr); + + return LsaNtStatusToWinError(result); + } } "@ -Language CSharp @@ -219,6 +219,9 @@ function Invoke-FastWebRequest { foreach ($k in $Headers.Keys){ $client.DefaultRequestHeaders.Add($k, $Headers[$k]) } + if (-not $Headers.ContainsKey('User-Agent')) { + $client.DefaultRequestHeaders.Add('User-Agent', 'Invoke-FastWebRequest/1.0') + } $task = $client.GetStreamAsync($Uri) $response = $task.Result if($task.IsFaulted) { @@ -335,46 +338,46 @@ function Invoke-GarmFailure() { } function Set-SystemInfo { - [CmdletBinding()] - param ( - [parameter(Mandatory=$true)] - [string]$CallbackURL, - [parameter(Mandatory=$true)] - [string]$RunnerDir, + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [string]$CallbackURL, [parameter(Mandatory=$true)] - [string]$BearerToken - ) - - # Construct the path to the .runner file - $agentInfoFile = Join-Path $RunnerDir ".runner" - - # Read and parse the JSON content from the .runner file - $agentInfo = ConvertFrom-Json (Get-Content -Raw -Path $agentInfoFile) - $AgentId = $agentInfo.agent_id - - # Retrieve OS information - $osInfo = Get-WmiObject -Class Win32_OperatingSystem - $osName = $osInfo.Caption - $osVersion = $osInfo.Version - - # Strip status from the callback URL - if ($CallbackUrl -match '^(.*)/status(/)?$') { - $CallbackUrl = $matches[1] - } - - $SysInfoUrl = "$CallbackUrl/system-info/" - $Payload = @{ - os_name = $OSName - os_version = $OSVersion - agent_id = $AgentId - } | ConvertTo-Json - - # Send the POST request - try { - Invoke-RestMethod -Uri $SysInfoUrl -Method Post -Body $Payload -ContentType 'application/json' -Headers @{ 'Authorization' = "Bearer $BearerToken" } -ErrorAction Stop - } catch { - Write-Output "Failed to send the system information." - } + [string]$RunnerDir, + [parameter(Mandatory=$true)] + [string]$BearerToken + ) + + # Construct the path to the .runner file + $agentInfoFile = Join-Path $RunnerDir ".runner" + + # Read and parse the JSON content from the .runner file + $agentInfo = ConvertFrom-Json (Get-Content -Raw -Path $agentInfoFile) + $AgentId = $agentInfo.agent_id + + # Retrieve OS information + $osInfo = Get-WmiObject -Class Win32_OperatingSystem + $osName = $osInfo.Caption + $osVersion = $osInfo.Version + + # Strip status from the callback URL + if ($CallbackUrl -match '^(.*)/status(/)?$') { + $CallbackUrl = $matches[1] + } + + $SysInfoUrl = "$CallbackUrl/system-info/" + $Payload = @{ + os_name = $OSName + os_version = $OSVersion + agent_id = $AgentId + } | ConvertTo-Json + + # Send the POST request + try { + Invoke-RestMethod -Uri $SysInfoUrl -Method Post -Body $Payload -ContentType 'application/json' -Headers @{ 'Authorization' = "Bearer $BearerToken" } -ErrorAction Stop + } catch { + Write-Output "Failed to send the system information." + } } $CallbackURL="{{.CallbackURL}}" @@ -383,42 +386,42 @@ if (!($CallbackURL -match "^(.*)/status(/)?$")) { } $GHRunnerGroup = "{{.GitHubRunnerGroup}}" function Get-IsAgentMode { - return {{ if .AgentMode }}$true{{ else }}$false{{ end }} + return {{ if .AgentMode }}$true{{ else }}$false{{ end }} } function Get-AgentURL { - return "{{ .AgentURL }}" + return "{{ .AgentURL }}" } function Get-AgentToken { - return "{{ .AgentToken }}" + return "{{ .AgentToken }}" } function Get-AgentDownloadURL { - return "{{ .AgentDownloadURL }}" + return "{{ .AgentDownloadURL }}" } function Get-AgentShellEnabled { - return "{{ .AgentShell }}" + return "{{ .AgentShell }}" } function Install-GarmAgent { - [CmdletBinding()] - param ( - [parameter(Mandatory=$true)] - [System.Management.Automation.PSCredential]$pscreds - ) - Update-GarmStatus -Message "Agent mode is enabled" -CallbackURL $CallbackURL | Out-Null - $agentDir = "C:\garm-agent" - mkdir -Force $agentDir - $agentURL = Get-AgentURL - $agentDownloadURL = Get-AgentDownloadURL - $agentToken = Get-AgentToken - $agentDownloadHeaders=@{ - "Authorization"="Bearer $Token" - } - $shellEnabled = Get-AgentShellEnabled - Set-Content "$agentDir\garm-agent.toml" @" + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [System.Management.Automation.PSCredential]$pscreds + ) + Update-GarmStatus -Message "Agent mode is enabled" -CallbackURL $CallbackURL | Out-Null + $agentDir = "C:\garm-agent" + mkdir -Force $agentDir + $agentURL = Get-AgentURL + $agentDownloadURL = Get-AgentDownloadURL + $agentToken = Get-AgentToken + $agentDownloadHeaders=@{ + "Authorization"="Bearer $Token" + } + $shellEnabled = Get-AgentShellEnabled + Set-Content "$agentDir\garm-agent.toml" @" server_url = "$agentURL" log_file = "C:/garm-agent/garm-agent.log" shell = "cmd.exe" @@ -429,17 +432,17 @@ runner_cmdline = ["C:\\Windows\\system32\\cmd.exe", "/C", "C:\\actions-runner\\r state_db_path = "C:/garm-agent/agent-state.db" "@ - Update-GarmStatus -Message "Downloading agent from: $agentDownloadURL" -CallbackURL $CallbackURL | Out-Null - Start-ExecuteWithRetry -ScriptBlock { - Invoke-FastWebRequest -Headers $agentDownloadHeaders -Uri "$agentDownloadURL" -OutFile $agentDir\garm-agent.exe - } -MaxRetryCount 5 -RetryInterval 5 -RetryMessage "Retrying download of runner..." - - try { - New-Service -Name garm-agent -BinaryPathName "$agentDir\garm-agent.exe daemon --config $agentDir\garm-agent.toml" -DisplayName "garm-agent" -Description "GARM agent" -StartupType Automatic -Credential $pscreds - } catch { - Invoke-GarmFailure -CallbackURL $CallbackURL -Message "failed to set up garm agent $_" - } - Start-Service garm-agent + Update-GarmStatus -Message "Downloading agent from: $agentDownloadURL" -CallbackURL $CallbackURL | Out-Null + Start-ExecuteWithRetry -ScriptBlock { + Invoke-FastWebRequest -Headers $agentDownloadHeaders -Uri "$agentDownloadURL" -OutFile $agentDir\garm-agent.exe + } -MaxRetryCount 5 -RetryInterval 5 -RetryMessage "Retrying download of runner..." + + try { + New-Service -Name garm-agent -BinaryPathName "$agentDir\garm-agent.exe daemon --config $agentDir\garm-agent.toml" -DisplayName "garm-agent" -Description "GARM agent" -StartupType Automatic -Credential $pscreds + } catch { + Invoke-GarmFailure -CallbackURL $CallbackURL -Message "failed to set up garm agent $_" + } + Start-Service garm-agent } function Install-Runner() { @@ -475,7 +478,7 @@ function Install-Runner() { $result = [GrantSysPrivileges]::GrantPrivilege($sidBytes, ("SeBatchLogonRight", "SeServiceLogonRight")) if ($result -ne 0) { - Throw "Failed to grant privileges" + Throw "Failed to grant privileges" } $bundle = wget -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/system/cert-bundle @@ -515,7 +518,7 @@ function Install-Runner() { # Ensure runner has full access to actions-runner folder $runnerACL = Get-Acl $runnerDir $runnerACL.SetAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( - $userName, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + $userName, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" ))) Set-Acl -Path $runnerDir -AclObject $runnerAcl @@ -538,12 +541,12 @@ function Install-Runner() { Update-GarmStatus -CallbackURL $CallbackURL -Message "Creating system service" $SVC_NAME=(gc -raw $serviceNameFile) - if (!(Get-IsAgentMode)) { - New-Service -Name "$SVC_NAME" -BinaryPathName "C:\actions-runner\bin\RunnerService.exe" -DisplayName "$SVC_NAME" -Description "GitHub Actions Runner ($SVC_NAME)" -StartupType Automatic -Credential $pscreds - Start-Service "$SVC_NAME" - } else { - Install-GarmAgent $pscreds - } + if (!(Get-IsAgentMode)) { + New-Service -Name "$SVC_NAME" -BinaryPathName "C:\actions-runner\bin\RunnerService.exe" -DisplayName "$SVC_NAME" -Description "GitHub Actions Runner ($SVC_NAME)" -StartupType Automatic -Credential $pscreds + Start-Service "$SVC_NAME" + } else { + Install-GarmAgent $pscreds + } Set-SystemInfo -CallbackURL $CallbackURL -RunnerDir $runnerDir -BearerToken $Token Update-GarmStatus -Message "runner successfully installed" -CallbackURL $CallbackURL -Status "idle" | Out-Null @@ -551,23 +554,23 @@ function Install-Runner() { $GithubRegistrationToken = Start-ExecuteWithRetry -ScriptBlock { Invoke-WebRequest -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/runner-registration-token/ } -MaxRetryCount 5 -RetryInterval 5 -RetryMessage "Retrying download of GitHub registration token..." - $argList = @{ - "--unattended" = $null; - "--url" = "{{ .RepoURL }}"; - "--token" = $GithubRegistrationToken; - "--name" = "{{ .RunnerName }}"; - "--labels" = "{{ .RunnerLabels }}" - "--no-default-labels" = $null; - "--ephemeral" = $null; - } - {{- if .GitHubRunnerGroup }} - $argList["--runnergroup"] = {{.GitHubRunnerGroup}} - {{- end }} - if (!(Get-IsAgentMode)) { - $argList["--runasservice"] = $null - $argList["--windowslogonaccount"] = "$userName" - $argList["--windowslogonpassword"] = $userPasswd - } + $argList = @{ + "--unattended" = $null; + "--url" = "{{ .RepoURL }}"; + "--token" = $GithubRegistrationToken; + "--name" = "{{ .RunnerName }}"; + "--labels" = "{{ .RunnerLabels }}" + "--no-default-labels" = $null; + "--ephemeral" = $null; + } + {{- if .GitHubRunnerGroup }} + $argList["--runnergroup"] = {{.GitHubRunnerGroup}} + {{- end }} + if (!(Get-IsAgentMode)) { + $argList["--runasservice"] = $null + $argList["--windowslogonaccount"] = "$userName" + $argList["--windowslogonpassword"] = $userPasswd + } ./config.cmd @argList if ($LASTEXITCODE) { @@ -576,9 +579,9 @@ function Install-Runner() { $agentInfoFile = Join-Path $runnerDir ".runner" $agentInfo = ConvertFrom-Json (gc -raw $agentInfoFile) Set-SystemInfo -CallbackURL $CallbackURL -RunnerDir $runnerDir -BearerToken $Token - if ((Get-IsAgentMode)) { - Install-GarmAgent $pscreds - } + if ((Get-IsAgentMode)) { + Install-GarmAgent $pscreds + } Invoke-GarmSuccess -CallbackURL $CallbackURL -Message "runner successfully installed" -AgentID $agentInfo.agentId {{- end }} } catch {