- Controllers/SelfTestController.wsc: runs the same checks as
tests/Test-Components.vbs in-process, returns JSON
({ok, checks:[{name,pass,detail}]}) - no PowerShell/cscript/SSH needed
- Application.wsc routes /self-test to it; always HTTP 200 (pass/fail lives
in the JSON body, matching conventional health-check design)
- tests/run-self-test.sh: pure curl + python3 wrapper, verified working
directly from the Linux host with zero Windows tooling
- tests/Test-Http.ps1: now also calls /self-test and folds each check into
its own PASS/FAIL output
- Verified both the happy path and a genuine failure path (deliberately
broke HomeController's registration, confirmed /self-test correctly
reported ok:false with the specific failing check pinpointed, then
re-registered and confirmed full recovery)
- docs updated: ARCHITECTURE.md, DECISIONS.md, TEST-RESULTS.md
master
| @@ -0,0 +1,160 @@ | |||
| <?xml version="1.0"?> | |||
| <?component error="true" debug="false"?> | |||
| <component> | |||
| <registration | |||
| description="WscMvc SelfTestController" | |||
| progid="WscMvc.SelfTestController" | |||
| version="1.00" | |||
| classid="{D2634944-4646-4C55-956E-4C05E7E10904}"> | |||
| </registration> | |||
| <public> | |||
| <method name="RunSelfTest"> | |||
| <parameter name="logDir"/> | |||
| <parameter name="body"/> | |||
| </method> | |||
| </public> | |||
| <script language="VBScript"> | |||
| <![CDATA[ | |||
| Option Explicit | |||
| ' HTTP+JSON test harness, callable as GET /self-test from any CLI (curl, | |||
| ' Invoke-WebRequest, etc.) - no cscript/PowerShell/WSH access to the VM | |||
| ' required. Runs the same checks as tests/Test-Components.vbs, in-process, | |||
| ' against the live registered components. Intentionally returns raw | |||
| ' Err.Description in failure details: unlike Default.asp's own error | |||
| ' handling (which must never leak internals to an arbitrary client), this | |||
| ' route IS the diagnostics surface, so its whole job is to say what broke. | |||
| ' This is a dev/test-milestone tool, not a production data endpoint - | |||
| ' revisit whether it should be gated/removed during M6 hardening. | |||
| Sub RunSelfTest(logDir, body) | |||
| Dim checks, allPass | |||
| checks = "" | |||
| allPass = True | |||
| ' --- RequestContext basic contract --- | |||
| Dim ctx1, ctx1Ok, ctx1Detail | |||
| ctx1Ok = False | |||
| ctx1Detail = "" | |||
| On Error Resume Next | |||
| Set ctx1 = CreateObject("WscMvc.RequestContext") | |||
| If Err.Number = 0 Then ctx1.Initialize "/self-test-a", "GET", logDir | |||
| If Err.Number <> 0 Then | |||
| ctx1Detail = Err.Description | |||
| Err.Clear | |||
| ElseIf ctx1.Path <> "/self-test-a" Then | |||
| ctx1Detail = "Path roundtrip mismatch: got [" & ctx1.Path & "]" | |||
| ElseIf ctx1.HttpMethod <> "GET" Then | |||
| ctx1Detail = "HttpMethod roundtrip mismatch: got [" & ctx1.HttpMethod & "]" | |||
| ElseIf Len(ctx1.CorrelationId) = 0 Then | |||
| ctx1Detail = "CorrelationId was empty" | |||
| ElseIf ctx1.ElapsedMs() < 0 Then | |||
| ctx1Detail = "ElapsedMs was negative" | |||
| Else | |||
| ctx1Ok = True | |||
| End If | |||
| On Error Goto 0 | |||
| checks = AppendCheck(checks, "request_context_contract", ctx1Ok, ctx1Detail) | |||
| allPass = allPass And ctx1Ok | |||
| ' --- Two contexts must not collide on correlation id --- | |||
| Dim ctx2, distinctOk, distinctDetail | |||
| distinctOk = False | |||
| distinctDetail = "" | |||
| On Error Resume Next | |||
| Set ctx2 = CreateObject("WscMvc.RequestContext") | |||
| ctx2.Initialize "/self-test-b", "GET", logDir | |||
| If Err.Number <> 0 Then | |||
| distinctDetail = Err.Description | |||
| Err.Clear | |||
| ElseIf ctx1.CorrelationId = ctx2.CorrelationId Then | |||
| distinctDetail = "Both contexts produced the same CorrelationId: " & ctx1.CorrelationId | |||
| Else | |||
| distinctOk = True | |||
| End If | |||
| On Error Goto 0 | |||
| checks = AppendCheck(checks, "correlation_id_uniqueness", distinctOk, distinctDetail) | |||
| allPass = allPass And distinctOk | |||
| ' --- Application.Run happy path --- | |||
| Dim app, ctxHello, helloStatus, helloType, helloBody, helloOk, helloDetail | |||
| helloOk = False | |||
| helloDetail = "" | |||
| On Error Resume Next | |||
| Set app = CreateObject("WscMvc.Application") | |||
| Set ctxHello = CreateObject("WscMvc.RequestContext") | |||
| ctxHello.Initialize "/hello", "GET", logDir | |||
| helloStatus = "" : helloType = "" : helloBody = "" | |||
| app.Run ctxHello, helloStatus, helloType, helloBody | |||
| If Err.Number <> 0 Then | |||
| helloDetail = Err.Description | |||
| Err.Clear | |||
| ElseIf helloStatus <> "200 OK" Then | |||
| helloDetail = "Expected 200 OK, got [" & helloStatus & "]" | |||
| ElseIf helloType <> "text/html; charset=utf-8" Then | |||
| helloDetail = "Unexpected content type [" & helloType & "]" | |||
| ElseIf helloBody <> "Hello from WSC-MVC!" Then | |||
| helloDetail = "Unexpected body [" & helloBody & "]" | |||
| Else | |||
| helloOk = True | |||
| End If | |||
| On Error Goto 0 | |||
| checks = AppendCheck(checks, "application_run_hello", helloOk, helloDetail) | |||
| allPass = allPass And helloOk | |||
| ' --- Application.Run unknown route (expected 404, not an error) --- | |||
| Dim ctxUnknown, unkStatus, unkType, unkBody, unkOk, unkDetail | |||
| unkOk = False | |||
| unkDetail = "" | |||
| On Error Resume Next | |||
| Set ctxUnknown = CreateObject("WscMvc.RequestContext") | |||
| ctxUnknown.Initialize "/self-test-does-not-exist", "GET", logDir | |||
| unkStatus = "" : unkType = "" : unkBody = "" | |||
| app.Run ctxUnknown, unkStatus, unkType, unkBody | |||
| If Err.Number <> 0 Then | |||
| unkDetail = Err.Description | |||
| Err.Clear | |||
| ElseIf unkStatus <> "404 Not Found" Then | |||
| unkDetail = "Expected 404 Not Found, got [" & unkStatus & "]" | |||
| Else | |||
| unkOk = True | |||
| End If | |||
| On Error Goto 0 | |||
| checks = AppendCheck(checks, "application_run_unknown_route", unkOk, unkDetail) | |||
| allPass = allPass And unkOk | |||
| Set ctx1 = Nothing | |||
| Set ctx2 = Nothing | |||
| Set ctxHello = Nothing | |||
| Set ctxUnknown = Nothing | |||
| Set app = Nothing | |||
| body = "{""ok"":" & LCase(CStr(allPass)) & ",""checks"":[" & checks & "]}" | |||
| End Sub | |||
| Function AppendCheck(checksSoFar, name, pass, detail) | |||
| Dim entry | |||
| entry = "{""name"":""" & JsonEscape(name) & """,""pass"":" & LCase(CStr(pass)) & _ | |||
| ",""detail"":""" & JsonEscape(detail) & """}" | |||
| If Len(checksSoFar) = 0 Then | |||
| AppendCheck = entry | |||
| Else | |||
| AppendCheck = checksSoFar & "," & entry | |||
| End If | |||
| End Function | |||
| Function JsonEscape(s) | |||
| Dim result | |||
| result = s | |||
| result = Replace(result, "\", "\\") | |||
| result = Replace(result, """", "\""") | |||
| result = Replace(result, vbCrLf, "\n") | |||
| result = Replace(result, vbCr, "\n") | |||
| result = Replace(result, vbLf, "\n") | |||
| result = Replace(result, vbTab, "\t") | |||
| JsonEscape = result | |||
| End Function | |||
| ]]> | |||
| </script> | |||
| </component> | |||
| @@ -64,6 +64,46 @@ Sub Run(ctx, statusLine, contentType, body) | |||
| contentType = "text/html; charset=utf-8" | |||
| body = helloBody | |||
| Set ctrl = Nothing | |||
| ElseIf path = "/self-test" Then | |||
| Set ctrl = Nothing | |||
| On Error Resume Next | |||
| Set ctrl = CreateObject("WscMvc.SelfTestController") | |||
| 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" | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| Dim selfTestBody | |||
| selfTestBody = "" | |||
| On Error Resume Next | |||
| ctrl.RunSelfTest ctx.LogDir, selfTestBody | |||
| 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 | |||
| LogOutcome ctx, statusLine | |||
| Exit Sub | |||
| End If | |||
| On Error Goto 0 | |||
| ' Always 200: the self-test HTTP call itself succeeded. Whether the | |||
| ' underlying checks passed is reported in the JSON body's "ok" field | |||
| ' and per-check "pass" fields, not the HTTP status - this matches | |||
| ' conventional health-check endpoint design (5xx is reserved for the | |||
| ' diagnostics mechanism itself being broken, handled above). | |||
| statusLine = "200 OK" | |||
| contentType = "application/json; charset=utf-8" | |||
| body = selfTestBody | |||
| Set ctrl = Nothing | |||
| Else | |||
| ' Expected outcome, not a failure: no matching route yet (M3 adds a real table). | |||
| statusLine = "404 Not Found" | |||
| @@ -29,6 +29,19 @@ GET /hello | |||
| - `RequestContext.Initialize` generates a correlation id from `Fix(Timer)` plus `Scripting.FileSystemObject.GetTempName()` — not `Randomize`/`Rnd()`, which was tried first and shown experimentally to collide when two contexts are created within the same clock tick (see `docs/DECISIONS.md`). | |||
| - `Application.LogOutcome` appends one line per request (timestamp, correlation id, method, path, status line, elapsed ms) to `logs/app.log`, guarded end-to-end by `On Error Resume Next` so a logging failure can never affect the HTTP response. Concurrent writers are serialized with a manual lock-file mutex (`logs/app.log.lock`, via `CreateTextFile(..., OverwriteExisting:=False)`) since no ASP intrinsic locking primitive (`Application.Lock`) is available to code that must not reference ASP intrinsics. The retry budget is deliberately short (see `docs/DECISIONS.md`): logging is explicitly best-effort and may drop lines under heavy concurrency without affecting correctness of the response. | |||
| ## Test harness: GET /self-test (JSON, frontend-agnostic) | |||
| `Controllers/SelfTestController.wsc` (`WscMvc.SelfTestController`) exposes the same checks as `tests/Test-Components.vbs`, but callable over plain HTTP and returning JSON — no PowerShell, cscript, or SSH access to the VM required. `Application.Run` routes `path = "/self-test"` to it. The HTTP status is always `200` if the self-test mechanism itself ran (a broken `SelfTestController`/`Application` still degrades to the normal `500` path); the JSON body's top-level `"ok"` field and each check's `"pass"` field carry the actual test results — this is conventional health-check design (5xx is reserved for the diagnostics mechanism being broken, not for a failed assertion inside it). | |||
| ``` | |||
| curl http://100.127.62.31:8090/self-test | |||
| {"ok":true,"checks":[{"name":"request_context_contract","pass":true,"detail":""}, ...]} | |||
| ``` | |||
| `tests/run-self-test.sh` wraps this in `curl` + `python3 -m json.tool`, runnable from any CLI (this was verified working directly from the Linux OpenClaw host, not just from the VM). `tests/Test-Http.ps1` also calls `/self-test` and folds each check into its own PASS/FAIL output, complementing (not duplicating) its own direct `/hello` and denied-`.wsc`-access checks, which `/self-test` cannot observe about itself since those are IIS-layer/`web.config` concerns, not component-layer ones. | |||
| `SelfTestController`'s error details intentionally include raw `Err.Description` text — unlike every other route, which must never leak internals to an arbitrary caller, this route's entire purpose is diagnostics. It is a dev/test-milestone tool; reconsider whether it should be gated or removed before any production deployment during the M6 hardening pass. | |||
| ## COM identity | |||
| | Component | ProgID | CLSID | | |||
| @@ -36,6 +49,7 @@ GET /hello | |||
| | `Framework/RequestContext.wsc` | `WscMvc.RequestContext` | `{1C36FA55-34DF-4974-94B9-D657389362B2}` | | |||
| | `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` | | |||
| | `Controllers/HomeController.wsc` | `WscMvc.HomeController` | `{87488446-60BE-4068-8368-0B709BB68F3F}` | | |||
| | `Controllers/SelfTestController.wsc` | `WscMvc.SelfTestController` | `{D2634944-4646-4C55-956E-4C05E7E10904}` | | |||
| CLSIDs are fixed at creation and must never be recycled for a different component (AGENTS.md). `Application`'s public `Run` signature changed between M1 and M2 (added a leading `ctx` parameter) under the same CLSID; acceptable because this is active pre-release (v0.1) development with exactly one caller (`Default.asp`, updated in lockstep) — not a claim that live interface changes are safe for a published/external client. | |||
| @@ -63,3 +63,11 @@ Three real defects were found and fixed while building this, all confirmed exper | |||
| ## Investigated and resolved: an inherited `IUSR:(F)` ACE appeared under `logs/` | |||
| While diagnosing the concurrency finding above, `icacls C:\Projects\wsc-mvc\logs` showed an inherited `NT AUTHORITY\IUSR:(I)(F)` (Full Control) entry that wasn't present as an explicit ACE on `C:\Projects\wsc-mvc` or `C:\Projects` themselves. Initially flagged as a possible SPEC §10 "no broad filesystem write privileges" violation (anonymous web identity with Full Control beyond what it needs). Investigated properly rather than left as a guess: a recursive `icacls C:\Projects\wsc-mvc /T` scan shows the `IUSR:(F)` ACE present **only** on `logs/` and files/folders created underneath it during testing (`app.log`, diagnostic marker files, etc.) — confirmed absent on `Framework/`, `Controllers/`, `Default.asp`, `web.config`, `tools/`, `tests/`. Both `C:\Projects` and `C:\Projects\wsc-mvc` carry an inherited `CREATOR OWNER:(I)(OI)(CI)(IO)(F)` ACE (the `(IO)` = inherit-only flag), which is standard NTFS behavior: it doesn't grant CREATOR OWNER anything on the folder itself, but materializes into a concrete Full Control ACE for whichever security principal actually creates each new child file/folder underneath. Since IUSR (impersonated by classic ASP for anonymous requests, per this site's `anonymousAuthentication` config) was the one creating files under `logs/` during testing, it received Full Control on exactly those files it created there — nowhere else. **Resolved, not a defect**: this is expected NTFS ownership semantics, correctly scoped to only the dynamically-created log tree (which is already denied over HTTP via `hiddenSegments`), not a broad grant across the source tree. No remediation needed; no M6 follow-up required for this specific concern. | |||
| ## Test harness: HTTP+JSON self-test endpoint (2026-09-19) | |||
| Daniel asked for the test harness to be "frontend agnostic" — runnable via a plain HTTP/API call from any CLI (not tied to PowerShell/cscript, which require either being on the VM or an SSH hop to it). Added `GET /self-test` (`Controllers/SelfTestController.wsc`), which runs the same checks as `tests/Test-Components.vbs` in-process against the live registered components and returns JSON (`{"ok":bool,"checks":[{"name","pass","detail"}, ...]}`). Verified directly from the Linux OpenClaw host with plain `curl` (no SSH, no PowerShell): a passing run, and a genuine failing run (deliberately removed `WscMvc.HomeController`'s registry entries, confirmed `/self-test` correctly reported `"ok":false` with `application_run_hello` pinpointed and a clear detail message, while every other check still correctly reported `true` — then re-registered and confirmed full recovery). | |||
| Design choice: HTTP status is always `200` when the self-test mechanism itself executes; pass/fail lives in the JSON body (`"ok"` and per-check `"pass"`). A `5xx` is reserved for the diagnostics mechanism itself being broken (e.g. `SelfTestController` fails to instantiate), which still flows through `Application.Run`'s existing central error handling unchanged. This mirrors conventional health-check endpoint design and keeps a clean separation between "the test infrastructure is broken" and "a test found a real problem." | |||
| `SelfTestController` is the first component whose failure details intentionally include raw `Err.Description` — everywhere else in this codebase that would violate SPEC §10 ("avoid... raw exception text to clients"), but a diagnostics-only endpoint's entire job is reporting what broke. Flagged in `docs/ARCHITECTURE.md` for reconsideration (gate or remove) before any production deployment, during the M6 hardening pass — not a concern for the current dev/test milestones. | |||
| @@ -149,6 +149,30 @@ Result: **PASS** — `/hello` returns `500` while unregistered and `200` after r | |||
| - A logging mechanism with guaranteed no-loss-under-load delivery — explicitly deferred to M6 (see `docs/DECISIONS.md`). | |||
| - Formal 400/405 method-not-allowed handling — still waiting on M3's route table. | |||
| ## Test harness: GET /self-test (frontend-agnostic HTTP+JSON) | |||
| Requested by Daniel: the harness should be callable "via an api call" with a "json response," runnable from the CLI over plain HTTP, not tied to any specific frontend/tooling. | |||
| Command (from the Linux OpenClaw host, no SSH/PowerShell/cscript involved): | |||
| ``` | |||
| curl http://100.127.62.31:8090/self-test | |||
| ``` | |||
| Result on a healthy deployment: **PASS** | |||
| ```json | |||
| {"ok":true,"checks":[ | |||
| {"name":"request_context_contract","pass":true,"detail":""}, | |||
| {"name":"correlation_id_uniqueness","pass":true,"detail":""}, | |||
| {"name":"application_run_hello","pass":true,"detail":""}, | |||
| {"name":"application_run_unknown_route","pass":true,"detail":""} | |||
| ]} | |||
| ``` | |||
| Verified the failure-reporting path is real, not just the happy path: deliberately removed `WscMvc.HomeController`'s registry entries, re-ran `curl .../self-test`, got `"ok":false` with `application_run_hello` pinpointed (`"detail":"Expected 200 OK, got [500 Internal Server Error]"`) while the other three checks correctly still reported `true`; confirmed `GET /hello` itself also correctly degraded to `500` at the same time. Re-registered and confirmed both `/self-test` and `/hello` fully recovered. **PASS** | |||
| Also verified `tests/run-self-test.sh` (`curl` + `python3 -m json.tool`, zero Windows tooling dependency) and the updated `tests/Test-Http.ps1` (now folds each `/self-test` check into its own PASS/FAIL output alongside its existing direct `/hello` and denied-`.wsc` checks) both work end to end. **PASS** | |||
| `GET /Controllers/SelfTestController.wsc` (the component's own source file) still correctly denied — `404`, same `hiddenSegments` rule as every other component. **PASS** | |||
| ## M2 gate status: **PASS** | |||
| The SPEC §13 M2 gate ("repeated/concurrent requests and failure cases behave predictably") was run on real Windows/IIS and holds unconditionally for HTTP response correctness. Four real defects were found and fixed while building this milestone (WSC property-syntax limitation, a locale-formatting bug, a `Randomize` collision, and a naive concurrent-logging approach that made things worse before a working lock-file mutex was verified) — see `docs/DECISIONS.md` for full detail on each. Proceeding to M3 (routing) is unblocked. | |||
| @@ -25,6 +25,22 @@ try { | |||
| Report $false "GET /hello request" $_.Exception.Message | |||
| } | |||
| # GET /self-test is the frontend-agnostic JSON harness (Controllers/SelfTestController.wsc) - | |||
| # callable from any CLI via plain HTTP, not just PowerShell/cscript on the VM. This exercises | |||
| # the same component-level checks as tests/Test-Components.vbs, in-process, over HTTP. | |||
| try { | |||
| $selfTestResp = Invoke-WebRequest -Uri "$BaseUrl/self-test" -UseBasicParsing | |||
| Report ($selfTestResp.StatusCode -eq 200) "GET /self-test status 200" "got $($selfTestResp.StatusCode)" | |||
| Report ($selfTestResp.Headers['Content-Type'] -eq 'application/json; charset=utf-8') "GET /self-test content-type" "got $($selfTestResp.Headers['Content-Type'])" | |||
| $selfTestJson = $selfTestResp.Content | ConvertFrom-Json | |||
| foreach ($check in $selfTestJson.checks) { | |||
| Report ([bool]$check.pass) "self-test: $($check.name)" $check.detail | |||
| } | |||
| Report ([bool]$selfTestJson.ok) "GET /self-test overall ok" "detail in individual checks above" | |||
| } catch { | |||
| Report $false "GET /self-test request" $_.Exception.Message | |||
| } | |||
| try { | |||
| $wscResp = Invoke-WebRequest -Uri "$BaseUrl/Framework/Application.wsc" -UseBasicParsing | |||
| Report $false "GET /Framework/Application.wsc denied" "got $($wscResp.StatusCode)" | |||
| @@ -0,0 +1,23 @@ | |||
| #!/usr/bin/env bash | |||
| # Frontend-agnostic test runner: plain curl + a JSON parser, no PowerShell, | |||
| # no cscript, no SSH access to the VM required. Runnable from any CLI that | |||
| # can reach the site over HTTP. | |||
| # | |||
| # Usage: ./run-self-test.sh http://100.127.62.31:8090 | |||
| set -euo pipefail | |||
| BASE_URL="${1:?Usage: run-self-test.sh <base-url>, e.g. http://100.127.62.31:8090}" | |||
| response=$(curl -sf "$BASE_URL/self-test") | |||
| echo "$response" | python3 -m json.tool | |||
| ok=$(echo "$response" | python3 -c 'import json, sys; print("true" if json.load(sys.stdin)["ok"] else "false")') | |||
| if [ "$ok" = "true" ]; then | |||
| echo "RESULT: ALL PASS" | |||
| exit 0 | |||
| else | |||
| echo "RESULT: FAILURE" | |||
| exit 1 | |||
| fi | |||
| @@ -10,7 +10,8 @@ if (-not $ProjectRoot) { | |||
| $components = @( | |||
| (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'), | |||
| (Join-Path $ProjectRoot 'Framework\Application.wsc'), | |||
| (Join-Path $ProjectRoot 'Controllers\HomeController.wsc') | |||
| (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'), | |||
| (Join-Path $ProjectRoot 'Controllers\SelfTestController.wsc') | |||
| ) | |||
| foreach ($path in $components) { | |||
| @@ -15,6 +15,7 @@ if (-not $ProjectRoot) { | |||
| # can never be touched even if it happened to reuse a ProgID string. | |||
| $components = @( | |||
| @{ Path = (Join-Path $ProjectRoot 'Controllers\SelfTestController.wsc'); ProgId = 'WscMvc.SelfTestController'; ClassId = '{D2634944-4646-4C55-956E-4C05E7E10904}' }, | |||
| @{ Path = (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'); ProgId = 'WscMvc.HomeController'; ClassId = '{87488446-60BE-4068-8368-0B709BB68F3F}' }, | |||
| @{ Path = (Join-Path $ProjectRoot 'Framework\Application.wsc'); ProgId = 'WscMvc.Application'; ClassId = '{851C7763-1638-42FE-A166-BF3DD3A96A88}' }, | |||
| @{ Path = (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'); ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' } | |||
| @@ -25,6 +25,10 @@ | |||
| <match url="^hello/?$" /> | |||
| <action type="Rewrite" url="Default.asp?route=/hello" appendQueryString="false" /> | |||
| </rule> | |||
| <rule name="WscMvc-SelfTest" stopProcessing="true"> | |||
| <match url="^self-test/?$" /> | |||
| <action type="Rewrite" url="Default.asp?route=/self-test" appendQueryString="false" /> | |||
| </rule> | |||
| </rules> | |||
| </rewrite> | |||
| <defaultDocument> | |||
Powered by TurnKey Linux.