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:
@@ -1,21 +0,0 @@
|
||||
|
||||
(15 rows affected)
|
||||
Tag,FolderId,OwnerId
|
||||
---,--------,-------
|
||||
INSERTED-ROW,580369C8-DB8B-4352-97EB-4F525C989A7C,07E0290C-8735-474E-B494-1230C1E259B3
|
||||
INSERTED-ROW,52A0DE78-BF1C-4F6D-BE2E-7C401D64F690,96EBC481-D26A-47BE-B660-13D49E7D0A61
|
||||
INSERTED-ROW,465E2F07-525B-4772-BBEB-F7965F605966,81B2461F-78C0-40DF-9B13-2B255DE502FC
|
||||
INSERTED-ROW,03150541-5278-4FB5-B815-630A17611B3D,E924A354-0377-4E1E-80C6-2EB0194AA55F
|
||||
INSERTED-ROW,228DDFE9-112E-42BB-8B61-CF88163B94AA,C3B1FD79-663D-4271-A251-380E8B74EB7F
|
||||
INSERTED-ROW,775F159A-72B7-4B2C-BF13-E57F84BDFCCE,2C82EF33-009E-4779-A51E-389C65D4A3D4
|
||||
INSERTED-ROW,0E6B2936-0522-4897-814E-B16FB4091B4C,663F9410-A6CC-4651-9A5C-62844A313957
|
||||
INSERTED-ROW,4BD7784F-7BA4-431B-952A-33D422161191,F8FBBF59-8F34-4327-96CA-63A8103D8EE6
|
||||
INSERTED-ROW,878D64DF-07B5-4973-BAA4-BC7DA4C7C8BE,4DC7E9E4-8D5D-4ECC-9459-84F7A1BF1759
|
||||
INSERTED-ROW,9F368D86-9CB0-48FD-A871-1AA565808BE8,D8E84734-9E35-414D-9342-862A63AD87FB
|
||||
INSERTED-ROW,542AFFDC-4632-4080-B6C6-19611FB2CEA2,592D399B-31C0-4150-9BFD-880CF7A244C5
|
||||
INSERTED-ROW,A7B028C1-D6BD-49FA-8C16-93CF566C6101,37E67FC4-4B9E-4C06-A522-A26A73BAC9B0
|
||||
INSERTED-ROW,7AA0D6AC-94F1-49B7-8FFE-A13A15144A84,F30E83BB-2FCA-40A1-A658-AE7ED22F294F
|
||||
INSERTED-ROW,6033AC61-19F0-4A4C-AD5E-BE54DE64DF6A,5A826EB9-A05A-41D4-A29C-BDEBE926E8DB
|
||||
INSERTED-ROW,AF61A942-23D0-469C-BB89-B7294BD1DB68,1F12D23C-A9A2-4863-A94D-C6278AD7835E
|
||||
|
||||
(15 rows affected)
|
||||
@@ -1,74 +0,0 @@
|
||||
# cms-admin-assets-root-folders-seed
|
||||
|
||||
## Goal
|
||||
Для каждого `Sites.SiteId` в DB `MoreThenCms` где нет соответствующего root `Folders` row с `Discriminator='AssetsFolder'`, `LoweredPath='/'`, `OwnerId=<SiteId>` — добавить недостающий root row. Это unblock'нёт admin `/admin/assets/<siteId>/getList?path=` который сейчас crash'ит 500 NullReferenceException на пустых assets для 15+ sites (включая `emspb.ru`, `pilorama98.ru`, `labtools.pro`, `kupimknigi.spb.ru`, `sestech.ru`, etc.).
|
||||
|
||||
## Root cause
|
||||
`MoreThenCms.Admin.ViewModels.Builders.AssetsJsonViewModelBuilder.Build` (`AssetsJsonViewModelBuilder.cs:22`) делает `new JValue(model.ParentPath)` БЕЗ null-check на model. `_assetsFoldersService.GetFolderByPath(ctx, ownerId, '')` возвращает null когда root folder не существует. Это происходит для sites которые **никогда не открывали admin assets UI** (root создаётся lazy при first upload, видимо).
|
||||
|
||||
Frontend код (`AssetsAppFunc.cs:66-86`) делает proper null-check → HTTP 404. Только admin view-model builder упустил.
|
||||
|
||||
Двух-уровневое решение:
|
||||
- **Краткосрочно (этой task'и):** seed missing root rows в DB. Один INSERT per site. Risk: minimal (lookup existing row pattern; используем same shape).
|
||||
- **Долгосрочно (отдельный task):** patch `AssetsJsonViewModelBuilder.Build` → null-guard → empty response. Требует recompile DLL `MoreThenCms.Admin.dll` (нет полного build env, рискованно).
|
||||
|
||||
## Key files
|
||||
- `MoreThenCms\Assets\Services\AssetsFoldersService.cs:47-54` — `GetFolderByPath` query (returns null if not found).
|
||||
- `MoreThenCms.Web\ViewModels\Builders\AssetsJsonViewModelBuilder.cs:22` — точка падения.
|
||||
- `MoreThenCms.Web\Admin\Controllers\AssetsController.cs:44-54` — вызов GetList.
|
||||
- DB: `[MoreThenCms].[dbo].[Folders]` (polymorphic, Discriminator='AssetsFolder' для assets).
|
||||
|
||||
## Discovery (2026-05-19)
|
||||
- 15 sites без root AssetsFolder подтверждены через `SELECT s.SiteId, s.PrimaryDomain, ... FROM Sites s LEFT JOIN ...`. См. полный список в Decisions.
|
||||
- Sample existing root row: `FolderId=1591A38C-..., OwnerId=7EB313DD-F289-..., Path='/', LoweredPath='/', DateCreated=2017-03-13 07:10:15, CreatedById=E4CC416B-...`. Тот же shape надо воспроизвести для missing.
|
||||
- Затрагивает: emspb.ru (`96EBC481-...`), pilorama98.ru (`37E67FC4-...`), labtools.pro, kupimknigi.spb.ru, sestech.ru, aquamax.spb.ru, artmone.pro, priemka-kvartiry.ru, profund.spb.ru, ics-artmaterials.com, _voda-indigo.ru, plus 4 sites с NULL `PrimaryDomain`.
|
||||
|
||||
## План
|
||||
1. **Dry-run**: `SELECT COUNT(*) FROM Sites s WHERE NOT EXISTS (root for s.SiteId)` — должно быть = 15 (sanity check).
|
||||
2. **Seed**: INSERT root row per missing site. Один transaction, no rollback unless count mismatch.
|
||||
3. **Verify**: повторить original probe — root count should be 0 missing.
|
||||
4. **Test**: hit `https://emspb.snolla.com/admin/assets/96EBC481-.../getList?path=` (через docker exec без auth → 302→login без crash). Затем user-side browser test.
|
||||
|
||||
## SQL
|
||||
```sql
|
||||
DECLARE @CreatedById uniqueidentifier = (
|
||||
SELECT TOP 1 CreatedById FROM Folders
|
||||
WHERE LoweredPath='/' AND Discriminator='AssetsFolder'
|
||||
);
|
||||
INSERT INTO Folders (FolderId, OwnerId, Path, LoweredPath, DateCreated, UtcDateCreated, CreatedById, Discriminator)
|
||||
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'
|
||||
);
|
||||
```
|
||||
|
||||
## Atomic revert
|
||||
Сохранить список inserted FolderId через `OUTPUT INSERTED.FolderId` в temp file. Если что:
|
||||
```sql
|
||||
DELETE FROM Folders WHERE FolderId IN (<list>);
|
||||
```
|
||||
|
||||
## Open questions
|
||||
- [ ] Аналогичная проблема для ImagesFolder/StylesheetsFolder/ScriptsFolder в admin Themes UI? Проверить.
|
||||
- [ ] DLL patch `AssetsJsonViewModelBuilder.Build` нужен в долгую — отдельная task. Сейчас обходим seed'ом.
|
||||
|
||||
## Decisions log
|
||||
- 2026-05-19: вынес из `cms-port-leak-fix` (там был open issue #8 на emspb). User подтвердил воспроизводимость на pilorama98 → не site-specific, общая data issue.
|
||||
- 2026-05-19: **DB seed применён** — 15 rows inserted (single transaction, idempotent NOT EXISTS). Inserted FolderId/OwnerId captured в `cms-admin-assets-root-folders-seed.inserted-rows.txt`. Server-side verified (probe вернул 302→login вместо 500). User browser-verified на pilorama98/emspb — admin assets открывается, empty list без ошибок.
|
||||
- 2026-05-19: **DLL recompile отложен** — `AssetsJsonViewModelBuilder.Build` нужен null-guard как defensive code, но требует build env (нет gitea/build/registry на recovery host'е). Ждёт [[vds-kzntsv-bootstrap]] для восстановления pipeline'а. На текущем seed-only fix'е admin assets работает для всех 15 sites.
|
||||
|
||||
## Completed steps
|
||||
- [x] Diagnostic: stack trace из Application event log → `AssetsJsonViewModelBuilder.cs:22` NullRef on `model.ParentPath`. Source code analysis показал отсутствие null-check (vs proper null-check во frontend `AssetsAppFunc.cs:66-86`).
|
||||
- [x] DB probe schema: `Folders` table polymorphic с Discriminator, columns FolderId/OwnerId/Path/LoweredPath/DateCreated/CreatedById обязательные.
|
||||
- [x] Dry-run: 15 sites без root AssetsFolder (включая emspb/pilorama98/labtools.pro/kupimknigi/sestech/etc), 4 sites с NULL PrimaryDomain тоже в списке.
|
||||
- [x] **SQL seed** через `docker exec mssql sqlcmd` — INSERT 15 rows с `OUTPUT INSERTED.FolderId, INSERTED.OwnerId INTO @Inserted`, output saved в `.tasks/cms-admin-assets-root-folders-seed.inserted-rows.txt`.
|
||||
- [x] Verify: missing-root count = 0. Probe `wget --header='Host: emspb.snolla.com'` на getList URL → 302 Found (auth redirect — normal), нет больше 500.
|
||||
- [x] User browser-verified pilorama98 admin assets открывается.
|
||||
- [x] Wiki ingest: новый concept `cms-admin-assets-root-folder-seed.md`, snapshot issue #8 → resolved, `cms-server-port-leak-fix` sibling-link, log.md + index.md обновлены.
|
||||
|
||||
## Notes
|
||||
- Связано: [[cms-server-port-leak-fix]] (предыдущая task'а закрыта; admin URLs теперь без `:8089`); [[recovery-architecture-snapshot]] (issue #8 — resolved); [[vds-kzntsv-bootstrap]] (build env для долгосрочного DLL fix'а).
|
||||
@@ -1,52 +0,0 @@
|
||||
# cms-maljarka-https-mode-bug-fix
|
||||
|
||||
## Goal
|
||||
|
||||
Починить `maljarka.tandemmebel.ru` чтобы он отвечал 200 (не 502) через публичный HTTPS chain. Root cause diagnosed в [[traefik-maljarka-502-bug]]: IIS+CMS падает на этом hostname когда `HTTPS=on`/`SERVER_PORT=443` выставлены URL Rewrite rule'ом. Full mechanism — wiki concept [[cms-maljarka-https-mode-crash]].
|
||||
|
||||
## Key files
|
||||
|
||||
- `C:\sites\snolla\Web.config` — URL Rewrite rule "ForwardedProto-HTTPS" из [[cms-server-port-leak-fix]]
|
||||
- `C:\inetpub\logs\LogFiles\W3SVC*\` — IIS logs site `snolla` (нужно найти sub-status 502.x для конкретного request)
|
||||
- CMS DB — table с per-host config (per [[recovery-architecture-snapshot]], CMS использует MSSQL контейнер)
|
||||
- CMS source: `MoreThenCms.Admin\Mis\Web\Mvc\UrlHelpers\UrlHelpers.cs:14-37` (`Url.SiteRoot()`) — паттерн из [[cms-server-port-leak-fix]]; возможно тут helper падает specifically на этом host
|
||||
|
||||
## Bisect-trace evidence
|
||||
|
||||
```
|
||||
direct backend Host: maljarka.tandemmebel.ru → 200 OK (43KB)
|
||||
direct backend Host: maljarka.tandemmebel.ru + X-Forwarded-For → 200 OK
|
||||
direct backend Host: maljarka.tandemmebel.ru + X-Forwarded-Proto:https → 502 ← триггер
|
||||
direct backend Host: tandemmebel.ru + X-Forwarded-Proto:https → 301 (correct)
|
||||
```
|
||||
|
||||
Single variable: `X-Forwarded-Proto: https` для этого specific hostname.
|
||||
|
||||
## Next action
|
||||
|
||||
1. **IIS logs**:
|
||||
```powershell
|
||||
Get-Content 'C:\inetpub\logs\LogFiles\W3SVC1\u_extend1*.log' -Tail 500 | Select-String 'maljarka' | Select-Object -Last 20
|
||||
```
|
||||
Найти sub-status (502.3 = bad gateway, 502.4 = no server, 502.5 = ARR config, etc.).
|
||||
|
||||
2. **Event Viewer**:
|
||||
```powershell
|
||||
Get-EventLog -LogName Application -Source 'ASP.NET*' -Newest 50 | Where-Object Message -Match 'maljarka' | Format-List
|
||||
```
|
||||
Stack trace покажет точное место crash.
|
||||
|
||||
3. **CMS DB inspection** — есть ли запись для `maljarka.tandemmebel.ru` в table с per-host URLs, и поле HttpsUrl/BaseUrl не NULL ли там.
|
||||
|
||||
4. **Quick workaround если real customer traffic** — apply Workaround B из [[cms-maljarka-https-mode-crash]] (negate condition в URL Rewrite rule for this Host). Backup Web.config first.
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] Какой priority? Зависит от того real customer traffic на `maljarka.tandemmebel.ru` или это internal/test subdomain.
|
||||
- [ ] Если CMS bug — fix CMS code или DB hot-fix?
|
||||
- [ ] DLL patch для null-guard (от [[cms-admin-assets-root-folders-seed]]) — может тот же класс bugов; может в одном вылазе чинить вместе.
|
||||
|
||||
## Notes
|
||||
|
||||
- 3 workarounds + 5 investigation places задокументированы в [[cms-maljarka-https-mode-crash]] wiki concept.
|
||||
- Не trivially связан с [[cms-server-port-leak-fix]] — там URL Rewrite rule добавлен правильно, но CMS код в этом конкретном случае ломается. Регрессия CMS, не fix'а.
|
||||
@@ -1,104 +0,0 @@
|
||||
# cms-port-leak-fix
|
||||
|
||||
## Goal
|
||||
Убрать утечку internal-портов в URL'ы, которые CMS генерирует серверно. Сейчас часть admin-роутов выдают URL'ы вида `https://labtools.snolla.com:8089/admin/...` — `:8089` это IIS site binding на хосте, не должен попадать наружу.
|
||||
|
||||
Связанный, тот же класс баг (open issue #1 в [recovery-architecture-snapshot.md](../.wiki/concepts/recovery-architecture-snapshot.md)): CMS делает HTTP→HTTPS redirect с `:4443` (traefik external HTTPS port на хосте). Желательно фиксить общим решением.
|
||||
|
||||
## Симптомы (от user, 2026-05-19)
|
||||
Сломанные URLs admin'ки на `labtools.snolla.com` (наверняка и на остальных 10 cms доменах тоже):
|
||||
- `https://labtools.snolla.com:8089/admin/assets/<guid>/getList?path=`
|
||||
- `https://labtools.snolla.com:8089/admin/themes/getImageSizes/`
|
||||
- `https://labtools.snolla.com:8089/admin/templates/editors.tmpl.html?v=2.006`
|
||||
|
||||
Browser сразу делает request на `:8089` напрямую → fail (этот порт не открыт наружу через router NAT; traefik слушает 443/4443 only).
|
||||
|
||||
## Hypothesis — root cause
|
||||
Цепочка:
|
||||
1. Traefik backend = `http://host.docker.internal:8089/`, `passHostHeader: true` → IIS видит `Host: labtools.snolla.com` (без порта).
|
||||
2. IIS binding на site `snolla` = `*:8089` → connection-level порт = 8089.
|
||||
3. `HttpRequest.Url.Port` / `Url.Authority` в ASP.NET возвращают **порт сокета**, не порт из Host header. Значит `Authority = "labtools.snolla.com:8089"`.
|
||||
4. Где-то в admin views / controllers / partials CMS строит absolute URL через `Request.Url.GetLeftPart(UriPartial.Authority)` или `string.Format("{0}://{1}", scheme, host_with_port)` → утечка `:8089`.
|
||||
5. Альтернатива — admin SPA получает baseUrl в server-rendered JS-переменной (`window.adminBaseUrl = '@Request.Url.Scheme://@Request.Url.Authority/admin'` или подобное). Это нужно подтвердить grep'ом по deployed-ASP-views + .js.
|
||||
|
||||
Аналогичная hypothesis для `:4443`:
|
||||
- Не из `Url.Port` (он = 8089 на attempt2-host, был 18080 на VM-эпохе).
|
||||
- Скорее CMS читает `Request.Headers["X-Forwarded-Host"]` который traefik по умолчанию шлёт как `host:port`, ИЛИ кто-то один раз протестил через `https://host:4443/` напрямую и попал в captured-redirect.
|
||||
- Возможно: CMS делает `Response.Redirect` с absolute URL, и старый CMS-код в эпоху production-traefik (на синке тоже :4443?) научился возвращать публичный port из конфига.
|
||||
- **Эта часть требует тест-кейса** — повторить на host'е сейчас и посмотреть, попадает ли вообще `:4443` в response Location header.
|
||||
|
||||
## Key files (текущие deployed)
|
||||
- `C:\sites\snolla\Views\Shared\_Layout.cshtml` — main layout, есть `var hostingUrl = new Uri("https://" + AppSettings["primaryDomain"]);` (line 11-12). Не порт, но паттерн «строим absolute URL» подсветить.
|
||||
- `C:\sites\snolla\Areas\Admin\Views\_ViewStart.cshtml` — корень всех admin views, скорее всего тянет admin-specific layout.
|
||||
- `C:\sites\snolla\Areas\Admin\Views\**\Shared\*` — admin layout (искать; возможно тут baseUrl).
|
||||
- `C:\sites\snolla\admin\templates\*.tmpl.html` — статические template'ы admin SPA. Сами URL'ы не генерируют, но загружаются из JS которая видит baseUrl откуда-то.
|
||||
- `C:\sites\snolla\Web.config` — appSettings (`primaryDomain` уже есть), будущие `<rewrite>` rules.
|
||||
- `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\*.yml` — site routes (тут middleware подключаем).
|
||||
- `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\snolla.yml` `:1` — already has `passHostHeader: true`. Добавить middleware на forwarded-headers.
|
||||
|
||||
## План этапов
|
||||
|
||||
### Phase 1 — diagnostic (не write-op)
|
||||
- [ ] Сделать XHR call на `https://labtools.snolla.com/admin/themes/getImageSizes/` (с auth-cookie если есть) и записать response headers + body. Подтвердить откуда именно `:8089` приходит (Location header? `<base>` tag? `<script var>` ? JSON `baseUrl` field?).
|
||||
- [ ] Найти server-side место — grep по `:8089`, `Request.Url.Authority`, `Request.Url.GetLeftPart`, `Url.Action(.., protocol:)`, `UriBuilder` в `MoreThenCms.Web\Admin\` source + deployed.
|
||||
- [ ] Проверить, отдаёт ли traefik сейчас `X-Forwarded-*` headers backend'у (по default traefik их шлёт, но `X-Forwarded-Port` = 443 entrypoint port, и `X-Forwarded-Host` = client Host).
|
||||
- [ ] Воспроизвести `:4443` leak: тест с публичной сети, посмотреть Location header в HTTP→HTTPS redirect.
|
||||
|
||||
### Phase 2 — fix через IIS URL Rewrite + ARR (RECOMMENDED)
|
||||
**Почему этот путь:** не трогаем CMS source code, фикс на инфра-уровне, переносим только конфиг — не риск перекомпиляции. Standard ASP.NET reverse-proxy паттерн.
|
||||
|
||||
Шаги:
|
||||
- [ ] Установить IIS modules: `URL Rewrite 2.1` + `Application Request Routing 3.0` через MSI (download links в notes).
|
||||
- [ ] В `C:\sites\snolla\Web.config` (и stostayer/stostayer.old аналогично) добавить inbound rewrite rule со `<serverVariables>` блоком — переписать `HTTPS=on`, `SERVER_PORT=443`, `SERVER_PORT_SECURE=1` если `HTTP_X_FORWARDED_PROTO=https`.
|
||||
- [ ] В `applicationHost.config` (требует admin-shell) добавить `<allowedServerVariables>` для всех затрагиваемых vars (HTTPS, SERVER_PORT, SERVER_PORT_SECURE, HTTP_HOST если будем переписывать).
|
||||
- [ ] Перезапустить IIS site / app pool, проверить `Request.Url.Port == 443` через простой `@Request.Url.Port` echo на test-странице.
|
||||
- [ ] Если CMS читает `Request.Url.Host` через Authority — переписать также `HTTP_HOST` (но это рискованнее, может сломать вирт.хосты на IIS).
|
||||
|
||||
### Phase 3 — traefik middleware (если Phase 2 не закрывает всё)
|
||||
- [ ] Добавить `data/custom/forwarded-headers-middleware.yml`:
|
||||
```yaml
|
||||
http:
|
||||
middlewares:
|
||||
forward-https:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: https
|
||||
X-Forwarded-Port: "443"
|
||||
```
|
||||
- [ ] В каждый site-yml router добавить `middlewares: [forward-https]`.
|
||||
- [ ] `docker restart traefik`, проверить headers пробрасываются.
|
||||
|
||||
### Phase 4 — verify
|
||||
- [ ] Phone-test admin login + admin/themes/getImageSizes/ XHR — URL без портов.
|
||||
- [ ] Smoke test publicly через `Invoke-WebRequest` на all 11 cms domains, посмотреть Location headers на любых redirect'ах — `:4443` / `:8089` не должно нигде встречаться.
|
||||
- [ ] Update [recovery-architecture-snapshot.md](../.wiki/concepts/recovery-architecture-snapshot.md) — issue #1 → resolved.
|
||||
|
||||
## Open questions
|
||||
- [ ] Можно ли вообще менять `applicationHost.config` без брейка стека? (это глобальный IIS config, не per-site).
|
||||
- [ ] CMS-код в admin строит absolute URL'ы — server-side (Razor) или client-side (JS)? Это меняет место fix'а.
|
||||
- [ ] Bind на `*:80` для IIS site `snolla` ещё держится? Если да — traefik можно (теоретически) переключить на :80 backend, но это вернёт Docker NAT loopback bug attempt 1. **НЕ ДЕЛАТЬ** без рекомендации loop-detect recipe.
|
||||
- [ ] `*.tmpl.html` файлы — статичные. Кто их load'ит и откуда берёт URL? Возможно admin.js, который читает `<base href>` или meta tag.
|
||||
|
||||
## Decisions log
|
||||
- 2026-05-19: вынес из `iis-on-host-migration` (Phase 5 «бонусы») в отдельную задачу — user попросил. Аргументация: scope крупнее чем «бонус» — затрагивает все 11 sites + traefik + потенциально applicationHost.config. Тестировать надо отдельно от migration soak.
|
||||
- 2026-05-19: **Phase 1 diagnostic** — `Url.SiteRoot()` в `UrlHelpers.cs:14-37` подтверждён как root cause (читает `Request.ServerVariables["SERVER_PORT"]` который = 8089 на attempt2-host). VM SSH probe подтвердил binding `MoreThenCms.Web *:80:` → SERVER_PORT=80 → port stripped whitelist'ом → почему VM работала.
|
||||
- 2026-05-19: **Path B abandoned** (traefik http entrypoint :80 → :8090 + IIS backend :80) — Docker Desktop's WSL2 NAT quirk возвращает 17-byte 301 plain text на `host.docker.internal:80` независимо от того что внутри traefik :80 entrypoint удалён. Quirk reproducible через `gateway.docker.internal:80` и LAN IP `192.168.1.143:80` тоже. Traefik configs восстановлены из `.bak-pre-portleak-2026-05-19`, canary yml удалён.
|
||||
- 2026-05-19: **Path C applied** — URL Rewrite 2.1 + apphost allowedServerVariables + Web.config rewrite rule. Verified end-to-end через `docker exec traefik wget https://127.0.0.1:443/...` (full chain включая traefik X-Forwarded-Proto auto-injection).
|
||||
- 2026-05-19: **emspb /admin/assets 500** — user подтвердил «только этот site, остальное работает», вынесено из scope этой task'и в open issue #8 `recovery-architecture-snapshot`. Не блокирует closing.
|
||||
|
||||
## Completed steps
|
||||
- [x] **Phase 1 diagnostic**: grep `Url\.SiteRoot\|GetLeftPart\|Url\.(Authority\|Port)`, найден `UrlHelpers.cs:14-37`. VM SSH `appcmd list site /xml` подтвердил VM binding `:80`. `findstr` на VM applicationHost.config = no rewrite rules, `appcmd list module` = no URL Rewrite/ARR — VM working state без специальной IIS-magic.
|
||||
- [x] **Path B aborted**: backup 3 files (traefik.yml, docker-compose.yml, emspb.yml), edit entrypoint :80→:8090, port mapping 8000:8090, canary yml для `emspb.snolla.com` с backend `:80`. `docker compose up -d --force-recreate` → recreate'ed. Loop **не ушёл** на host.docker.internal:80 (probe 17-byte 301). Revert configs из bak, canary yml удалён, recreate again.
|
||||
- [x] **Path C applied**:
|
||||
- URL Rewrite 2.1 MSI install (rewrite_amd64_en-US.msi, 6 MB, version 7.1.1993.2351) via elevated `Start-Process msiexec`.
|
||||
- `applicationHost.config` backup `.bak-pre-portleak-2026-05-19`, добавлены HTTPS/SERVER_PORT/SERVER_PORT_SECURE в `<rewrite>/<allowedServerVariables>` через elevated `appcmd set config /+...` (после неудачной попытки `$cfg.OuterXml` save которая flatt'нула 937 lines в 1 — restored from backup).
|
||||
- `C:\sites\snolla\Web.config` backup `.bak-pre-portleak-2026-05-19` (19050 bytes), добавлен `<rewrite>/<rules>/<rule name="ForwardedProto-HTTPS">` с conditions `HTTP_X_FORWARDED_PROTO=https` → setVars HTTPS/SERVER_PORT/SERVER_PORT_SECURE. UTF-8 BOM сохранён (EF BB BF verified post-edit).
|
||||
- [x] **Regression fix**: `customErrors mode="off"` → `mode="Off"` после YSOD config error (pre-existing bug пробудился ASP.NET full-reload после нашего rewrite-block edit).
|
||||
- [x] **Verify via docker exec traefik** через full chain — `mis.siteRoot = 'https://emspb.snolla.com'` и `mis.siteRoot = 'https://labtools.snolla.com'` (without `:8089`, https scheme). User-side browser test confirmed остальные 10 cms работают.
|
||||
- [x] **Wiki ingest**: new concept `cms-server-port-leak-fix.md`; `traefik-on-windows-docker-desktop.md` Pitfall 5 → RESOLVED; `recovery-architecture-snapshot.md` open issue #1 → resolved + новый #8 emspb assets; `index.md` + `log.md` обновлены.
|
||||
|
||||
## Notes
|
||||
- URL Rewrite 2.1 download: `https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi`
|
||||
- ARR 3.0 download: `https://download.microsoft.com/download/E/9/8/E9849D6A-020E-47E4-9FD0-A023E99B54EB/requestRouter_amd64.msi` (не понадобился — наш fix без ARR)
|
||||
- Связанные wiki: [[cms-server-port-leak-fix]] (детально), [[traefik-on-windows-docker-desktop]] Pitfall 5, [[recovery-architecture-snapshot]] § Известные открытые баги.
|
||||
- Не трогать stayer routes — они уже `.disabled` через traefik, локальный доступ через `:8090/:8091` напрямую (с `:8090/8091` в URL'ах это **корректно**, не leak).
|
||||
@@ -1,55 +0,0 @@
|
||||
# traefik-maljarka-502-bug
|
||||
|
||||
## Goal
|
||||
|
||||
Разобраться почему `maljarka.tandemmebel.ru` через traefik HTTPS возвращает 502 Bad Gateway, хотя backend (IIS на host:8089 с правильным Host header) отвечает 200 OK с валидным CMS body.
|
||||
|
||||
## Evidence (2026-05-21)
|
||||
|
||||
| Path | Result |
|
||||
|---|---|
|
||||
| `docker exec traefik wget --spider https://maljarka.tandemmebel.ru/` | **HTTP/1.1 502 Bad Gateway** |
|
||||
| `docker exec traefik wget --header='Host: maljarka.tandemmebel.ru' http://host.docker.internal:8089/` | **HTTP/1.1 200 OK** (Microsoft-IIS/10.0, 43730 bytes, "Малярка от Тандеммебель") |
|
||||
| `Invoke-WebRequest http://localhost:8089/ -Headers @{Host='maljarka.tandemmebel.ru'}` | **200 OK** (host->host direct) |
|
||||
| `docker exec traefik wget --spider https://www.tandemmebel.ru/` | **200 OK** (sibling route, working) |
|
||||
|
||||
## Config diff
|
||||
|
||||
`maljarka.yml` vs `tandemmebel.yml` — syntactically identical (router→service→loadBalancer→servers, same backend URL `http://host.docker.internal:8089/`, same `passHostHeader: true`). LE cert для `maljarka.tandemmebel.ru` присутствует в `acme.json`.
|
||||
|
||||
## Hypotheses
|
||||
|
||||
1. **Stale traefik state** — uptime 38h+ с регулярными `docker provider connection error` retry-loop'ами. Restart traefik может сбросить bad state. **Easy first check.**
|
||||
2. **Hidden file encoding difference** — BOM / line endings / trailing whitespace в `maljarka.yml` vs `tandemmebel.yml`. `Get-FileHash` оба yml + `Format-Hex` на первые 16 байт.
|
||||
3. **acme.json corruption для maljarka cert** — cert exists но key/chain broken. Traefik не может handshake → fallback вернёт 502? Maybe.
|
||||
4. **Service name collision** — `maljarka` router/service name conflicts c чем-то ещё в dynamic config. Vrai unlikely (grep подтвердил unique).
|
||||
5. **Backend connection limit** — service-specific. Маловероятно.
|
||||
|
||||
## Implementation sketch
|
||||
|
||||
```powershell
|
||||
# 1. Get exact error body from traefik
|
||||
docker exec traefik wget -O - --server-response https://maljarka.tandemmebel.ru/ 2>&1 | Select-Object -First 30
|
||||
|
||||
# 2. Enable traefik access logs если не включены (data/traefik.yml accessLog: filePath)
|
||||
docker exec traefik cat /etc/traefik/traefik.yml | Select-String 'accessLog'
|
||||
|
||||
# 3. Restart traefik (brief ~1-2s interruption ВСЕХ routes)
|
||||
docker restart traefik
|
||||
Start-Sleep 5
|
||||
docker exec traefik wget --spider https://maljarka.tandemmebel.ru/ 2>&1 | Select-Object -First 3
|
||||
|
||||
# 4. Bytewise compare yml files
|
||||
Get-FileHash *.yml -Algorithm MD5 | Where-Object Path -Match '(maljarka|tandemmebel)\.yml$'
|
||||
Get-Content C:\...\maljarka.yml -Raw -Encoding Byte | Format-Hex | Select-Object -First 5
|
||||
```
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] Является ли `maljarka.tandemmebel.ru` критичным для бизнеса (real traffic) или это test-subdomain? Если real — bump priority.
|
||||
- [ ] Существует ли site binding для этого hostname в IIS `snolla` site, или CMS catch-all'ит через `*` binding?
|
||||
|
||||
## Notes
|
||||
|
||||
- Скоприровано из close-out [[iis-traefik-dead-routes-cleanup]] как side-finding.
|
||||
- Атомарных изменений на инфре не делал — задача чисто диагностическая.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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]].
|
||||
@@ -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).
|
||||
@@ -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]].
|
||||
@@ -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 | Когда обязателен |
|
||||
|---|---|---|
|
||||
| `&` | `&` | **всегда** (в attribute value и в text content) |
|
||||
| `<` | `<` | **всегда** |
|
||||
| `"` | `"` | если значение в `"..."` (наш кейс — атрибуты обычно в двойных кавычках) |
|
||||
| `'` | `'` | если значение в `'...'` |
|
||||
| `>` | `>` | формально не обязателен, но 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 (& -> &) -->
|
||||
<add name="MoreThenCmsEntities"
|
||||
connectionString="...Password=^I9D)LB)DK)8J#xBDG$t}W&_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 '&', '&'
|
||||
# и потом подставлять $xmlSafePassword в Web.config
|
||||
|
||||
# или комбинированный escape всех 5 reserved chars:
|
||||
function Xml-EscapeAttr($s) {
|
||||
$s -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"' -replace "'",'''
|
||||
}
|
||||
```
|
||||
|
||||
## Связанные паттерны
|
||||
|
||||
- [[cms-config-rewrite-pattern]] — UTF-8 BOM ловушка при `Set-Content` без `-Encoding utf8` (другая XML/IIS гнойная тема).
|
||||
- При генерации паролей для DB-логинов CMS — лучше **запретить `&`, `<`, `"`** в генераторе, чтобы не словить эту проблему в будущем (особенно при автоматизированных patches Web.config через CI).
|
||||
Reference in New Issue
Block a user