| @@ -0,0 +1,26 @@ | |||||
| <?xml version="1.0"?> | |||||
| <?component error="true" debug="false"?> | |||||
| <component> | |||||
| <registration | |||||
| description="WscMvc HomeController" | |||||
| progid="WscMvc.HomeController" | |||||
| version="1.00" | |||||
| classid="{87488446-60BE-4068-8368-0B709BB68F3F}"> | |||||
| </registration> | |||||
| <public> | |||||
| <method name="Hello"> | |||||
| <parameter name="body"/> | |||||
| </method> | |||||
| </public> | |||||
| <script language="VBScript"> | |||||
| <![CDATA[ | |||||
| Option Explicit | |||||
| Sub Hello(body) | |||||
| body = "Hello from WSC-MVC!" | |||||
| End Sub | |||||
| ]]> | |||||
| </script> | |||||
| </component> | |||||
| @@ -0,0 +1,43 @@ | |||||
| <%@ Language="VBScript" %> | |||||
| <% Option Explicit %> | |||||
| <% | |||||
| Dim route, app, statusLine, contentType, body | |||||
| route = Request.QueryString("route") | |||||
| Set app = Nothing | |||||
| On Error Resume Next | |||||
| Set app = Server.CreateObject("WscMvc.Application") | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| On Error Goto 0 | |||||
| Response.Status = "500 Internal Server Error" | |||||
| Response.ContentType = "text/plain; charset=utf-8" | |||||
| Response.Write "Internal Server Error" | |||||
| Response.End | |||||
| End If | |||||
| On Error Goto 0 | |||||
| statusLine = "" | |||||
| contentType = "" | |||||
| body = "" | |||||
| On Error Resume Next | |||||
| app.Run route, statusLine, contentType, body | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| On Error Goto 0 | |||||
| Set app = Nothing | |||||
| Response.Status = "500 Internal Server Error" | |||||
| Response.ContentType = "text/plain; charset=utf-8" | |||||
| Response.Write "Internal Server Error" | |||||
| Response.End | |||||
| End If | |||||
| On Error Goto 0 | |||||
| Response.Status = statusLine | |||||
| Response.ContentType = contentType | |||||
| Response.Write body | |||||
| Set app = Nothing | |||||
| %> | |||||
| @@ -0,0 +1,69 @@ | |||||
| <?xml version="1.0"?> | |||||
| <?component error="true" debug="false"?> | |||||
| <component> | |||||
| <registration | |||||
| description="WscMvc Application bootstrap" | |||||
| progid="WscMvc.Application" | |||||
| version="1.00" | |||||
| classid="{851C7763-1638-42FE-A166-BF3DD3A96A88}"> | |||||
| </registration> | |||||
| <public> | |||||
| <method name="Run"> | |||||
| <parameter name="pathInfo"/> | |||||
| <parameter name="statusLine"/> | |||||
| <parameter name="contentType"/> | |||||
| <parameter name="body"/> | |||||
| </method> | |||||
| </public> | |||||
| <script language="VBScript"> | |||||
| <![CDATA[ | |||||
| Option Explicit | |||||
| ' Primitive-only contract: scalars in, scalars out by reference. | |||||
| ' No ASP intrinsic objects (Request/Response/Server/Session) are referenced here. | |||||
| Sub Run(pathInfo, statusLine, contentType, body) | |||||
| Dim ctrl, helloBody | |||||
| If pathInfo = "/hello" Then | |||||
| Set ctrl = Nothing | |||||
| On Error Resume Next | |||||
| Set ctrl = CreateObject("WscMvc.HomeController") | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| On Error Goto 0 | |||||
| statusLine = "500 Internal Server Error" | |||||
| contentType = "text/plain; charset=utf-8" | |||||
| body = "Internal Server Error" | |||||
| Exit Sub | |||||
| End If | |||||
| On Error Goto 0 | |||||
| helloBody = "" | |||||
| On Error Resume Next | |||||
| ctrl.Hello helloBody | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| On Error Goto 0 | |||||
| statusLine = "500 Internal Server Error" | |||||
| contentType = "text/plain; charset=utf-8" | |||||
| body = "Internal Server Error" | |||||
| Set ctrl = Nothing | |||||
| Exit Sub | |||||
| End If | |||||
| On Error Goto 0 | |||||
| statusLine = "200 OK" | |||||
| contentType = "text/html; charset=utf-8" | |||||
| body = helloBody | |||||
| Set ctrl = Nothing | |||||
| Else | |||||
| statusLine = "404 Not Found" | |||||
| contentType = "text/plain; charset=utf-8" | |||||
| body = "Not Found" | |||||
| End If | |||||
| End Sub | |||||
| ]]> | |||||
| </script> | |||||
| </component> | |||||
| @@ -0,0 +1,40 @@ | |||||
| # WSC-MVC — Architecture (as implemented through M1) | |||||
| ## Request flow | |||||
| ``` | |||||
| GET /hello | |||||
| -> IIS URL Rewrite rule "WscMvc-Hello" (^hello/?$) | |||||
| -> /Default.asp?route=/hello | |||||
| -> Server.CreateObject("WscMvc.Application") | |||||
| -> Application.Run("/hello", statusLine, contentType, body) [Framework/Application.wsc] | |||||
| -> CreateObject("WscMvc.HomeController") | |||||
| -> HomeController.Hello(body) [Controllers/HomeController.wsc] | |||||
| -> Default.asp sets Response.Status/ContentType, writes body | |||||
| ``` | |||||
| ## Component boundary contract | |||||
| - `Default.asp` is the only file that touches ASP intrinsic objects (`Request`, `Response`, `Server`). It contains no business logic — only reading the `route` query parameter, invoking `WscMvc.Application`, and writing the response. | |||||
| - `Framework/Application.wsc` and `Controllers/HomeController.wsc` never reference `Request`/`Response`/`Server`/`Session`. All data crosses the ASP-to-WSC and WSC-to-WSC boundaries as VBScript scalars (strings), passed by VBScript's default ByRef semantics for "out" values. See `docs/DECISIONS.md` for why this sidesteps SPEC §15's open question about passing ASP intrinsics into WSC. | |||||
| - Routing in M1 is a single hardcoded `If pathInfo = "/hello"` check inside `Application.Run`. This is intentionally minimal and will be replaced by the explicit allowlisted route table in M3 (`Router.wsc`); do not extend it ad hoc before that milestone. | |||||
| ## COM identity | |||||
| | Component | ProgID | CLSID | | |||||
| |---|---|---| | |||||
| | `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` | | |||||
| | `Controllers/HomeController.wsc` | `WscMvc.HomeController` | `{87488446-60BE-4068-8368-0B709BB68F3F}` | | |||||
| CLSIDs are fixed at creation and must never be recycled for a different component (AGENTS.md). | |||||
| ## IIS site (test host: win2025test, 100.127.62.31) | |||||
| - Site: `WscMvc`, binding `*:8090`, physical path `C:\Projects\wsc-mvc`. | |||||
| - App pool: `WscMvc`, 64-bit, no managed code. | |||||
| - `web.config` hides `Framework/`, `Controllers/`, `tests/`, `tools/`, `docs/` (path-segment based, anywhere in the tree) and denies `.wsc/.vbs/.ps1/.md` by extension. | |||||
| - Default document is `Default.asp`; `/hello` is served via a rewrite rule, not the default document. | |||||
| ## Deferred to later milestones (do not implement early) | |||||
| Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M2+: request/response context object, central error mapping, explicit route table, HTML views/templates, ADODB, auth, logging. M1's error handling is intentionally local (`Default.asp` and `Application.wsc` each guard their own `CreateObject`/method-call boundary) rather than centralized. | |||||
| @@ -0,0 +1,37 @@ | |||||
| # WSC-MVC — Decisions and Experiment Log | |||||
| ## M0 — Environment inventory (2026-09-19, win2025test VM, 100.127.62.31) | |||||
| Host: Windows Server 2025 Standard, build 10.0.26100, 64-bit, AMD Ryzen 5 1500X (4c). | |||||
| Verified via SSH (key-based, `Administrator`) + PowerShell 5.1. | |||||
| - IIS: installed (`Web-Server`) with `Web-ASP`, `Web-CGI`, `Web-ISAPI-Ext`, `Web-ISAPI-Filter`, management tools/console/scripting tools all Installed. | |||||
| - URL Rewrite module: present (`%SystemRoot%\system32\inetsrv\rewrite.dll`, registered as `RewriteModule` in global modules). | |||||
| - WebAdministration PowerShell module: available (v1.0.0.0). | |||||
| - WSC/COM runtime: `scrobj.dll` present in both `System32` and `SysWOW64`. | |||||
| - `regsvr32.exe`, `cscript.exe`, `wscript.exe`: present at standard `C:\WINDOWS\system32` paths. | |||||
| - Existing app pools are all 64-bit (`enable32BitAppOnWin64 = False`); no 32-bit requirement observed. WSC-MVC will target 64-bit and re-verify if this changes. | |||||
| - Existing sites: `Default Web Site` (`*:80`), `AspClassicUnifiedFramework` (`*:8080`, unrelated pre-existing project). No port conflict for a new dedicated site on `*:8090`. | |||||
| - Disk: `C:` only (106 GB, ~80 GB free). No `D:`/`E:`/`F:` volumes despite drive letters being visible (0 bytes each — not usable). Project deployed under `C:\Projects\wsc-mvc`. | |||||
| - Firewall: `World Wide Web Services (HTTP Traffic-In)` inbound rule enabled. | |||||
| - HTTP reachability: VM only reachable over the private Tailscale tailnet (100.127.62.31); no public exposure. | |||||
| No blocking gaps found for M0/M1. Proceeding to M1. | |||||
| ## Open question #1 — Can WSC receive ASP intrinsic objects across the IIS/COM boundary? | |||||
| **Decision: not attempted for M1.** Per SPEC and AGENTS.md hard rules, business logic must never reference ASP `Request`/`Response`/`Server`/`Session` anyway, so this is avoided by design rather than tested as a compatibility question. `Application.Run` and `HomeController.Hello` use a **primitive-only, ByRef-out contract**: all parameters are strings (`pathInfo` in; `statusLine`, `contentType`, `body` out via VBScript's default ByRef parameter passing — no `ByVal`/`ByRef` keywords are available in WSC `<public>` XML, so behavior follows the `Sub`/`Function` signature in the script block). `Default.asp` owns all ASP host object access and is the only file that touches `Request`/`Response`/`Server`. | |||||
| This resolves the open question pragmatically: no experiment needed because the architecture never crosses that boundary. Revisit only if a future milestone needs to pass richer data (e.g. multi-value POST bodies) — prefer additional scalar out-params or a `Scripting.Dictionary` (a standard COM component, not an ASP intrinsic) over passing ASP objects into a WSC. | |||||
| ## Registration tooling | |||||
| `regsvr32.exe /s "<path>.wsc"` registers a Windows Script Component directly (scrobj.dll is the underlying handler associated with the `.wsc` extension). Verified working for both `Application.wsc` and `HomeController.wsc` on this host — see `docs/TEST-RESULTS.md`. `/s /u` unregisters. Both scripts require elevation (registry write); the deployment/registration step is intentionally separate from the request-handling pipeline per SPEC §10 and AGENTS.md. | |||||
| ## Static/source protection approach | |||||
| Used IIS `requestFiltering/hiddenSegments` (blocks any URL path containing `Framework`, `Controllers`, `tests`, `tools`, `docs` as a path segment, anywhere in the tree) plus `requestFiltering/fileExtensions` denylist for `.wsc`, `.vbs`, `.ps1`, `.md`. This keeps `Default.asp` at the project root (matching the SPEC §5 target tree) while denying direct access to source/config/test files, rather than moving `Default.asp` into a separate `public/` webroot. Verified with `tests/Test-Http.ps1` (direct `.wsc` request must return 404). | |||||
| ## Site/port allocation | |||||
| New dedicated site `WscMvc`, app pool `WscMvc` (64-bit, no managed code), binding `*:8090`, physical path `C:\Projects\wsc-mvc`. Chosen to avoid the existing `*:80` and `*:8080` bindings already in use on this VM. | |||||
| @@ -0,0 +1,69 @@ | |||||
| Option Explicit | |||||
| Dim app, ctrl, statusLine, contentType, body, pass | |||||
| pass = True | |||||
| On Error Resume Next | |||||
| Set app = CreateObject("WscMvc.Application") | |||||
| If Err.Number <> 0 Then | |||||
| WScript.Echo "FAIL: could not create WscMvc.Application - " & Err.Description | |||||
| pass = False | |||||
| Err.Clear | |||||
| End If | |||||
| On Error Goto 0 | |||||
| If pass Then | |||||
| statusLine = "" | |||||
| contentType = "" | |||||
| body = "" | |||||
| On Error Resume Next | |||||
| app.Run "/hello", statusLine, contentType, body | |||||
| If Err.Number <> 0 Then | |||||
| WScript.Echo "FAIL: Application.Run raised error on /hello - " & Err.Description | |||||
| pass = False | |||||
| Err.Clear | |||||
| End If | |||||
| On Error Goto 0 | |||||
| End If | |||||
| If pass Then | |||||
| If statusLine = "200 OK" And contentType = "text/html; charset=utf-8" And body = "Hello from WSC-MVC!" Then | |||||
| WScript.Echo "PASS: /hello -> " & statusLine & " | " & contentType & " | " & body | |||||
| Else | |||||
| WScript.Echo "FAIL: unexpected /hello result -> [" & statusLine & "] [" & contentType & "] [" & body & "]" | |||||
| pass = False | |||||
| End If | |||||
| End If | |||||
| If pass Then | |||||
| statusLine = "" | |||||
| contentType = "" | |||||
| body = "" | |||||
| On Error Resume Next | |||||
| app.Run "/does-not-exist", statusLine, contentType, body | |||||
| If Err.Number <> 0 Then | |||||
| WScript.Echo "FAIL: Application.Run raised error on unknown route - " & Err.Description | |||||
| pass = False | |||||
| Err.Clear | |||||
| End If | |||||
| On Error Goto 0 | |||||
| End If | |||||
| If pass Then | |||||
| If statusLine = "404 Not Found" Then | |||||
| WScript.Echo "PASS: unknown route -> " & statusLine | |||||
| Else | |||||
| WScript.Echo "FAIL: unexpected unknown-route result -> [" & statusLine & "]" | |||||
| pass = False | |||||
| End If | |||||
| End If | |||||
| Set app = Nothing | |||||
| If pass Then | |||||
| WScript.Echo "RESULT: ALL PASS" | |||||
| WScript.Quit 0 | |||||
| Else | |||||
| WScript.Echo "RESULT: FAILURE" | |||||
| WScript.Quit 1 | |||||
| End If | |||||
| @@ -0,0 +1,50 @@ | |||||
| [CmdletBinding()] | |||||
| param( | |||||
| [Parameter(Mandatory = $true)] | |||||
| [string]$BaseUrl | |||||
| ) | |||||
| $script:failures = 0 | |||||
| function Report { | |||||
| param($ok, $label, $detail) | |||||
| if ($ok) { | |||||
| Write-Output "PASS: $label" | |||||
| } else { | |||||
| Write-Output "FAIL: $label ($detail)" | |||||
| $script:failures++ | |||||
| } | |||||
| } | |||||
| try { | |||||
| $resp = Invoke-WebRequest -Uri "$BaseUrl/hello" -UseBasicParsing | |||||
| Report ($resp.StatusCode -eq 200) "GET /hello status 200" "got $($resp.StatusCode)" | |||||
| Report ($resp.Headers['Content-Type'] -eq 'text/html; charset=utf-8') "GET /hello content-type" "got $($resp.Headers['Content-Type'])" | |||||
| Report ($resp.Content -eq 'Hello from WSC-MVC!') "GET /hello body" "got '$($resp.Content)'" | |||||
| } catch { | |||||
| Report $false "GET /hello request" $_.Exception.Message | |||||
| } | |||||
| try { | |||||
| $wscResp = Invoke-WebRequest -Uri "$BaseUrl/Framework/Application.wsc" -UseBasicParsing | |||||
| Report $false "GET /Framework/Application.wsc denied" "got $($wscResp.StatusCode)" | |||||
| } catch [System.Net.WebException] { | |||||
| $webResp = $_.Exception.Response | |||||
| if ($webResp) { | |||||
| $code = [int]$webResp.StatusCode | |||||
| Report ($code -eq 404) "GET /Framework/Application.wsc denied" "got $code" | |||||
| } else { | |||||
| Report $false "GET /Framework/Application.wsc denied" $_.Exception.Message | |||||
| } | |||||
| } catch { | |||||
| Report $false "GET /Framework/Application.wsc denied" $_.Exception.Message | |||||
| } | |||||
| Write-Output "---" | |||||
| if ($script:failures -eq 0) { | |||||
| Write-Output "RESULT: ALL PASS" | |||||
| exit 0 | |||||
| } else { | |||||
| Write-Output "RESULT: $($script:failures) FAILURE(S)" | |||||
| exit 1 | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| [CmdletBinding()] | |||||
| param( | |||||
| [string]$ProjectRoot = (Split-Path -Parent $PSScriptRoot) | |||||
| ) | |||||
| $components = @( | |||||
| (Join-Path $ProjectRoot 'Framework\Application.wsc'), | |||||
| (Join-Path $ProjectRoot 'Controllers\HomeController.wsc') | |||||
| ) | |||||
| foreach ($path in $components) { | |||||
| if (-not (Test-Path $path)) { | |||||
| throw "Component not found: $path" | |||||
| } | |||||
| Write-Output "Registering $path" | |||||
| $proc = Start-Process -FilePath 'regsvr32.exe' -ArgumentList "/s `"$path`"" -PassThru -Wait -WindowStyle Hidden | |||||
| if ($proc.ExitCode -ne 0) { | |||||
| throw "regsvr32 failed for $path (exit $($proc.ExitCode))" | |||||
| } | |||||
| } | |||||
| Write-Output "All WSC-MVC components registered." | |||||
| @@ -0,0 +1,23 @@ | |||||
| [CmdletBinding()] | |||||
| param( | |||||
| [string]$ProjectRoot = (Split-Path -Parent $PSScriptRoot) | |||||
| ) | |||||
| $components = @( | |||||
| (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'), | |||||
| (Join-Path $ProjectRoot 'Framework\Application.wsc') | |||||
| ) | |||||
| foreach ($path in $components) { | |||||
| if (-not (Test-Path $path)) { | |||||
| Write-Warning "Component not found, skipping: $path" | |||||
| continue | |||||
| } | |||||
| Write-Output "Unregistering $path" | |||||
| $proc = Start-Process -FilePath 'regsvr32.exe' -ArgumentList "/s /u `"$path`"" -PassThru -Wait -WindowStyle Hidden | |||||
| if ($proc.ExitCode -ne 0) { | |||||
| throw "regsvr32 /u failed for $path (exit $($proc.ExitCode))" | |||||
| } | |||||
| } | |||||
| Write-Output "All WSC-MVC components unregistered." | |||||
| @@ -0,0 +1,36 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <configuration> | |||||
| <system.webServer> | |||||
| <security> | |||||
| <requestFiltering> | |||||
| <hiddenSegments> | |||||
| <add segment="Framework" /> | |||||
| <add segment="Controllers" /> | |||||
| <add segment="tests" /> | |||||
| <add segment="tools" /> | |||||
| <add segment="docs" /> | |||||
| </hiddenSegments> | |||||
| <fileExtensions> | |||||
| <add fileExtension=".wsc" allowed="false" /> | |||||
| <add fileExtension=".vbs" allowed="false" /> | |||||
| <add fileExtension=".ps1" allowed="false" /> | |||||
| <add fileExtension=".md" allowed="false" /> | |||||
| </fileExtensions> | |||||
| </requestFiltering> | |||||
| </security> | |||||
| <rewrite> | |||||
| <rules> | |||||
| <rule name="WscMvc-Hello" stopProcessing="true"> | |||||
| <match url="^hello/?$" /> | |||||
| <action type="Rewrite" url="Default.asp?route=/hello" appendQueryString="false" /> | |||||
| </rule> | |||||
| </rules> | |||||
| </rewrite> | |||||
| <defaultDocument> | |||||
| <files> | |||||
| <clear /> | |||||
| <add value="Default.asp" /> | |||||
| </files> | |||||
| </defaultDocument> | |||||
| </system.webServer> | |||||
| </configuration> | |||||
Powered by TurnKey Linux.