diff --git a/Controllers/HomeController.wsc b/Controllers/HomeController.wsc new file mode 100644 index 0000000..c73e8ec --- /dev/null +++ b/Controllers/HomeController.wsc @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/Default.asp b/Default.asp new file mode 100644 index 0000000..571c0f7 --- /dev/null +++ b/Default.asp @@ -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 +%> diff --git a/Framework/Application.wsc b/Framework/Application.wsc new file mode 100644 index 0000000..128e9e5 --- /dev/null +++ b/Framework/Application.wsc @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..50c783a --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 0000000..d78c4bb --- /dev/null +++ b/docs/DECISIONS.md @@ -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 `` 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 ".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. diff --git a/tests/Test-Components.vbs b/tests/Test-Components.vbs new file mode 100644 index 0000000..bfe27eb --- /dev/null +++ b/tests/Test-Components.vbs @@ -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 diff --git a/tests/Test-Http.ps1 b/tests/Test-Http.ps1 new file mode 100644 index 0000000..53dc106 --- /dev/null +++ b/tests/Test-Http.ps1 @@ -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 +} diff --git a/tools/Register-Components.ps1 b/tools/Register-Components.ps1 new file mode 100644 index 0000000..28846df --- /dev/null +++ b/tools/Register-Components.ps1 @@ -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." diff --git a/tools/Unregister-Components.ps1 b/tools/Unregister-Components.ps1 new file mode 100644 index 0000000..a2a434c --- /dev/null +++ b/tools/Unregister-Components.ps1 @@ -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." diff --git a/web.config b/web.config new file mode 100644 index 0000000..010e330 --- /dev/null +++ b/web.config @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +