cleanup: drop CMS files imported via subtree (they stay in MoreThenCms); drop stale bootstrap-manifest.md (per-project metadata from MoreThenCms's bootstrap, does not apply to admin)

This commit is contained in:
2026-05-21 13:48:36 +03:00
parent 85b3ee847f
commit 3cdcdad048
11 changed files with 0 additions and 891 deletions

View File

@@ -1,21 +0,0 @@
---
title: Bootstrap Manifest
type: concept
updated: 2026-05-18
generator: project-bootstrap@1.11.0
---
# Bootstrap Manifest
Skills used to initialize this project's `.wiki/` and `.tasks/` layout, with their versions at install time.
| Skill | Version | Role |
|---|---|---|
| `project-bootstrap` | 1.11.0 | orchestrator |
| `setup-wiki` | 1.0.0 | wiki canonical layout |
| `setup-tasks` | 1.0.0 | tasks canonical layout |
| `project-discipline` | 0.1.1 | cross-project policy |
| `setup-interns` | 0.3.0 | interns MCP server install (one-time, per machine) |
| `using-interns` | 0.2.0 | interns runtime policy + per-session permission grant |
This file is overwritten if `project-bootstrap` is re-run on the same project. For history, use `git log .wiki/concepts/bootstrap-manifest.md`.

View File

@@ -1,92 +0,0 @@
---
title: Admin assets crash — missing root AssetsFolder seed
type: concept
tags: [cms, admin, data-seed, mssql, gotcha, null-check]
sources: [../sources/iis-host-migration-2026-05-19.md]
updated: 2026-05-19
---
# CMS admin assets crash — root AssetsFolder seed
`AssetsJsonViewModelBuilder.cs:22` (admin /admin/assets/.../getList) падает NullReferenceException на любом site, где **root row `Folders` (Discriminator='AssetsFolder', LoweredPath='/', OwnerId=SiteId)** отсутствует. Root создаётся lazy (при first asset upload через admin), и sites которые никогда не использовали admin assets UI — без root.
Связано: [[cms-server-port-leak-fix]] (нашли при verification после port-leak fix), [[recovery-architecture-snapshot]] (был open issue #8).
## Симптом
```
GET /admin/assets/<siteId>/getList?path=
→ 500 NullReferenceException
[NullReferenceException]
MoreThenCms.Admin.ViewModels.Builders.AssetsJsonViewModelBuilder.Build(...)
AssetsJsonViewModelBuilder.cs:22
MoreThenCms.Web.Admin.Controllers.AssetsController.GetList(Guid ownerId, String path)
AssetsController.cs:46
```
В `AssetsController.GetList` (line 46):
```csharp
var model = _assetsViewModelBuilder.Build(
_assetsFoldersService.GetFolderByPath(ExecutionContext, ownerId, path), ...);
```
`GetFolderByPath` (`MoreThenCms\Assets\Services\AssetsFoldersService.cs:47-54`) делает `repo.Find(p => p.LoweredPath == path && p.OwnerId == ownerId)` — возвращает **null** если row нет. `Build` дальше делает `new JValue(model.ParentPath)` без null-check → crash.
Frontend код (`AssetsAppFunc.cs:66-86`) делает proper null-check → HTTP 404. Только admin view-model builder упустил.
## Кого затрагивает (snapshot 2026-05-19)
После recovery: **15 sites без root** (из ~50). Sites где admin assets когда-либо открывался → root есть. Остальные:
- emspb.ru, pilorama98.ru, labtools.pro, kupimknigi.spb.ru, sestech.ru, aquamax.spb.ru, artmone.pro, priemka-kvartiry.ru, profund.spb.ru, ics-artmaterials.com, _voda-indigo.ru
- 4 sites с NULL `PrimaryDomain` (legacy/test data)
Проверочный запрос:
```sql
SELECT s.SiteId, s.PrimaryDomain,
CASE WHEN EXISTS (SELECT 1 FROM Folders f
WHERE f.OwnerId = s.SiteId
AND f.LoweredPath = '/'
AND f.Discriminator = 'AssetsFolder')
THEN 'OK' ELSE 'NO ROOT' END AS RootStatus
FROM Sites s
ORDER BY RootStatus, s.PrimaryDomain;
```
## Fix (применён 2026-05-19)
DB seed — один INSERT для всех missing roots, идемпотентный (`WHERE NOT EXISTS`):
```sql
DECLARE @CreatedById uniqueidentifier = 'E4CC416B-5D13-4757-8EFE-03CBEA15B18C'; -- existing admin user
DECLARE @Inserted TABLE (FolderId uniqueidentifier, OwnerId uniqueidentifier);
INSERT INTO Folders (FolderId, OwnerId, Path, LoweredPath,
DateCreated, UtcDateCreated, CreatedById, Discriminator)
OUTPUT INSERTED.FolderId, INSERTED.OwnerId INTO @Inserted
SELECT NEWID(), s.SiteId, '/', '/',
SYSDATETIME(), SYSUTCDATETIME(), @CreatedById, 'AssetsFolder'
FROM Sites s
WHERE NOT EXISTS (SELECT 1 FROM Folders f
WHERE f.OwnerId = s.SiteId
AND f.LoweredPath = '/'
AND f.Discriminator = 'AssetsFolder');
SELECT FolderId, OwnerId FROM @Inserted; -- save for atomic revert
```
15 rows вставлены. Inserted FolderId/OwnerId сохранены в `.tasks/cms-admin-assets-root-folders-seed.inserted-rows.txt` для atomic revert (`DELETE FROM Folders WHERE FolderId IN (...)`).
После: `/admin/assets/<siteId>/getList?path=` возвращает HTTP 302 → /login для unauth (normal auth path), вместо 500. Authorized users видят empty assets list.
## Долгосрочный TODO (не сделано — нет рабочего build env)
`AssetsJsonViewModelBuilder.Build` должна null-guard'иться — возвращать empty JObject ({parentPath:"", path:"/", folders:[], files:[]}) когда model null. Это **defensive code**, плюс auto-create root в `GetFolderByPath` для root path (как обычно делают админ-репозитории).
Требует recompile `MoreThenCms.Admin.dll` — отложено до восстановления полного build pipeline (gitea/build/registry — см. [[vds-kzntsv-bootstrap]] task).
## Возможно затрагивает ещё (open для следующей сессии)
`Folders` table — polymorphic: `AssetsFolder`, `ImagesFolder` (OwnerId=ThemeId), `ScriptsFolder` (ThemeId), `StylesheetsFolder` (ThemeId). Аналогичная null-falling логика может быть в admin views для Themes (Stylesheets/Scripts/Images). Не проверено, не reported user'ом.
Если будет — same pattern: `SELECT s.SiteId vs Folders root по соответствующему Discriminator` + seed.

View File

@@ -1,125 +0,0 @@
---
title: Web.config rewrite — кодировка и connection-string patterns
type: concept
tags: [iis, webconfig, encoding, powershell, traceback]
sources: [../sources/nas-recovery-session-2026-05-18.md]
updated: 2026-05-19
---
# Web.config rewrite на Windows: encoding pitfall
## Сценарий
После cross-host миграции CMS в VM, нужно переписать connection strings (`192.168.1.10``10.0.2.2` или другой host). Делается через SSH/PowerShell `Get-Content`+`-replace`+`Set-Content`.
## Pitfall: Set-Content без -Encoding
```powershell
# WRONG — на PowerShell 5.1 пишет UTF-16 LE
(Get-Content $f -Raw) -replace 'old', 'new' | Set-Content $f -NoNewline
```
**Симптом** после этого:
- IIS возвращает `HTTP 500.19 - Internal Server Error`, "Файл конфигурации создан в неправильном формате XML", error code `0x8007000d`.
- В детале: первые строки файла показывают артефакты `????????` (UTF-16 в окне UTF-8 ожидаемой проверки).
**Корень:** PowerShell 5.1 (Windows встроенный) `Set-Content` без `-Encoding` использует **default = UTF-16 LE** (Unicode). IIS ожидает UTF-8 (BOM или без BOM).
## Правильный способ
```powershell
$utf8WithBom = New-Object System.Text.UTF8Encoding($true)
$content = Get-Content $file -Raw
$new = $content -replace 'old', 'new'
[System.IO.File]::WriteAllText($file, $new, $utf8WithBom)
```
Или, если хочется без BOM:
```powershell
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($file, $new, $utf8NoBom)
```
ASP.NET / IIS работают и с BOM, и без — но для дефолтного XML конфига Microsoft предпочитает с BOM.
### PowerShell 7+ workaround
В Core: `Set-Content -Encoding utf8` пишет UTF-8 БЕЗ BOM (отличается от PS 5!). `-Encoding utf8BOM`с BOM.
## Connection string patterns в CMS
Нашли в `MoreThenCms.WebUI/Web.config` source (development):
```xml
<add name="MoreThenCmsEntities"
connectionString="Data Source=DESKTOP-XYZ\SQLEXPRESS;Initial Catalog=MoreThenCms;
Integrated Security=True;MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
```
В production (внутри OVA, после восстановления):
```xml
<add name="MoreThenCmsEntities"
connectionString="Data Source=192.168.1.10;Initial Catalog=MoreThenCms;
Integrated Security=False;User Id=snolla;
Password=fXkH4@8O%3pc;MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
```
То есть в prod CMS использует **SQL login `snolla`**, не Integrated Security. Сам логин остался в `master.mdf` после восстановления — никаких дополнительных шагов не понадобилось.
## Mass-edit нескольких сайтов
Под Windows VM было 4 Web.config с одинаковым паттерном:
- `C:\inetpub\wwwroot\MoreThenCms.Web\Web.config`
- `C:\inetpub\wwwroot\Snolla.IdentityManager\Web.config`
- `C:\stayer\stostayer.old\web.config`
- (плюс `C:\stayer\MoreThenCms.Web\` — main stostayer без 192.168.1.10 reference)
Скрипт через SSH в VM:
```powershell
$files = @(
'C:\inetpub\wwwroot\MoreThenCms.Web\Web.config',
'C:\inetpub\wwwroot\Snolla.IdentityManager\Web.config',
'C:\stayer\stostayer.old\web.config'
)
$utf8WithBom = New-Object System.Text.UTF8Encoding($true)
foreach ($f in $files) {
$content = Get-Content $f -Raw
$new = $content -replace [regex]::Escape('192.168.1.10'), '10.0.2.2'
[System.IO.File]::WriteAllText($f, $new, $utf8WithBom)
}
# затем
iisreset
```
## Storage providers (Azure / MinIO)
В `appSettings` нашли:
```xml
<add name="azureGalleries" storageType="MoreThenCms.FileStorage.Azure.AzureCloudStorage, ...">
<settings>
<add name="connectionString"
value="DefaultEndpointsProtocol=http;AccountName=snolla;
AccountKey=<base64>" />
<add name="container" value="galleries" />
</settings>
</add>
```
Используется **Azure Storage SDK с custom endpoint**. В production endpoint указывает на **MinIO** (S3-compat но **не** Azure-compat — пользователь упомянул "не так всё" и обещал пояснить). Точная схема подключения — TBD, надо изучать `MoreThenCms.FileStorage.Azure.AzureCloudStorage` класс.
## `[regex]::Escape` для безопасной замены IP
`192.168.1.10` без escape — точки в regex matchят любой char. Лучше escape:
```powershell
$pattern = [regex]::Escape('192.168.1.10')
$new = $content -replace $pattern, '10.0.2.2'
```
Связано: [[snolla-recovery-vm]], [[traefik-on-windows-docker-desktop]].

View File

@@ -1,102 +0,0 @@
---
title: CMS 502 при HTTPS-mode для конкретного hostname (maljarka.tandemmebel.ru)
type: concept
tags: [cms, iis, gotcha, url-rewrite, https]
sources: []
updated: 2026-05-21
---
# CMS 502 при HTTPS-mode для maljarka.tandemmebel.ru
CMS код (или CMS DB config) для `maljarka.tandemmebel.ru` падает когда IIS request попадает с включённым HTTPS-context (`HTTPS=on`/`SERVER_PORT=443`). Тот же hostname работает (200 OK) когда request интерпретируется как plain HTTP. Другие cms hosts (`tandemmebel.ru`, `emspb.ru`, `pilorama98.ru` и т.д.) работают нормально в HTTPS-mode.
## Симптом
Через traefik HTTPS chain:
```
GET https://maljarka.tandemmebel.ru/ → 502 Bad Gateway
```
## Bisect-trace
| Direct backend request (внутри traefik container) | Headers | Result |
|---|---|---|
| `wget http://host.docker.internal:8089/ -H 'Host: maljarka.tandemmebel.ru'` | base | **200 OK** (43730 bytes, "Малярка от Тандеммебель") |
| same + `X-Forwarded-For: 1.2.3.4` | XFF only | **200 OK** |
| same + `X-Forwarded-Proto: https` | XFP=https | **502** ← триггер |
| same + ALL X-Forwarded-* | full | **502** |
| `Host: tandemmebel.ru` + XFP=https | sibling control | 301 (correct) |
`X-Forwarded-Proto: https` — единственная переменная которая триггерит 502 для maljarka.tandemmebel.ru.
## Mechanism
После [[cms-server-port-leak-fix]] в `C:\sites\snolla\Web.config` добавлен URL Rewrite rule:
```xml
<rule name="ForwardedProto-HTTPS" stopProcessing="false">
<match url=".*" />
<conditions>
<add input="{HTTP_X_FORWARDED_PROTO}" pattern="^https$" />
</conditions>
<action type="None" />
<serverVariables>
<set name="HTTPS" value="on" />
<set name="SERVER_PORT" value="443" />
<set name="SERVER_PORT_SECURE" value="1" />
</serverVariables>
</rule>
```
Когда traefik проксит request через HTTPS entrypoint, он добавляет `X-Forwarded-Proto: https` (стандартное поведение). IIS URL Rewrite rule срабатывает → ставит `HTTPS=on` / `SERVER_PORT=443`. CMS код (`MoreThenCms.Web`) видит request как HTTPS.
Для большинства hostname'ов CMS строит URLs корректно в HTTPS-context. Для `maljarka.tandemmebel.ru` — что-то падает (NullRef в URL builder, missing config, redirect loop, etc.) → ASP.NET unhandled exception → IIS возвращает 502.
## Where to investigate (next session)
1. **IIS Logs**`C:\inetpub\logs\LogFiles\W3SVC*\` для site `snolla` — найти request с `Host: maljarka.tandemmebel.ru` + XFP=https → status code + sub-status.
2. **ASP.NET Event Log**`eventvwr.msc` → Application log → ASP.NET 4.0 errors с stack trace.
3. **CMS DB** — table с per-host config (если есть field "https URL" / "base URL"); может для maljarka.tandemmebel.ru эта запись `NULL`/empty.
4. **CMS source** — grep по `siteRoot`, `BaseUrl`, `HttpsUrl` в коде CMS; искать где условие `IsSecureConnection` или `Request.IsHttps` ветвит логику.
5. **`Url.SiteRoot()`** — уже паттерн из [[cms-server-port-leak-fix]]; возможно тут другой helper падает specifically на этом host.
## Workarounds
### Workaround A: Strip X-Forwarded-Proto для maljarka в traefik (быстро, но скрывает CMS bug)
В `maljarka.yml` добавить middleware:
```yaml
http:
routers:
maljarka:
...
middlewares: [maljarka-strip-xfp]
middlewares:
maljarka-strip-xfp:
headers:
customRequestHeaders:
X-Forwarded-Proto: ""
```
Side effect: maljarka страница может содержать `http://...` asset links вместо `https://...` — mixed content warnings в браузере.
### Workaround B: Per-host bypass в URL Rewrite rule
В `C:\sites\snolla\Web.config` обновить rule:
```xml
<conditions>
<add input="{HTTP_X_FORWARDED_PROTO}" pattern="^https$" />
<add input="{HTTP_HOST}" pattern="^maljarka\.tandemmebel\.ru$" negate="true" />
</conditions>
```
Workaround C: hot-fix DB / Web.config с правильным базовым URL для maljarka.tandemmebel.ru (нужно исследование CMS).
## Применено
Не применено. Diagnosed only — fix отложен до CMS code investigation. Side-finding из [[traefik-maljarka-502-bug]].
## Ссылки
- URL Rewrite rule origin: [[cms-server-port-leak-fix]]
- Other CMS-side 5xx bugs (паттерн): [[cms-admin-assets-root-folder-seed]] (другой NullRef), `emspb /admin/assets 500` (snapshot open issue #8).

View File

@@ -1,174 +0,0 @@
---
title: CMS port-leak fix — URL Rewrite serverVariables на host IIS
type: concept
tags: [iis, url-rewrite, traefik, x-forwarded, asp-net, gotcha]
sources: [../sources/iis-host-migration-2026-05-19.md]
updated: 2026-05-19
---
# CMS port-leak fix
Решение для open issue #1 из [[recovery-architecture-snapshot]] (раньше «X-Forwarded headers не настроены, :4443 leak») и админ-utечки `:8089` обнаруженной 2026-05-19 вечером после attempt 2 host-IIS миграции. Документирует **root cause**, **почему VM работала**, **почему обходной path-B на Windows Docker Desktop невозможен**, и **что в итоге применено**.
Связано: [[traefik-on-windows-docker-desktop]] Pitfall 5, [[iis-host-migration-2026-05-19]] Phase 10, [[docker-host-loopback-detect]].
## Симптом
Admin URLs формата (после attempt 2 миграции на host-IIS:8089):
- `https://emspb.snolla.com:8089/admin/assets/<guid>/getList?path=`
- `https://emspb.snolla.com:8089/admin/themes/getImageSizes/`
- `https://emspb.snolla.com:8089/admin/templates/editors.tmpl.html?v=2.006`
Browser HSTS upgrade'ит `http://...:8089``https://...:8089` → TCP open, TLS handshake fails (8089 = plain HTTP) → admin SPA ломается.
Аналогично, ранее (issue #1) — CMS делал HTTP→HTTPS redirect с `:4443` (traefik external port).
## Root cause
`MoreThenCms.Admin\Mis\Web\Mvc\UrlHelpers\UrlHelpers.cs:14-37` `Url.SiteRoot()`:
```csharp
var port = context.Request.ServerVariables["SERVER_PORT"];
if (usePort) {
if (port == null || port == "80" || port == "443") port = "";
else port = ":" + port;
}
var protocol = context.Request.ServerVariables["SERVER_PORT_SECURE"];
if (protocol == null || protocol == "0") protocol = "http://"; else protocol = "https://";
var sOut = protocol + context.Request.ServerVariables["SERVER_NAME"] + port + appPath;
```
Читает **socket-level** server variables. На IIS site `snolla` binding `*:8089` HTTP → `SERVER_PORT=8089`, `SERVER_PORT_SECURE=0` → формирует `http://emspb.snolla.com:8089` → рендерится в Razor:
```cshtml
@* C:\sites\snolla\Views\Shared\_Layout.cshtml:229 *@
mis.siteRoot = '@Url.SiteRoot().Replace("http:", "https:")' + '/admin';
@* и _LogInLayout.cshtml:156 *@
mis.siteRoot = '@Url.SiteRoot()';
```
Замена `http:→https:` в Layout была *прошлым* частичным patch'ем — не убирает порт. Десятки .cshtml в admin также используют `Url.SiteRoot()` для inline `<style background: url(... + "/images/loading2.gif")>` — все utекают `:8089`.
## Почему VM работала (без всяких rewrite)
SSH probe VM (parallel running snolla-recovery): `appcmd list site /xml``MoreThenCms.Web bindings="http/*:80:"`. **`SERVER_PORT=80`** → `port` пустеет (whitelist `{80,443}`), `mis.siteRoot = 'http://emspb.snolla.com'` (no port, http scheme). Browser HSTS upgrade'ит до https и попадает обратно через router-NAT-traefik-chain в backend. Работает.
VM `applicationHost.config` проверен — **никаких** rewrite rules, никакого URL Rewrite/ARR модуля. Production magic = просто IIS бинд на `:80`.
## Почему path B (host IIS :80 + traefik backend :80) НЕ работает на Windows Docker Desktop
Pre-flight 2026-05-19 вечер:
```
docker exec traefik wget -qS http://host.docker.internal:80/admin/account/login
→ HTTP/1.1 301 Moved Permanently
Content-Length: 17
Content-Type: text/plain
Location: https://emspb.snolla.com/admin/account/login
(НЕТ Server: header — не IIS, не traefik)
```
`Get-NetTCPConnection -LocalPort 80 -State Listen` → только `PID=4 NAME=System` (http.sys/IIS). Никакого другого listener'а. Direct `Invoke-WebRequest http://127.0.0.1/admin/...` с правильным Host header возвращает 200 OK с `Server: Microsoft-IIS/10.0`.
Но через **`host.docker.internal:80`**, **`gateway.docker.internal:80`** (192.168.65.1), **`192.168.1.143:80`** (LAN IP) — все три возвращают тот же 17-byte 301 plain text. То есть **Docker Desktop's WSL2 NAT proxy на Windows перехватывает container-→host:80 traffic и подменяет ответ HTTP→HTTPS redirect'ом**.
Подтверждение что **не** traefik: после смены traefik http entrypoint `:80 → :8090` (`docker-compose.yml ports: 8000:8090`, `traefik.yml http.address: ":8090"`) и `docker compose up -d --force-recreate``netstat -tlnp` в контейнере показывает `:8090 LISTEN`, **нет** `:80`. И всё равно `host.docker.internal:80` возвращает тот же 17-byte 301.
**Этого quirk'а нет на Linux Docker** (synology) — там `host.docker.internal` либо не существует, либо ведёт себя стандартно. Поэтому VM-стек работал, host-Windows-Docker — нет.
## Применённое решение (path C)
URL Rewrite 2.1 + `<serverVariables>` rule на host IIS — переписывает `SERVER_PORT/SERVER_PORT_SECURE/HTTPS` ДО того как ASP.NET их читает.
### Шаги
1. **MSI install URL Rewrite 2.1** (elevated):
```powershell
$msi = "$env:TEMP\rewrite_amd64_en-US.msi"
Invoke-WebRequest 'https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi' -OutFile $msi
Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart" -Verb RunAs -Wait
```
Verify: `Get-Item C:\Windows\System32\inetsrv\rewrite.dll` (FileVersion 7.1.1993.2351).
2. **applicationHost.config — `<allowedServerVariables>`** (elevated, через appcmd):
```powershell
$appcmd = 'C:\Windows\System32\inetsrv\appcmd.exe'
foreach ($n in 'HTTPS', 'SERVER_PORT', 'SERVER_PORT_SECURE') {
& $appcmd set config -section:system.webServer/rewrite/allowedServerVariables "/+[name='$n']" /commit:apphost
}
```
**Built-in IIS server variables** (HTTPS/SERVER_PORT/*) MUST be в apphost-allowedServerVariables, **не** site Web.config. Иначе HTTP 500 "URL Rewrite Module Error" без body.
3. **Site `C:\sites\snolla\Web.config`** (UTF-8 BOM!) — добавить в `<system.webServer>`:
```xml
<rewrite>
<rules>
<rule name="ForwardedProto-HTTPS" stopProcessing="false">
<match url=".*" />
<conditions>
<add input="{HTTP_X_FORWARDED_PROTO}" pattern="^https$" />
</conditions>
<serverVariables>
<set name="HTTPS" value="on" />
<set name="SERVER_PORT" value="443" />
<set name="SERVER_PORT_SECURE" value="1" />
</serverVariables>
<action type="None" />
</rule>
</rules>
</rewrite>
```
`<action type="None"/>` — rule не редиректит/переписывает URL, только сидит как side-effect ставящая server vars. `stopProcessing="false"` чтобы другие rules могли продолжить (на случай если добавим).
4. **Recycle** — IIS auto-recycle при изменении Web.config (~3 сек).
5. **Verify**:
```bash
docker exec traefik sh -c "wget -qS --no-check-certificate -O- \
--header='Host: emspb.snolla.com' \
https://127.0.0.1:443/admin/account/login 2>&1 | grep siteRoot"
# Expected: mis.siteRoot = 'https://emspb.snolla.com'; (БЕЗ :8089)
```
Traefik 2.x по-default шлёт `X-Forwarded-Proto: https` для requests через https entrypoint — наш `HTTP_X_FORWARDED_PROTO` condition fires автоматически.
### Atomic revert
```powershell
Copy-Item C:\sites\snolla\Web.config.bak-pre-portleak-2026-05-19 C:\sites\snolla\Web.config -Force
# ~3 сек на app pool reload
# apphost можно оставить (allowedServerVariables inert без rule)
# MSI URL Rewrite можно оставить (модуль без rule = no behavior)
```
## Gotchas
### `customErrors mode="off"` lowercase — fatal regression
`C:\sites\snolla\Web.config:43` имел `<customErrors mode="off" />` (lowercase). ASP.NET до моего edit'а **lazily** валидировал это — работало. После моего Web.config edit'а (добавил `<rewrite>` block) → ASP.NET full-reload → enum parse fail:
```
Значение свойства 'mode' не может быть проанализировано.
Ошибка: Значение перечисления должно быть одним из следующих: RemoteOnly, On, Off.
```
YSOD config error блокирует **весь** site. Fix: `mode="Off"` (capital O). Урок: при любом Web.config edit'е сначала grep на enum-style attributes на корректный casing.
### `[xml]` save в PowerShell flattens applicationHost.config
`$cfg.OuterXml` сбрасывает все пробелы/переносы → файл 937 строк превращается в 1 строку. IIS читает (whitespace для XML parser не важен), но файл нечитаем для людей. Backup и `appcmd set config /+...` — единственно правильный способ редактировать apphost programmatically.
### Site-level `<allowedServerVariables>` для built-in vars
Site `Web.config` MOJET содержать `<rewrite><allowedServerVariables>` ТОЛЬКО для custom variables (HTTP_X_*, etc). Для built-in IIS vars (HTTPS, SERVER_PORT, SERVER_PORT_SECURE) — apphost OR HTTP 500. Не дублировать в site-level — конфликт.
## Сделано на host'е (2026-05-19 вечер)
- `C:\Windows\System32\inetsrv\config\applicationHost.config` — `<rewrite>/<allowedServerVariables>` += HTTPS, SERVER_PORT, SERVER_PORT_SECURE. Backup `.bak-pre-portleak-2026-05-19`.
- `C:\sites\snolla\Web.config` — `<rewrite>/<rules>` += rule "ForwardedProto-HTTPS"; `customErrors mode="off"` → `"Off"`. Backup `.bak-pre-portleak-2026-05-19`.
- URL Rewrite 2.1 MSI installed.
- Затрагивает **все 11 cms hosts** (общий site `snolla`): emspb, labtools, labtoolspro, pilorama98, tandemmebel, kupimknigi, maljarka, sestech, isc-artmaterials, rimiz, plus snolla.com sub-domains.
## Sibling fix (отдельная task, тот же день)
После port-leak fix'а surfaced `/admin/assets/<guid>/getList → 500 NullReferenceException` на admin assets. Оказался **не site-specific** — общий для 15 sites без root AssetsFolder в DB. Решён DB seed'ом, см. [[cms-admin-assets-root-folder-seed]].

View File

@@ -1,71 +0,0 @@
---
title: Web.config — XML escape для спецсимволов в connection-string паролях
type: concept
tags: [iis, webconfig, encoding, xml, gotcha]
sources: [../sources/iis-host-migration-2026-05-19.md]
updated: 2026-05-19
---
# Web.config: XML-escape для `&` (и других reserved chars) в пароле
## Симптом
После patch Web.config с новым connection-string'ом ASP.NET-сайт отдаёт **HTTP 500** на любой запрос. В Event Viewer / IIS logs — `System.Configuration.ConfigurationErrorsException: Configuration system failed to initialize` или `Unrecognized escape sequence`. На уровне XML парсера — error при чтении Web.config.
## Корень
`Web.config` — это **XML-документ**. Атрибуты в нём (`<add connectionString="..." />`) проходят через XML-парсер до того, как .NET runtime увидит сам connection string. Если в пароле есть **XML-зарезервированный символ**, парсер ломается.
XML reserved chars (в значениях атрибутов):
| Символ | XML-entity escape | Когда обязателен |
|---|---|---|
| `&` | `&amp;` | **всегда** (в attribute value и в text content) |
| `<` | `&lt;` | **всегда** |
| `"` | `&quot;` | если значение в `"..."` (наш кейс — атрибуты обычно в двойных кавычках) |
| `'` | `&apos;` | если значение в `'...'` |
| `>` | `&gt;` | формально не обязателен, но safer |
В именно нашем кейсе из [[iis-host-migration-2026-05-19]]: пароль `^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF` содержит `&` — XML-парсер начинал интерпретировать `&_ioaT...` как entity reference (`&_ioaT;` — невалидное имя entity) → exception → IIS 500.
## Правильный способ
```xml
<!-- WRONG (literal &) -->
<add name="MoreThenCmsEntities"
connectionString="...Password=^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF;..."
providerName="System.Data.SqlClient" />
<!-- RIGHT (& -> &amp;) -->
<add name="MoreThenCmsEntities"
connectionString="...Password=^I9D)LB)DK)8J#xBDG$t}W&amp;_ioaT!M!LF;..."
providerName="System.Data.SqlClient" />
```
После XML-парсинга .NET runtime получит литеральный `&` в значении атрибута и передаст в SqlConnection — там пароль уже текст, всё ок.
## Что НЕ требует escape
- `^`, `$`, `(`, `)`, `{`, `}`, `[`, `]`, `*`, `+`, `?`, `\`, `/`, `.`, `,`, `;`, `:`, `=`, `#`, `@`, `%`, `!`, `~`, `|` — все безопасны в XML attribute value.
- Особое: `;` в пароле в ADO.NET conn-string'е требует обёртки пароля в `'...'` или `"..."` внутри connection-string'а — но это отдельная история.
## PowerShell-подсказка для patch
При программной замене conn-string'а**сразу escape `&` в значение**, не оставляй на потом:
```powershell
# 💻 Windows host
$plainPassword = '^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF'
$xmlSafePassword = $plainPassword -replace '&', '&amp;'
# и потом подставлять $xmlSafePassword в Web.config
# или комбинированный escape всех 5 reserved chars:
function Xml-EscapeAttr($s) {
$s -replace '&','&amp;' -replace '<','&lt;' -replace '>','&gt;' -replace '"','&quot;' -replace "'",'&apos;'
}
```
## Связанные паттерны
- [[cms-config-rewrite-pattern]] — UTF-8 BOM ловушка при `Set-Content` без `-Encoding utf8` (другая XML/IIS гнойная тема).
- При генерации паролей для DB-логинов CMS — лучше **запретить `&`, `<`, `"`** в генераторе, чтобы не словить эту проблему в будущем (особенно при автоматизированных patches Web.config через CI).