浏览代码

M0/M1: environment inventory + first vertical slice (Default.asp, Application.wsc, HomeController.wsc, IIS config, tests)

master
Bottybotsterson 2 周前
父节点
当前提交
7a2efd0fcb
共有 10 个文件被更改,包括 415 次插入 和 0 次删除
  1. +26
    -0
      Controllers/HomeController.wsc
  2. +43
    -0
      Default.asp
  3. +69
    -0
      Framework/Application.wsc
  4. +40
    -0
      docs/ARCHITECTURE.md
  5. +37
    -0
      docs/DECISIONS.md
  6. +69
    -0
      tests/Test-Components.vbs
  7. +50
    -0
      tests/Test-Http.ps1
  8. +22
    -0
      tools/Register-Components.ps1
  9. +23
    -0
      tools/Unregister-Components.ps1
  10. +36
    -0
      web.config

+ 26
- 0
Controllers/HomeController.wsc 查看文件

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

+ 43
- 0
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
%>

+ 69
- 0
Framework/Application.wsc 查看文件

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

+ 40
- 0
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.

+ 37
- 0
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 `<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.

+ 69
- 0
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

+ 50
- 0
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
}

+ 22
- 0
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."

+ 23
- 0
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."

+ 36
- 0
web.config 查看文件

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