| @@ -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.