Skip to content

Commit d6a9584

Browse files
authored
feat: add macOS Keychain integration for CA certificate (#51)
1 parent 1df4730 commit d6a9584

18 files changed

Lines changed: 731 additions & 66 deletions

File tree

.claude/commands/ci.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Commit working changes and push them to CI
22

3-
Run `cargo clippy -- -D warning` before pushing changes.
3+
Run `cargo clippy -- -D warnings` before pushing changes.
44

55
Also fetch the latest version of the base branch and merge it into the current branch.
66

7-
Enter loop where you wait for CI to complete, resolve issues,
8-
and return to user once CI is green or a major decision is needed
9-
to resolve it.
7+
Enter loop where you wait for CI to complete using `./scripts/wait-pr-checks.sh`
8+
(auto-detects PR from current branch), resolve issues, and return to user once
9+
CI is green or a major decision is needed to resolve it.

.config/nextest.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,8 @@ failure-output = "final"
2222
success-output = "never"
2323
status-level = "retry"
2424

25+
# Test timeout - terminate tests that run longer than 30 seconds
26+
slow-timeout = { period = "30s", terminate-after = 1 }
27+
2528
# Allow more retries in CI for flaky tests
2629
retries = { backoff = "exponential", count = 2, delay = "1s", max-delay = "10s" }

.github/workflows/tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ jobs:
5454
fi
5555
- uses: actions/checkout@v4
5656

57+
- name: Install Rust
58+
uses: dtolnay/rust-toolchain@stable
59+
with:
60+
toolchain: stable
61+
5762
- name: Fix permissions on current directory
5863
run: |
5964
# Clean up any files left from previous sudo runs

CLAUDE.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ Run the full suite:
4949
cargo test
5050
```
5151

52+
### Test Performance Requirements
53+
54+
**All tests must complete within seconds, not minutes.** The CI timeout is set to 30 seconds per test. Tests that require longer operations (like timeouts) should use minimal durations:
55+
56+
- Use `HttpjailCommand::timeout(2)` for timeout tests with `sleep 3`
57+
- Network tests should use `--connect-timeout 5 --max-time 8` for curl commands
58+
- Any test taking longer than a few seconds should be optimized or redesigned
59+
60+
This ensures fast feedback during development and prevents CI timeouts.
61+
5262
## Cargo Cache
5363

5464
Occasionally you will encounter permissions issues due to running the tests under sudo. In these cases,
@@ -62,6 +72,15 @@ DO NOT `cargo clean`. Instead, `chown -R <user> target`.
6272
cargo test --test weak_integration
6373
```
6474

75+
### Certificate Trust on macOS
76+
77+
- **curl and most CLI tools**: Respect the `SSL_CERT_FILE`/`SSL_CERT_DIR` environment variables that httpjail sets, so they work even without the CA in the system keychain
78+
- **Go programs (gh, go, etc.)**: Use the macOS Security.framework and ignore environment variables, requiring the CA to be installed in the keychain via `httpjail trust --install`
79+
- When the CA is not trusted in the keychain, httpjail will:
80+
- Still attempt TLS interception (not pass-through)
81+
- Warn that applications may fail with certificate errors
82+
- Go programs will fail to connect until `httpjail trust --install` is run
83+
6584
## Documentation
6685

6786
User-facing documentation should be in the README.md file.
@@ -107,8 +126,9 @@ The CI workspace is located at `/home/ci/actions-runner/_work/httpjail/httpjail`
107126
./scripts/ci-scp.sh root@ci-1:/path/to/file ./ # Download
108127

109128
# Wait for PR checks to pass or fail
110-
./scripts/wait-pr-checks.sh 47 # Monitor PR #47
111-
./scripts/wait-pr-checks.sh 47 coder/httpjail # Specify repo explicitly
129+
./scripts/wait-pr-checks.sh # Auto-detect PR from current branch
130+
./scripts/wait-pr-checks.sh 47 # Monitor specific PR #47
131+
./scripts/wait-pr-checks.sh 47 coder/httpjail # Specify PR and repo explicitly
112132
```
113133

114134
### Manual Testing on CI

Cargo.lock

Lines changed: 22 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ url = "2.5"
4040
v8 = "129"
4141

4242
[target.'cfg(target_os = "macos")'.dependencies]
43-
nix = { version = "0.27", features = ["user"] }
4443
libc = "0.2"
44+
atty = "0.2"
4545

4646
[target.'cfg(target_os = "linux")'.dependencies]
4747
libc = "0.2"
@@ -53,6 +53,9 @@ assert_cmd = "2.0"
5353
predicates = "3.0"
5454
serial_test = "3.0"
5555

56+
[package.metadata.cargo-udeps.ignore]
57+
dev = ["serial_test"]
58+
5659
[profile.fast]
5760
inherits = "release"
5861
opt-level = 1

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ httpjail creates an isolated network environment for the target process, interce
126126
### macOS
127127

128128
- No special permissions required (runs in weak mode)
129+
- **Automatic keychain trust:** On first run, httpjail will attempt to automatically install its CA certificate to your user keychain (with macOS password prompt). This enables HTTPS interception for most applications.
130+
- **Manual keychain management:**
131+
- `httpjail trust` - Check if the CA certificate is trusted
132+
- `httpjail trust --install` - Manually install CA to user keychain (with prompt)
133+
- `httpjail trust --remove` - Remove CA from keychain
134+
- **Application compatibility:**
135+
- ✅ Most CLI tools (curl, npm, etc.) work with environment variables or keychain trust
136+
- ❌ Go programs (gh, go) require keychain trust and may fail until `httpjail trust --install` is run
137+
- ❌ Some applications may bypass proxy settings entirely
129138

130139
## Configuration File
131140

scripts/wait-pr-checks.sh

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,52 @@
11
#!/bin/bash
22
# wait-pr-checks.sh - Poll GitHub Actions status for a PR and exit on first failure or when all pass
33
#
4-
# Usage: ./scripts/wait-pr-checks.sh <pr-number> [repo]
5-
# pr-number: The PR number to check
4+
# Usage: ./scripts/wait-pr-checks.sh [pr-number] [repo]
5+
# pr-number: Optional PR number (auto-detects from current branch if not provided)
66
# repo: Optional repository in format owner/repo (defaults to coder/httpjail)
77
#
88
# Exit codes:
99
# 0 - All checks passed
1010
# 1 - A check failed
11-
# 2 - Invalid arguments
11+
# 2 - Invalid arguments or no PR found
1212
#
1313
# Requires: gh, jq
1414

1515
set -euo pipefail
1616

17-
# Parse arguments
18-
if [ $# -lt 1 ]; then
19-
echo "Usage: $0 <pr-number> [repo]" >&2
20-
echo "Example: $0 47" >&2
21-
echo "Example: $0 47 coder/httpjail" >&2
22-
exit 2
17+
# Parse arguments and auto-detect PR if needed
18+
if [ $# -eq 0 ]; then
19+
# Auto-detect PR from current branch
20+
echo "Auto-detecting PR from current branch..." >&2
21+
CURRENT_BRANCH=$(git branch --show-current)
22+
23+
# Try to find PR for current branch
24+
PR_INFO=$(gh pr list --head "${CURRENT_BRANCH}" --json number,headRefName --limit 1 2>/dev/null || echo "[]")
25+
PR_NUMBER=$(echo "$PR_INFO" | jq -r '.[0].number // empty')
26+
27+
if [ -z "$PR_NUMBER" ]; then
28+
echo "Error: No PR found for branch '${CURRENT_BRANCH}'" >&2
29+
echo "" >&2
30+
echo "Usage: $0 [pr-number] [repo]" >&2
31+
echo " When called without arguments, auto-detects PR from current branch" >&2
32+
echo " Examples:" >&2
33+
echo " $0 # Auto-detect PR from current branch" >&2
34+
echo " $0 47 # Monitor PR #47" >&2
35+
echo " $0 47 coder/httpjail # Monitor PR #47 in specific repo" >&2
36+
exit 2
37+
fi
38+
39+
# Auto-detect repo from git remote
40+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "coder/httpjail")
41+
echo "Found PR #${PR_NUMBER} for branch '${CURRENT_BRANCH}' in ${REPO}" >&2
42+
elif [ $# -eq 1 ]; then
43+
PR_NUMBER="$1"
44+
REPO="coder/httpjail"
45+
else
46+
PR_NUMBER="$1"
47+
REPO="$2"
2348
fi
2449

25-
PR_NUMBER="$1"
26-
REPO="${2:-coder/httpjail}"
27-
2850
# Check for required tools
2951
if ! command -v jq &> /dev/null; then
3052
echo "Error: jq is required but not installed" >&2
@@ -71,7 +93,43 @@ while true; do
7193
if [ $failed_count -gt 0 ]; then
7294
echo -e "\n\n${RED}❌ The following check(s) failed:${NC}"
7395
echo "$json_output" | jq -r '.[] | select(.state == "FAILURE" or .state == "ERROR") | " - \(.name)"'
74-
echo -e "\nView details at: https://github.com/${REPO}/pull/${PR_NUMBER}/checks"
96+
97+
# Try to fetch logs for the first failed check
98+
echo -e "\n${YELLOW}Fetching logs for first failed check...${NC}\n"
99+
100+
# Get the first failed check details
101+
first_failed=$(echo "$json_output" | jq -r '.[] | select(.state == "FAILURE" or .state == "ERROR") | "\(.name)|\(.link)"' | head -1)
102+
103+
if [ -n "$first_failed" ]; then
104+
IFS='|' read -r check_name check_link <<< "$first_failed"
105+
echo -e "${YELLOW}=== Logs for: ${check_name} ===${NC}"
106+
107+
# Extract run ID and job ID from the link
108+
if [[ "$check_link" =~ /runs/([0-9]+)/job/([0-9]+) ]]; then
109+
run_id="${BASH_REMATCH[1]}"
110+
job_id="${BASH_REMATCH[2]}"
111+
112+
# Use direct API call to get job logs (more reliable than gh run view)
113+
if job_logs=$(gh api "repos/${REPO}/actions/jobs/${job_id}/logs" --paginate 2>&1); then
114+
# Look for error patterns in the logs
115+
error_logs=$(echo "$job_logs" | grep -E "(error:|Error:|ERROR:|warning:|clippy::|failed|Failed|##\[error\])" | head -30)
116+
if [ -n "$error_logs" ]; then
117+
echo "$error_logs"
118+
else
119+
# If no error patterns found, show last 100 lines which often contain the failure
120+
echo "$job_logs" | tail -100
121+
fi
122+
else
123+
# If logs aren't ready, try to at least show the conclusion
124+
echo "Full logs not available yet. Check: ${check_link}"
125+
fi
126+
else
127+
echo "Could not parse check link: ${check_link}"
128+
fi
129+
echo ""
130+
fi
131+
132+
echo -e "\nView full details at: https://github.com/${REPO}/pull/${PR_NUMBER}/checks"
75133
exit 1
76134
fi
77135

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod dangerous_verifier;
22
pub mod jail;
3+
pub mod macos_keychain;
34
pub mod proxy;
45
pub mod proxy_tls;
56
pub mod rules;

0 commit comments

Comments
 (0)