Skip to content

Commit e34819e

Browse files
committed
first commit
0 parents  commit e34819e

35 files changed

Lines changed: 6216 additions & 0 deletions

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
frontend/node_modules
3+
frontend/dist
4+
.git
5+
.gitignore
6+
__pycache__
7+
*.pyc
8+
.venv
9+
.pytest_cache

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
8+
jobs:
9+
backend-test:
10+
runs-on: ubuntu-latest
11+
defaults:
12+
run:
13+
working-directory: backend
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: '3.12'
19+
- name: Install deps
20+
run: |
21+
python -m pip install -U pip
22+
pip install -e .[dev]
23+
- name: Lint
24+
run: |
25+
ruff check . || true
26+
- name: Test
27+
run: pytest -q
28+
29+
frontend-build:
30+
runs-on: ubuntu-latest
31+
defaults:
32+
run:
33+
working-directory: frontend
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: actions/setup-node@v4
37+
with:
38+
node-version: '20'
39+
cache: 'npm'
40+
cache-dependency-path: frontend/package-lock.json
41+
- name: Install
42+
run: |
43+
npm ci || npm install
44+
- name: Build
45+
run: npm run build --if-present
46+
47+
docker-build:
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v4
51+
- name: Build Docker image
52+
run: docker build . -t httpintercepter:ci

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# General
2+
.DS_Store
3+
node_modules/
4+
.env
5+
__pycache__/
6+
*.pyc
7+
*.pyo
8+
*.pyd
9+
*.egg-info/
10+
.venv/
11+
12+
# Frontend
13+
frontend/dist/
14+
15+
# Python tooling
16+
.pytest_cache/
17+
.ruff_cache/

.vscode/tasks.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Backend tests",
6+
"type": "shell",
7+
"command": "cmd",
8+
"args": [
9+
"/c",
10+
"python -m venv .venv && .venv\\Scripts\\python -m pip install -U pip && .venv\\Scripts\\pip install -e backend[dev] && .venv\\Scripts\\pytest -q backend"
11+
],
12+
"problemMatcher": [
13+
"$pytest"
14+
],
15+
"group": "build"
16+
}
17+
]
18+
}

Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Multi-stage build for frontend + backend
2+
3+
# 1) Frontend build
4+
FROM node:20-alpine AS frontend
5+
WORKDIR /app
6+
COPY frontend/package.json frontend/package-lock.json* ./
7+
RUN npm ci || npm install
8+
COPY frontend/ ./
9+
RUN npm run build
10+
11+
# 2) Backend runtime
12+
FROM python:3.12-slim AS runtime
13+
WORKDIR /app
14+
ENV PYTHONDONTWRITEBYTECODE=1
15+
ENV PYTHONUNBUFFERED=1
16+
17+
# Install backend deps
18+
COPY backend/pyproject.toml ./backend/pyproject.toml
19+
RUN pip install --no-cache-dir -U pip setuptools && \
20+
pip install --no-cache-dir -e ./backend[dev]
21+
22+
# Copy backend code and static
23+
COPY backend/ ./backend/
24+
COPY --from=frontend /app/dist ./frontend-dist
25+
26+
ENV FRONTEND_DIST_DIR=/app/frontend-dist
27+
EXPOSE 8181
28+
CMD ["python", "-m", "uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8181"]

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# HTTP Intercepter
2+
3+
A tiny debug HTTP interceptor: it exposes an `/inbound` endpoint that accepts any POST request, stores it in memory, and shows it in a minimalist Vue UI. Includes a FastAPI backend API for listing, viewing, and deleting captured requests.
4+
5+
## Structure
6+
7+
- `backend/` — FastAPI app, pytest tests
8+
- `frontend/` — Vue 3 (Vite) SPA
9+
- `Dockerfile` — Multi-stage build to bundle both
10+
11+
## Backend (dev)
12+
13+
- Create a virtual env and install:
14+
15+
```cmd
16+
python -m venv .venv
17+
.venv\Scripts\activate
18+
pip install -e backend[dev]
19+
```
20+
21+
- Run the API locally:
22+
23+
```cmd
24+
python -m uvicorn app.main:app --reload --port 8181 --app-dir backend
25+
```
26+
27+
The frontend dev server (below) proxies `/api` and `/inbound` to 8181.
28+
29+
## Frontend (dev)
30+
31+
```cmd
32+
cd frontend
33+
npm install
34+
npm run dev
35+
```
36+
37+
Open http://localhost:5173
38+
39+
## Docker
40+
41+
Build and run the combined image:
42+
43+
```cmd
44+
docker build -t httpintercepter:local .
45+
docker run --rm -p 8181:8181 httpintercepter:local
46+
```
47+
48+
Open http://localhost:8181
49+
50+
## API
51+
52+
- `POST /inbound` — store inbound request; returns OK
53+
- `GET /api/requests` — list summaries
54+
- `GET /api/requests/{id}` — full record
55+
- `DELETE /api/requests/{id}` — delete one
56+
- `DELETE /api/requests` — delete all
57+
58+
Note: storage is in-memory and resets on restart.

backend/MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
recursive-include app *.py

backend/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Backend quickstart
2+
3+
- Create venv and install deps:
4+
5+
```cmd
6+
python -m venv .venv
7+
.venv\Scripts\activate
8+
pip install -e .[dev]
9+
```
10+
11+
- Run dev server:
12+
13+
```cmd
14+
python -m uvicorn app.main:app --reload --port 8181 --app-dir backend
15+
```

backend/app/__init__.py

Whitespace-only changes.

backend/app/main.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
from typing import Dict, List, Optional
5+
import base64
6+
from fastapi import FastAPI, Request, Response, HTTPException
7+
from fastapi.staticfiles import StaticFiles
8+
import os
9+
from pydantic import BaseModel
10+
11+
app = FastAPI(title="HTTP Intercepter Backend")
12+
13+
14+
class RequestSummary(BaseModel):
15+
id: int
16+
method: str
17+
path: str
18+
ts: float
19+
ip: str
20+
content_length: int
21+
22+
23+
class StoredRequest(BaseModel):
24+
id: int
25+
method: str
26+
path: str
27+
ts: float
28+
ip: str
29+
headers: Dict[str, str]
30+
query: Dict[str, str]
31+
body_text: Optional[str] = None
32+
body_bytes_b64: Optional[str] = None
33+
body_length: int = 0
34+
35+
36+
_requests: List[StoredRequest] = []
37+
_next_id = 1
38+
39+
40+
@app.api_route("/inbound", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
41+
async def inbound(request: Request):
42+
global _next_id
43+
body = await request.body()
44+
try:
45+
body_text = body.decode("utf-8") if body else None
46+
except UnicodeDecodeError:
47+
body_text = None
48+
item = StoredRequest(
49+
id=_next_id,
50+
method=request.method,
51+
path=request.url.path,
52+
ts=datetime.utcnow().timestamp(),
53+
ip=request.client.host if request.client else "",
54+
headers={k: v for k, v in request.headers.items()},
55+
query={k: v for k, v in request.query_params.items()},
56+
body_text=body_text,
57+
body_bytes_b64=(base64.b64encode(body).decode("ascii") if body and body_text is None else None),
58+
body_length=len(body) if body else 0,
59+
)
60+
_requests.append(item)
61+
_next_id += 1
62+
return Response(content="OK", media_type="text/plain")
63+
64+
65+
@app.get("/api/requests", response_model=List[RequestSummary])
66+
async def list_requests():
67+
return [
68+
RequestSummary(
69+
id=r.id,
70+
method=r.method,
71+
path=r.path,
72+
ts=r.ts,
73+
ip=r.ip,
74+
content_length=r.body_length,
75+
)
76+
for r in reversed(_requests)
77+
]
78+
79+
80+
@app.get("/api/requests/{req_id}", response_model=StoredRequest)
81+
async def get_request(req_id: int):
82+
for r in _requests:
83+
if r.id == req_id:
84+
return r
85+
raise HTTPException(status_code=404, detail="Request not found")
86+
87+
88+
@app.delete("/api/requests/{req_id}")
89+
async def delete_request(req_id: int):
90+
global _requests
91+
before = len(_requests)
92+
_requests = [r for r in _requests if r.id != req_id]
93+
if len(_requests) == before:
94+
raise HTTPException(status_code=404, detail="Request not found")
95+
return {"ok": True}
96+
97+
98+
@app.delete("/api/requests")
99+
async def delete_all_requests():
100+
_requests.clear()
101+
return {"ok": True}
102+
103+
104+
@app.get("/healthz")
105+
async def healthz():
106+
return {"ok": True}
107+
108+
@app.get("/")
109+
async def index():
110+
return {"name": "http-intercepter", "status": "running"}
111+
112+
# Optionally serve built frontend if present (Docker production)
113+
_dist_dir = os.getenv("FRONTEND_DIST_DIR", "frontend-dist")
114+
if os.path.isdir(_dist_dir):
115+
app.mount("/", StaticFiles(directory=_dist_dir, html=True), name="static")

0 commit comments

Comments
 (0)