Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/BRANCH-PROTECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ Configure in GitHub **Settings → Rules → Rulesets** (or classic branch prote
| Require approvals | recommended (≥1 maintainer) |
| Require conversation resolution | recommended |
| Require status checks to pass | **yes** |
| Required checks | `.NET / build`, `.NET / ui-portable` (all matrix legs), `RPS saturation / rps` |
| Required checks | `.NET / build`, `.NET / ui-portable` (all 3 OS), `.NET / cli-e2e` (all 3 OS), `RPS saturation / rps` (all 3 OS on PR `compare-spot`) |
| Allow bypass | admins only for emergencies |

Exact check names must match the Actions job names as shown on a PR (including matrix suffixes). The checked-in template is [`.github/branch-ruleset-beta-stable.json`](branch-ruleset-beta-stable.json) — after the first green PR, confirm the matrix suffixes in the Actions UI and adjust the JSON if GitHub renamed a leg.

## `develop`

Keep lighter rules for velocity (direct push OK if that is current practice). Do **not** require full `compare-product` on every feature PR.
Expand All @@ -27,4 +29,6 @@ Keep lighter rules for velocity (direct push OK if that is current practice). Do
gh api repos/{owner}/{repo}/rulesets --input .github/branch-ruleset-beta-stable.json
```

Exact check names must match the Actions job names as shown on a PR (including matrix suffixes for `ui-portable`).
## Stable release RPS

After merge to `stable`, [`release.yml`](workflows/release.yml) runs Linux **`compare-product-smoke`** before the GA GitHub Release. Full 3-OS wiki-grade `compare-product` stays a maintainer `workflow_dispatch` on [`rps-saturation.yml`](workflows/rps-saturation.yml).
46 changes: 46 additions & 0 deletions .github/branch-ruleset-beta-stable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "release-lines",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/heads/beta", "refs/heads/stable"],
"exclude": []
}
},
"rules": [
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 1,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": true,
"allowed_merge_methods": ["merge", "squash", "rebase"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"do_not_enforce_on_create": false,
"required_status_checks": [
{ "context": ".NET / build" },
{ "context": ".NET / ui-portable (windows-latest)" },
{ "context": ".NET / ui-portable (ubuntu-latest)" },
{ "context": ".NET / ui-portable (macos-latest)" },
{ "context": ".NET / cli-e2e (windows-latest)" },
{ "context": ".NET / cli-e2e (ubuntu-latest)" },
{ "context": ".NET / cli-e2e (macos-latest)" },
{ "context": "RPS saturation / rps (ubuntu-latest)" },
{ "context": "RPS saturation / rps (windows-latest)" },
{ "context": "RPS saturation / rps (macos-15-intel)" }
]
}
},
{ "type": "non_fast_forward" },
{ "type": "deletion" }
],
"bypass_actors": []
}
26 changes: 26 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
version: 2
updates:
- package-ecosystem: nuget
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 10
labels:
- dependencies
groups:
nuget-minor-patch:
patterns:
- "*"
update-types:
- minor
- patch

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
labels:
- dependencies
42 changes: 42 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CodeQL

# Default advanced CodeQL already analyzes PRs. This workflow is a weekly/manual
# csharp scan with an explicit Restore+Build so release.yml can keep deleting the
# hosted CodeQL cache for disk without losing a scheduled security signal.
on:
schedule:
- cron: '17 7 * * 1'
workflow_dispatch:

permissions:
contents: read
security-events: write

jobs:
analyze:
name: Analyze (csharp)
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v6

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
build-mode: manual

- uses: actions/setup-dotnet@v5
with:
dotnet-version: |
10.0.x

- name: Build for CodeQL
run: |
dotnet restore src/Titanium.Web.Proxy.sln
dotnet build src/Titanium.Web.Proxy.sln -c Release --no-restore

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:csharp/manual-build"
41 changes: 35 additions & 6 deletions .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ jobs:
Add-Content -Path $env:GITHUB_PATH -Value "$env:USERPROFILE\.dotnet\tools"

- name: Restore
run: dotnet restore src/Titanium.Web.Proxy.sln --warnaserror
run: |
# Fail restore on high/critical vulnerable packages (NuGet audit via MSBuild).
dotnet restore src/Titanium.Web.Proxy.sln --warnaserror -p:NuGetAudit=true -p:NuGetAuditLevel=high

- name: Build
run: dotnet build src/Titanium.Web.Proxy.sln --configuration Release --no-restore --warnaserror
Expand Down Expand Up @@ -412,7 +414,7 @@ jobs:
- name: compare-editions (publish gate)
shell: pwsh
run: |
# Stable tags additionally require compare-product PASS via release.yml on that SHA.
# Stable GitHub Release additionally requires Linux compare-product-smoke via release.yml.
pwsh tools/RpsLoadProbe/run-rps.ps1 `
-Mode compare-editions `
-Concurrency '8,16,32,64' `
Expand Down Expand Up @@ -461,7 +463,7 @@ jobs:

publish:
if: github.event_name == 'push' && (github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/stable')
needs: [build, ui-portable, rps-publish-gate, rps-peer-gate]
needs: [build, ui-portable, cli-e2e, rps-publish-gate, rps-peer-gate]
runs-on: windows-latest
environment: nuget-publish
permissions:
Expand All @@ -478,6 +480,9 @@ jobs:
dotnet-version: |
10.0.x

- name: NuGet audit before pack
run: dotnet restore src/Titanium.Web.Proxy.sln -p:NuGetAudit=true -p:NuGetAuditLevel=high

- name: Pack Beta
if: github.ref == 'refs/heads/beta'
run: |
Expand All @@ -492,6 +497,27 @@ jobs:
dotnet pack src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj --configuration Release
dotnet pack src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj --configuration Release

- name: Generate CycloneDX SBOM for MIT packages
shell: pwsh
run: |
dotnet tool install --global CycloneDX --version 6.2.0
New-Item -ItemType Directory -Force -Path sbom | Out-Null
foreach ($proj in @(
'src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj',
'src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj',
'src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj'
)) {
$name = [IO.Path]::GetFileNameWithoutExtension($proj)
dotnet CycloneDX $proj -o "sbom" -F Json -fn "$name.cdx.json" -t --disable-package-restore
}

- name: Upload MIT package SBOM
uses: actions/upload-artifact@v4
with:
name: mit-nuget-sbom
path: sbom/*.cdx.json
if-no-files-found: warn

- name: Log in to NuGet
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1
id: login
Expand All @@ -513,10 +539,10 @@ jobs:
# Product zips stay on release.yml (v* tags). After beta/stable merge, create/move the
# version tag and dispatch release.yml (GITHUB_TOKEN tag pushes do not re-trigger workflows).
# release.yml also packs/pushes Chocolatey (titanium-cli / titanium-inspector) after the
# GitHub Release exists — no separate chocolatey step here.
# GitHub Release exists — skipped while vars.SKIP_CATALOG_PUBLISH=true.
cut-product-tag:
if: github.event_name == 'push' && (github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/stable')
needs: [build, ui-portable, rps-publish-gate, rps-peer-gate]
needs: [build, ui-portable, cli-e2e, rps-publish-gate, rps-peer-gate]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -528,6 +554,7 @@ jobs:
- name: Create/move product tag and dispatch release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SKIP_CATALOG_PUBLISH: ${{ vars.SKIP_CATALOG_PUBLISH }}
run: |
set -euo pipefail
PREFIX=$(grep -oP '(?<=<VersionPrefix>)[^<]+' src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj | head -n1)
Expand All @@ -544,5 +571,7 @@ jobs:
git tag -f -a "$TAG" -m "$TAG" "$GITHUB_SHA"
git push origin "refs/tags/$TAG" --force
echo "Dispatching product release for $TAG (channel=$CHANNEL) at $GITHUB_SHA"
gh workflow run release.yml --ref "$GITHUB_REF_NAME" -f channel="$CHANNEL"
SKIP_FLAG=false
if [[ "${SKIP_CATALOG_PUBLISH:-}" == "true" ]]; then SKIP_FLAG=true; fi
gh workflow run release.yml --ref "$GITHUB_REF_NAME" -f channel="$CHANNEL" -f skip_catalog_publish="$SKIP_FLAG"

87 changes: 83 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ on:
description: 'stable or beta'
required: true
default: 'stable'
skip_catalog_publish:
description: 'Skip Chocolatey (and any future catalog) publish'
required: false
type: boolean
default: false

permissions:
contents: read
Expand Down Expand Up @@ -294,7 +299,7 @@ jobs:
path: staged-cli-${{ matrix.rid }}/
if-no-files-found: error
build-plus:
needs: [resolve-version, test]
needs: [resolve-version, test, cli-e2e]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -551,9 +556,66 @@ jobs:
grep -qi 'IsSupported' /cli/smoke-out.txt
test -e /cli/libmsquic.so -o -e /cli/libmsquic.so.2 -o -e /cli/libmsquic.so.2.5.7

# Stable-only Linux product RPS smoke (not wiki-grade 3-OS median-of-3).
# Beta channel skips this; spot + editions already ran on the merge SHA.
rps-product-smoke:
needs: [resolve-version]
if: needs.resolve-version.outputs.channel == 'stable'
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Install libmsquic (HTTP/3)
run: |
set -euo pipefail
. /etc/os-release
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
"https://packages.microsoft.com/config/${ID}/${VERSION_ID}/packages-microsoft-prod.deb" \
-o packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm -f packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y libmsquic
pwsh -NoProfile -Command 'if (-not [System.Net.Quic.QuicListener]::IsSupported) { throw "QuicListener.IsSupported is false after libmsquic install" }; Write-Host "QuicListener.IsSupported=$([System.Net.Quic.QuicListener]::IsSupported)"'
- name: compare-product-smoke (Linux)
shell: pwsh
run: |
pwsh tools/RpsLoadProbe/run-rps.ps1 `
-Mode compare-product-smoke `
-Concurrency '8,16,32,64' `
-WarmupSec 2 `
-DurationSec 8 `
-Repeats 1 `
-ResultsDir tools/RpsLoadProbe/results `
-BombardierCheck
$csv = Get-ChildItem tools/RpsLoadProbe/results -Filter 'rps-ramp-*.csv' |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $csv) { throw 'No CSV found for compare-product-smoke gate validation' }
pwsh tools/RpsLoadProbe/validate-compare-product-gates.ps1 -CsvPath $csv.FullName
- name: Upload smoke CSV
if: always()
uses: actions/upload-artifact@v4
with:
name: rps-product-smoke-csv
path: tools/RpsLoadProbe/results/rps-ramp-*.csv
if-no-files-found: ignore

publish-release:
needs: [resolve-version, build-cli, build-plus, build-inspector, smoke-http3]
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
needs: [resolve-version, build-cli, build-plus, build-inspector, smoke-http3, rps-product-smoke]
if: |
always() &&
(startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') &&
needs.resolve-version.result == 'success' &&
needs.build-cli.result == 'success' &&
needs.build-plus.result == 'success' &&
needs.build-inspector.result == 'success' &&
needs.smoke-http3.result == 'success' &&
(needs.rps-product-smoke.result == 'success' || needs.rps-product-smoke.result == 'skipped')
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -565,6 +627,15 @@ jobs:
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Attach MIT NuGet SBOM when present
continue-on-error: true
run: |
set -euo pipefail
mkdir -p artifacts/sbom
# Optional artifact from .NET / publish (same SHA cut); ignore if missing.
if [[ -d artifacts/mit-nuget-sbom ]]; then
cp -a artifacts/mit-nuget-sbom/. artifacts/sbom/ || true
fi
- name: Build release-manifest and create GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -583,7 +654,12 @@ jobs:
find artifacts -type f \( \
-name '*.zip' -o -name '*.dll' -o -name '*.msi' \
-o -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*.dmg' \
-o -name '*.cdx.json' \
\) -exec cp {} dist/ \;
# Prefer explicit SBOM folder copied by the prior step when present.
if [[ -d artifacts/sbom ]]; then
cp -a artifacts/sbom/. dist/ || true
fi
python3 - <<'PY'
import hashlib, json, os, pathlib, datetime
tag = os.environ["RELEASE_TAG"]
Expand Down Expand Up @@ -710,7 +786,10 @@ jobs:

publish-chocolatey:
needs: [resolve-version, publish-release]
if: needs.publish-release.result == 'success'
if: |
needs.publish-release.result == 'success' &&
vars.SKIP_CATALOG_PUBLISH != 'true' &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_catalog_publish != 'true')
runs-on: windows-latest
permissions:
contents: read
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/rps-saturation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ name: RPS saturation
on:
pull_request:
branches: [beta, stable]
push:
branches: [beta, stable]
# Push editions are gated by .NET / rps-publish-gate (Ubuntu only). Do not duplicate
# 3-OS compare-editions on every beta/stable merge.
workflow_dispatch:
inputs:
mode:
Expand Down Expand Up @@ -203,11 +203,11 @@ permissions:

jobs:
rps:
# PR → beta/stable: compare-spot; push → beta/stable: compare-editions; dispatch: inputs.mode
# PR → beta/stable: compare-spot; dispatch: inputs.mode
permissions:
contents: read
env:
RPS_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || (github.event_name == 'pull_request' && 'compare-spot' || 'compare-editions') }}
RPS_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'compare-spot' }}
RPS_CONCURRENCY: ${{ github.event_name == 'workflow_dispatch' && inputs.concurrency || '8,16,32,64' }}
RPS_WARMUP_SEC: ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_sec || 2 }}
RPS_DURATION_SEC: ${{ github.event_name == 'workflow_dispatch' && inputs.duration_sec || 8 }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Requires .NET 10 or later for the library. CLI and Inspector downloads are self-
## What you can do

- Decrypt and inspect HTTPS in a native desktop Inspector on Windows, macOS, and Linux — AutoResponder, Map Local/Remote, breakpoints, Composer, HAR, curl/fetch
- Keep sign-in working: SSO hosts stay on OS bypass; hosts that reject MITM auto-tunnel
- When a server blocks the proxy (TLS fingerprinting or other reasons), we detect it and automatically tunnel that traffic
- Speak modern HTTP — HTTP/2 by default, optional HTTP/3, WebSocket, gRPC, SSE, GraphQL rules in the same grid
- Run the same engine as a reverse / edge proxy from the CLI (YAML, load balancing, ACME, live reload)
- Embed in .NET via NuGet (MIT); Inspector is free for personal and education use
Expand Down
Loading
Loading