Phases two and three ship together, because their code grew in the same files. With consent the app asks the router to open 80, 443 and a high port for ten minutes (UPnP, NAT-PMP or PCP), and the server, holding a single-use ticket bound to the caller's own address, connects back and must get an HMAC only the app can give; the mappings are removed after the run and remembered until then, so a crash cannot leave one open. Hairpin is tried from inside. Two STUN servers, the outside one first, tell easy from hard NAT. Mail gets port 25 to Gmail and Outlook, the reverse name and five blocklists through the user's own resolver, network-wide UCEPROTECT listings counted as a caution. DNS, speed against the server (four at a time, twelve an hour per address) and the device follow, and six uses (website, home from outside, files, game server, fediverse, mail) close the run as possible, possible with a named workaround, or can't tell. Findings can be shared after reading the exact JSON: a file per submission under the unit's state directory, ten a day per address, deletable by the token the app keeps, swept after twelve months; /privacy says all of it in English and Italian. QR codes lead to the downloads. Every language carries every string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
107 lines
8.8 KiB
Markdown
107 lines
8.8 KiB
Markdown
# CLAUDE.md — MeWeb quick reference
|
|
|
|
Read `AGENT.md` first and cite `PATTERNS.md` for code rules. This file holds commands, the map and the pitfalls.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
dotnet build src/MeWeb.Desktop/MeWeb.Desktop.csproj
|
|
dotnet run --project src/MeWeb.Desktop
|
|
dotnet run --project src/MeWeb.Server # ASPNETCORE_URLS=http://127.0.0.1:5481
|
|
dotnet run tools/setup.cs -- --group jdk # pinned JDK 21 into tools/jdk
|
|
dotnet run tools/setup.cs -- --group packaging # pinned nfpm into tools/packaging
|
|
dotnet build src/MeWeb.Android/MeWeb.Android.csproj \
|
|
-p:JavaSdkDirectory=$PWD/tools/jdk/linux-x64/jdk-21.0.12+8 -p:AndroidSdkDirectory=$HOME/Android/Sdk
|
|
dotnet run --project tools/Screenshots -- dist/screenshots [en,it,...]
|
|
dotnet run tools/strings.cs -- check # every language against English
|
|
dotnet run tools/strings.cs -- set Key values.json # one key, every language at once
|
|
dotnet run tools/strings.cs -- merge keys.json # many keys: {"Key": {"en": "…", "it": "…"}}
|
|
dotnet run tools/strings.cs -- import fr fr.json # one language's drafts: {"Key": "…"}
|
|
dotnet run tools/knowledge.cs # provider map -> artifacts/knowledge/ip2asn.bin
|
|
dotnet run tools/languages.cs # regenerate data/languages.json from CLDR
|
|
dotnet publish src/MeWeb.Desktop -c Release -r win-x64 --self-contained -p:PublishSingleFile=true \
|
|
-p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
|
|
dotnet publish src/MeWeb.Desktop -c Release -r osx-arm64 --self-contained
|
|
```
|
|
|
|
The Android workload installs into the user SDK (`dotnet workload install android`); the system SDK in `/opt/dotnet`
|
|
is root-owned, so CI uses the runner user's own `~/.dotnet` (passolo's `tools/release.sh` pattern).
|
|
|
|
## Repository, CI and the variables they read
|
|
|
|
| Name | Where | What |
|
|
|---|---|---|
|
|
| `GITEA_URL` | shell, optional | Gitea base URL for `tools/gitea/*.sh`; default `https://git.thepra.dev` |
|
|
| `GITEA_ORG` | shell, optional | organisation; default `Arasaka` |
|
|
| `GITEA_REPO` | shell, optional | repository for `set_secret.sh`; default `MeWeb` |
|
|
| (git credential store) | `~/.git-credentials` | the only Gitea authentication the scripts use: `git credential fill` for `git.thepra.dev` |
|
|
| `MEWEB_KEYSTORE_B64` | Actions secret | the Android keystore, base64 (`tools/gitea/set_secret.sh`) |
|
|
| `MEWEB_KEYSTORE_PASS` | Actions secret | its password (store and key share it), alias `meweb` |
|
|
| `GITEA_TOKEN` | Actions, automatic | the job token; `release.sh` uses it as `RELEASE_TOKEN` to create the release and attach files |
|
|
| `VERSION`, `COMMIT` | job env | written by `.gitea/scripts/prepare.sh` from `Directory.Build.props` and the commit |
|
|
| `MEWEB_KEYSTORE` | release step | a temporary file in `$RUNNER_TEMP` decoded by `.gitea/scripts/android.sh`, removed on exit |
|
|
|
|
`build.yml` runs on every push to `master`; `release.yml` on a `v*` tag (publishes the Gitea release) or by hand
|
|
(`publish: false` builds every platform and deploys the server only). Both run on the Arasaka org runner `nuvola`,
|
|
user `gitea-runner`, home `/var/lib/gitea-runner`: `tools/sdk.sh` installs the SDK from `global.json` into its
|
|
`~/.dotnet` (the system SDK in `/opt/dotnet` is older and root-owned), and the JDK, nfpm and the Android SDK are
|
|
cached in `~/.cache/meweb`. There is deliberately no `pull_request` trigger: the runner is the production box.
|
|
|
|
The server's one-time root setup is `deploy/max/setup.sh` (user `meweb`, `/var/www/meweb.arasaka.software`,
|
|
`/etc/meweb/meweb.env`, the unit, the runner's sudoers line, the certificate, nginx), run on the box through
|
|
`arasaka.software`'s `tools/max/run.sh`.
|
|
|
|
The Android release key: alias `meweb`, certificate SHA-256
|
|
`5f52cff4f52f4e0b6da6cd044bfceaf78d1d42db24f2470602db7f19cbfdf846`. Never regenerate it.
|
|
|
|
## Map
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `src/MeWeb.Shared` | `Endpoints.cs` (routes and hosts), wire contracts, `MeWebJsonContext`, generated `BuildInfo` |
|
|
| `src/MeWeb.Core` | the checks, no UI: `Net/` (address kinds, local network, TTL route, STUN, the probe's `Answerer`, DNS, speed, the device), `Router/` (UPnP IGD, NAT-PMP, PCP; `PortMapper` opens and removes mappings, `Leftovers` remembers them across a crash), `Mail/` (port 25, reverse name, blocklists through the user's resolver), `Knowledge/` (provider map and profiles), `History/` (checks, shared submissions), `Checks/` (`CheckRunner`, `Verdicts`, `Reach`, `Uses`, `ServerProbe`, `SharePayload`) |
|
|
| `src/MeWeb.App` | Avalonia views and view models, `Services/Localizer.cs` (+ `{s:Tr Key}`), `Strings/<code>.json`, `Resources/Colors.axaml` |
|
|
| `src/MeWeb.Desktop` | desktop head (`meweb` executable), Windows manifest |
|
|
| `src/MeWeb.Android` | Android head (`software.arasaka.meweb`), resources, manifest |
|
|
| `src/MeWeb.Server` | `meweb.arasaka.software`: `/api/info`, `/api/ip`, the download page `/` and `/privacy` (en, it), `/download/{platform}` from the latest Gitea release, the probe (`/api/probe/ticket`, `/api/probe/{id}`) and shared findings (`/api/share`, files under `/var/lib/meweb/shares`, swept after `KeepDays`) |
|
|
| `data/languages.json` | 113 languages: code, autonym, English name, `rtl`, `reviewed` or `draft` |
|
|
| `data/isps/*.json` | provider profiles: AS numbers, and per fact a value, who said it (`basis`), the source URL and the month |
|
|
| `tools/knowledge.cs` | downloads iptoasn's `ip2asn-combined.tsv.gz` (cached in `tools/cache`, 12 h) and writes the brotli table `artifacts/knowledge/ip2asn.bin` (~2 MB), embedded by `MeWeb.Core` when present; `tools/release.cs` runs it before every app build |
|
|
| `tools/setup.cs` + `setup.json` | pinned build tools |
|
|
| `tools/Screenshots` | headless renders of every view in every language |
|
|
| `branding/` | the icon (SVG source, PNG, ICO) |
|
|
|
|
## Pitfalls
|
|
|
|
- **A TTL ping needs an empty payload off-root.** .NET on Linux falls back to the `ping` utility without
|
|
`CAP_NET_RAW` and refuses any custom buffer ("Unable to send custom ping payload"); `SendPingAsync(target, timeout, [],
|
|
new PingOptions(ttl, false))` works and reports `TimeExceeded` with the hop's address.
|
|
- **A 100.64.x.x hop is not proof of CGNAT.** The owner's Dimensione line has a public IPv4 on the FRITZ!Box and still
|
|
shows `100.64.255.255` as hop 2. Only the router's own WAN address compared with the echo decides; hops are a hint.
|
|
- **Routers answer the SSDP search they don't implement.** The FRITZ!Box 5530 answers an IGD:2 search while describing
|
|
itself as IGD:1; the version comes from the description's `deviceType`. Its PCP answers `NOT_AUTHORIZED` (2).
|
|
- **`HeadlessUnitTestSession.Dispatch` builds a fresh `Application` each call**, and `AttachDeveloperTools` throws the
|
|
second time: the screenshot tool does the real check and every shot inside one `Dispatch`.
|
|
- **Fluent's `Expander` paints its header from resources** (`ExpanderHeaderBackground`, …), not from `Background`; the
|
|
`Expander.explain` style overrides them in `Style.Resources`.
|
|
- **The probe never trusts the client for a target.** A ticket is bound to the address the server saw, single use,
|
|
60 s; the run refuses any other caller address, private and CGNAT space, and `Probe:Refuse` (the box's own
|
|
addresses, so the owner cannot probe himself from home). `Probe__AllowPrivate=true` exists for a local server only.
|
|
- **Ask the outside STUN server first.** When `stun.meweb` is the caller's own public address (the owner's line), the
|
|
first binding hairpins inside the FRITZ!Box with the port preserved and the next destination reuses that mapping,
|
|
so the order `stun.nextcloud.com` then `stun.meweb` is what reads a symmetric NAT as hard.
|
|
- **Shared findings are files, not Mongo.** The box's Mongo has auth on and me-web? has no user there; a file per
|
|
submission under the unit's `StateDirectory`, with only the delete token's SHA-256, needs no credentials.
|
|
- **An address inside right-to-left text reorders around `→`.** `Texts.Isolate` wraps every literal value in U+2068/U+2069.
|
|
|
|
- **Avalonia 12 Android head:** `[Application] MainApplication : AvaloniaAndroidApplication<App>` plus a non-generic
|
|
`AvaloniaMainActivity`; the main view comes through `IActivityApplicationLifetime.MainViewFactory`, checked before
|
|
`ISingleViewApplicationLifetime`.
|
|
- **`ApplicationVersion` must be an integer**: computed from `Version` through separate `VersionMajor/Minor/Patch`
|
|
properties; one nested property function left it unevaluated (`XA0003`).
|
|
- **A headless Avalonia session does not end on its own** while the pulse animation runs: the screenshot tool calls
|
|
`Environment.Exit(0)` after dispatching.
|
|
- **The macOS apphost is ad-hoc signed by the .NET 10 SDK even on Linux** (`LC_CODE_SIGNATURE` present); Apple
|
|
Silicon refuses unsigned arm64 code, so never strip it.
|
|
- **The Windows single-file publish drops native `.pdb` files beside the exe**; only `meweb.exe` ships.
|