bambuddy-slicer-cert.cmd 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. @echo off
  2. rem Bambuddy: let Bambu Studio and OrcaSlicer connect to this Bambuddy's
  3. rem virtual printers. Double-click this file and confirm the administrator
  4. rem prompt. It adds this Bambuddy's certificate to the slicers' printer.cer,
  5. rem and installs a task that adds it again after a slicer update.
  6. rem
  7. rem bambuddy-slicer-cert.cmd /uninstall removes that task again
  8. rem
  9. rem The PowerShell code below the marker line does the work.
  10. setlocal
  11. set "BAMBUDDY_SELF=%~f0"
  12. set "BAMBUDDY_ARGS=%*"
  13. rem One line, so cmd.exe has read it all before PowerShell runs: /uninstall
  14. rem deletes the installed copy of this file.
  15. powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$s = [IO.File]::ReadAllText($env:BAMBUDDY_SELF); $m = '#BAMBUDDY' + '_POWERSHELL'; Invoke-Expression $s.Substring($s.IndexOf($m))" && exit /b 0 || exit /b 1
  16. #BAMBUDDY_POWERSHELL
  17. # Made by Bambuddy for the certificate with SHA-256 fingerprint
  18. # __BAMBUDDY_CA_FINGERPRINT__
  19. # Written for Windows PowerShell 5.1, which every Windows 10 and 11 has.
  20. $ErrorActionPreference = 'Stop'
  21. $CaPem = @'
  22. __BAMBUDDY_CA_PEM__
  23. '@
  24. $Fingerprint = '__BAMBUDDY_CA_FINGERPRINT__'
  25. $Id = ($Fingerprint -replace ':', '').Substring(0, 8)
  26. $TaskName = "Bambuddy slicer certificate $Id"
  27. # Under Program Files, where only administrators can create or change
  28. # anything: the task runs the copy kept there as SYSTEM.
  29. $InstallRoot = if ($env:ProgramFiles) { Join-Path $env:ProgramFiles 'Bambuddy slicer certificate' } else { $null }
  30. $InstallDir = if ($InstallRoot) { Join-Path $InstallRoot $Id } else { $null }
  31. $Arguments = " $env:BAMBUDDY_ARGS ".ToLowerInvariant()
  32. $Quiet = $Arguments.Contains(' /quiet ')
  33. $Uninstall = $Arguments.Contains(' /uninstall ')
  34. # Only for Bambuddy's own tests: look for printer.cer under these folders,
  35. # and do nothing that needs Windows (administrator rights, the task).
  36. $TestRoots = $env:BAMBUDDY_CERT_TEST_ROOTS
  37. $SystemBases = @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $env:ProgramW6432) | Where-Object { $_ } |
  38. ForEach-Object { [IO.Path]::GetFullPath($_).TrimEnd('\') + '\' } | Sort-Object -Unique
  39. function Write-Line([string]$Text) {
  40. if ($Quiet) {
  41. $script:Log += $Text + "`r`n"
  42. } else {
  43. Write-Host $Text
  44. }
  45. }
  46. function Get-CertBodies([string]$Text) {
  47. # The base64 text of each certificate, without line breaks: the same
  48. # certificate gives the same string however the file wraps its lines.
  49. $pattern = '-----BEGIN CERTIFICATE-----([A-Za-z0-9+/=\s]+?)-----END CERTIFICATE-----'
  50. foreach ($match in [regex]::Matches($Text, $pattern)) {
  51. [pscustomobject]@{
  52. Body = ($match.Groups[1].Value -replace '\s', '')
  53. Match = $match
  54. }
  55. }
  56. }
  57. function Get-Subject([string]$Body) {
  58. try {
  59. $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(, [Convert]::FromBase64String($Body))
  60. return $cert.Subject
  61. } catch {
  62. return $null
  63. }
  64. }
  65. function Find-CertFiles {
  66. if ($TestRoots) {
  67. foreach ($root in $TestRoots.Split(';')) {
  68. Get-ChildItem -LiteralPath $root -Recurse -Filter printer.cer -File -ErrorAction SilentlyContinue |
  69. ForEach-Object { $_.FullName }
  70. }
  71. return
  72. }
  73. # Only administrators can write below Program Files. The task runs as
  74. # SYSTEM and must never write where a user could plant a link, so it
  75. # changes nothing else; a run by hand also covers other install folders.
  76. $folders = @()
  77. foreach ($base in $SystemBases) {
  78. $folders += Join-Path $base 'Bambu Studio'
  79. $folders += Join-Path $base 'OrcaSlicer'
  80. }
  81. # Installed somewhere else: the uninstall entries say where.
  82. $keys = @(
  83. 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
  84. 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
  85. )
  86. if (-not $Quiet) {
  87. $keys += 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
  88. }
  89. foreach ($key in $keys) {
  90. Get-ItemProperty -Path $key -ErrorAction SilentlyContinue |
  91. Where-Object { $_.PSObject.Properties['DisplayName'] -and $_.DisplayName -match 'Bambu ?Studio|Orca ?Slicer' } |
  92. ForEach-Object {
  93. if ($_.PSObject.Properties['InstallLocation'] -and $_.InstallLocation) {
  94. $folders += $_.InstallLocation.Trim('"')
  95. }
  96. if ($_.PSObject.Properties['DisplayIcon'] -and $_.DisplayIcon) {
  97. $folders += Split-Path -Parent (($_.DisplayIcon -split ',')[0].Trim('"'))
  98. }
  99. }
  100. }
  101. if (-not $Quiet -and $env:LOCALAPPDATA) {
  102. # Installed for this user only.
  103. Get-ChildItem -Path (Join-Path $env:LOCALAPPDATA 'Programs') -Directory -ErrorAction SilentlyContinue |
  104. Where-Object { $_.Name -match 'Bambu ?Studio|Orca ?Slicer' } |
  105. ForEach-Object { $folders += $_.FullName }
  106. }
  107. $folders | Where-Object { $_ } | ForEach-Object {
  108. Join-Path $_ 'resources\cert\printer.cer'
  109. } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
  110. ForEach-Object { [IO.Path]::GetFullPath((Resolve-Path -LiteralPath $_).ProviderPath) } |
  111. Where-Object {
  112. $path = $_
  113. -not $Quiet -or (Test-UnderProgramFiles $path)
  114. } |
  115. Sort-Object -Unique
  116. }
  117. function Test-UnderProgramFiles([string]$Path) {
  118. return [bool]($SystemBases | Where-Object { $Path.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
  119. }
  120. function Test-Link([string]$Path) {
  121. # True when the file or a folder above it is a link (symbolic link,
  122. # junction): writing through it could change a file somewhere else.
  123. $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
  124. while ($item) {
  125. if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { return $true }
  126. $item = if ($item -is [IO.FileInfo]) { $item.Directory } else { $item.Parent }
  127. }
  128. return $false
  129. }
  130. function Update-CertFile([string]$Path) {
  131. $ours = (Get-CertBodies $CaPem | Select-Object -First 1).Body
  132. if (-not $TestRoots -and (Test-Link $Path)) {
  133. Write-Line " skipped, the path contains a link: $Path"
  134. return $true
  135. }
  136. $text = [IO.File]::ReadAllText($Path)
  137. $certs = @(Get-CertBodies $text)
  138. if ($certs | Where-Object { $_.Body -eq $ours }) {
  139. Write-Line " already there: $Path"
  140. return $true
  141. }
  142. try {
  143. # A slicer uses the first certificate with the right name, so an
  144. # earlier issue of this CA (same name, same key, other dates) would
  145. # hide this one: it goes. Not the bare legacy name: older Bambuddy
  146. # installs all share it, and it may be another one's.
  147. $subject = Get-Subject $ours
  148. if ($subject -eq 'CN=Virtual Printer CA') { $subject = $null }
  149. $replaced = $false
  150. $newText = $text
  151. foreach ($cert in ($certs | Sort-Object { $_.Match.Index } -Descending)) {
  152. if ($subject -and (Get-Subject $cert.Body) -eq $subject) {
  153. $newText = $newText.Remove($cert.Match.Index, $cert.Match.Length)
  154. $replaced = $true
  155. }
  156. }
  157. $newline = if ($text.Contains("`r`n")) { "`r`n" } else { "`n" }
  158. $newText = $newText.TrimEnd() + $newline + ($CaPem.Trim() -replace "`r?`n", $newline) + $newline
  159. $backup = "$Path.before-bambuddy"
  160. if (-not (Test-Path -LiteralPath $backup)) {
  161. Copy-Item -LiteralPath $Path -Destination $backup
  162. }
  163. # Written next to the file and then moved over it, so the slicer
  164. # never sees half a file.
  165. $tmp = "$Path.bambuddy-new"
  166. [IO.File]::WriteAllText($tmp, $newText, (New-Object System.Text.UTF8Encoding($false)))
  167. try {
  168. # One step on NTFS, and keeps the file's permissions.
  169. # [NullString]: PowerShell would pass $null as "", which Replace refuses.
  170. [IO.File]::Replace($tmp, $Path, [NullString]::Value)
  171. } catch {
  172. Move-Item -LiteralPath $tmp -Destination $Path -Force
  173. }
  174. if ($replaced) {
  175. Write-Line " replaced an older Bambuddy certificate: $Path"
  176. } else {
  177. Write-Line " added: $Path"
  178. }
  179. return $true
  180. } catch {
  181. Write-Line " FAILED: $Path ($($_.Exception.Message))"
  182. return $false
  183. }
  184. }
  185. function Test-Admin {
  186. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  187. return (New-Object Security.Principal.WindowsPrincipal($identity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
  188. }
  189. function Install-Task {
  190. # The task runs this copy as SYSTEM. It lives under Program Files,
  191. # whose permissions it inherits: only administrators can change it.
  192. New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
  193. if (Test-Link $InstallDir) { throw "$InstallDir contains a link" }
  194. $copy = Join-Path $InstallDir 'bambuddy-slicer-cert.cmd'
  195. if ([IO.Path]::GetFullPath($env:BAMBUDDY_SELF) -ne [IO.Path]::GetFullPath($copy)) {
  196. Copy-Item -LiteralPath $env:BAMBUDDY_SELF -Destination $copy -Force
  197. }
  198. $action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c `"`"$copy`" /quiet`""
  199. $triggers = @(
  200. (New-ScheduledTaskTrigger -AtStartup),
  201. (New-ScheduledTaskTrigger -AtLogOn),
  202. (New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5) -RepetitionInterval (New-TimeSpan -Hours 1) -RepetitionDuration (New-TimeSpan -Days 3650))
  203. )
  204. $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
  205. $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
  206. Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $triggers -Principal $principal -Settings $settings `
  207. -Description 'Adds the Bambuddy certificate to Bambu Studio and OrcaSlicer again after a slicer update.' -Force | Out-Null
  208. }
  209. $script:Log = ''
  210. $exitCode = 0
  211. try {
  212. if (-not $TestRoots -and -not (Test-Admin)) {
  213. Write-Host 'Bambuddy needs administrator rights to change the slicers. Please confirm the prompt.'
  214. $elevated = @{ FilePath = $env:BAMBUDDY_SELF; Verb = 'RunAs' }
  215. if ($env:BAMBUDDY_ARGS) { $elevated.ArgumentList = $env:BAMBUDDY_ARGS }
  216. Start-Process @elevated
  217. exit 0
  218. }
  219. if ($Uninstall) {
  220. Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
  221. Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction SilentlyContinue
  222. if (-not (Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue)) {
  223. Remove-Item -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue
  224. }
  225. Write-Line 'The automatic check is removed. The certificate stays in the slicers.'
  226. } else {
  227. Write-Line 'Bambuddy slicer certificate'
  228. Write-Line ''
  229. $files = @(Find-CertFiles)
  230. $failed = $false
  231. foreach ($file in $files) {
  232. if (-not (Update-CertFile $file)) { $failed = $true }
  233. }
  234. Write-Line ''
  235. if ($files.Count -eq 0) {
  236. Write-Line 'No Bambu Studio or OrcaSlicer installation found.'
  237. $exitCode = 2
  238. } elseif ($failed) {
  239. Write-Line 'Not every slicer could be updated, see above.'
  240. $exitCode = 1
  241. } elseif (-not $TestRoots -and (Get-Process -Name 'bambu-studio', 'orca-slicer' -ErrorAction SilentlyContinue)) {
  242. Write-Line 'Done. A slicer is running: close it completely and start it again.'
  243. } else {
  244. Write-Line 'Done. The slicer uses the certificate the next time it starts.'
  245. }
  246. if (-not $Quiet -and -not $TestRoots) {
  247. Install-Task
  248. Write-Line ''
  249. Write-Line 'Windows now checks this every hour and at every start, and adds the'
  250. Write-Line 'certificate again after a slicer update. To stop that, run this file'
  251. Write-Line 'with /uninstall.'
  252. $elsewhere = @($files | Where-Object { -not (Test-UnderProgramFiles $_) })
  253. if ($elsewhere.Count -gt 0) {
  254. Write-Line ''
  255. Write-Line 'The automatic check only changes slicers under Program Files. Run this'
  256. Write-Line 'file again after updating the slicer in:'
  257. foreach ($file in $elsewhere) { Write-Line " $file" }
  258. }
  259. }
  260. }
  261. } catch {
  262. Write-Line "Error: $($_.Exception.Message)"
  263. $exitCode = 1
  264. }
  265. if ($Quiet -and -not $TestRoots) {
  266. try {
  267. [IO.File]::WriteAllText((Join-Path $InstallDir 'last-run.log'), ((Get-Date).ToString('s') + "`r`n" + $script:Log))
  268. } catch { }
  269. }
  270. if (-not $Quiet -and -not $TestRoots) {
  271. Read-Host 'Press Enter to close' | Out-Null
  272. }
  273. exit $exitCode