31 changed files with 462 additions and 129 deletions
+4 -4
View File
@@ -1,10 +1,10 @@
API_PORT=3671 API_PORT=3671
MONGO_INITDB_ROOT_PASSWORD=pass MONGO_INITDB_ROOT_PASSWORD=change-this-mongo-password
MONGODB_URI=mongodb://theplug:pass@mongodb:27017/theplug?authSource=admin MONGODB_URI=mongodb://theplug:change-this-mongo-password@localhost:27017/theplug?authSource=admin
MONGODB_DB_NAME=theplug MONGODB_DB_NAME=theplug
REDIS_URL=redis://redis:6379 REDIS_URL=redis://localhost:6379
JWT_SECRET_KEY=change-this-secret JWT_SECRET_KEY=change-this-secret
JWT_ALGORITHM=HS256 JWT_ALGORITHM=HS256
JWT_EXPIRE_MINUTES=30 JWT_EXPIRE_MINUTES=30
AUTH_USERNAME=admin AUTH_USERNAME=admin
AUTH_PASSWORD=change-this-password AUTH_PASSWORD=change-this-password
-32
View File
@@ -1,32 +0,0 @@
name: Build and Push to Registry
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.reloia.uk
username: reloia
password: ${{ secrets.LOGIN_TOKEN }}
- name: Build and Push (Shell)
run: |
IMAGE_NAME="git.reloia.uk/reloia/theplug:latest"
echo "Building $IMAGE_NAME..."
docker build -t "$IMAGE_NAME" .
echo "Pushing $IMAGE_NAME..."
docker push "$IMAGE_NAME"
+26
View File
@@ -0,0 +1,26 @@
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app/app
WORKDIR /app
COPY pyproject.toml ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir \
"authlib>=1.6.0" \
"fastapi[standard]>=0.135.3" \
"motor>=3.7.1" \
"pwdlib[argon2,bcrypt]>=0.3.0" \
"pydantic>=2.12.5" \
"pydantic-settings>=2.13.1" \
"pyyaml>=6.0.3" \
"redis>=7.4.0"
COPY app ./app
EXPOSE 3671
CMD ["sh", "-c", "fastapi run /app/app/main.py --host 0.0.0.0 --port ${API_PORT:-3671}"]
+7 -1
View File
@@ -8,7 +8,13 @@ icona di una presa della spina idk
FastAPI + MongoDB + Redis FastAPI + MongoDB + Redis
``` ```
### Setup Dev ## Quick Setup (prod)
```bash
bash <(curl -s https://git.reloia.uk/reloia/thePlug/raw/branch/master/setup.sh)
```
## Setup Dev
imposta le variabili d'ambiente imposta le variabili d'ambiente
+2 -2
View File
@@ -11,7 +11,7 @@ from repositories.proxy import (
from repositories.proxy import ( from repositories.proxy import (
list_proxies as list_proxy_documents, list_proxies as list_proxy_documents,
) )
from services.proxy.validator import ProxyCodeValidationError, validate_proxy_code from services.mitmproxy.validator import ProxyCodeValidationError, validate_proxy_code
router = APIRouter( router = APIRouter(
prefix="/proxy", prefix="/proxy",
@@ -60,7 +60,7 @@ async def create_proxy_handler(payload: ProxyCreateInput):
return await create_proxy( return await create_proxy(
proxy_port=payload.proxy_port, proxy_port=payload.proxy_port,
name=payload.name, name=payload.name,
service={"address": payload.service.address, "port": payload.service.port}, service={"port": payload.service.port},
code=payload.code, code=payload.code,
) )
+14
View File
@@ -0,0 +1,14 @@
from fastapi import APIRouter, Depends, Request
from core.security import require_authenticated_user
router = APIRouter(
prefix="/services",
tags=["Services"],
dependencies=[Depends(require_authenticated_user)],
)
@router.get("")
async def get_services(request: Request):
return request.app.state.services
+8 -3
View File
@@ -7,13 +7,14 @@ from authlib.jose import jwt # type: ignore
from authlib.jose.errors import JoseError # type: ignore from authlib.jose.errors import JoseError # type: ignore
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from passlib.context import CryptContext from pwdlib import PasswordHash
from pwdlib.hashers.argon2 import Argon2Hasher
from core.config import settings from core.config import settings
from models.auth import AccessTokenPayload, AuthUserDocument from models.auth import AccessTokenPayload, AuthUserDocument
from repositories.auth import get_user_by_username, is_token_revoked from repositories.auth import get_user_by_username, is_token_revoked
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = PasswordHash((Argon2Hasher(),))
bearer_scheme = HTTPBearer(auto_error=False) bearer_scheme = HTTPBearer(auto_error=False)
@@ -41,7 +42,7 @@ def hash_password(password: str) -> str:
def verify_password(plain_password: str, password_hash: str) -> bool: def verify_password(plain_password: str, password_hash: str) -> bool:
return pwd_context.verify(plain_password, password_hash) return pwd_context.check(plain_password, password_hash)
def create_access_token(*, subject: str) -> str: def create_access_token(*, subject: str) -> str:
@@ -53,14 +54,18 @@ def create_access_token(*, subject: str) -> str:
"alg": settings.JWT_ALGORITHM, "alg": settings.JWT_ALGORITHM,
"typ": "JWT", "typ": "JWT",
} }
payload = { payload = {
"sub": subject, "sub": subject,
"jti": str(uuid4()), "jti": str(uuid4()),
"exp": expires_at, "exp": expires_at,
} }
token = jwt.encode(header, payload, settings.JWT_SECRET_KEY) token = jwt.encode(header, payload, settings.JWT_SECRET_KEY)
if isinstance(token, bytes): if isinstance(token, bytes):
return token.decode("utf-8") return token.decode("utf-8")
return token return token
+2 -4
View File
@@ -2,15 +2,13 @@ from integrations.docker.compose import (
find_docker_compose, find_docker_compose,
get_all_services, get_all_services,
get_services, get_services,
get_visible_folders,
) )
from integrations.docker.editor import change_port from integrations.docker.editor import change_port
from integrations.docker.models import DockerService from integrations.docker.models import DockerContainer
__all__ = [ __all__ = [
"DockerService", "DockerContainer",
"find_docker_compose", "find_docker_compose",
"get_visible_folders",
"get_services", "get_services",
"get_all_services", "get_all_services",
"change_port", "change_port",
+11 -11
View File
@@ -2,7 +2,7 @@ from pathlib import Path
import yaml import yaml
from integrations.docker.models import DockerService from integrations.docker.models import DockerContainer
from integrations.docker.ports import _get_service_port from integrations.docker.ports import _get_service_port
COMPOSE_NAMES = ( COMPOSE_NAMES = (
@@ -13,7 +13,7 @@ COMPOSE_NAMES = (
) )
def get_visible_folders(directory: Path) -> list[Path]: def _get_visible_folders(directory: Path) -> list[Path]:
return [ return [
p for p in Path(directory).iterdir() if p.is_dir() and not p.name.startswith(".") p for p in Path(directory).iterdir() if p.is_dir() and not p.name.startswith(".")
] ]
@@ -32,8 +32,8 @@ def find_docker_compose(path: Path) -> tuple[dict, str] | None:
return None return None
def get_services(folder: Path) -> list[DockerService] | None: def get_services(folder: Path) -> list[DockerContainer] | None:
services: list[DockerService] = [] services: list[DockerContainer] = []
compose_data = find_docker_compose(folder) compose_data = find_docker_compose(folder)
if compose_data is None: if compose_data is None:
return None return None
@@ -56,20 +56,20 @@ def get_services(folder: Path) -> list[DockerService] | None:
if not ports: if not ports:
continue continue
services.append({"name": service_name, "ports": ports}) services.append({"name": service_name, "ports": ports, "folder": folder})
return services or None return services
def get_all_services(directory: str | Path) -> list[DockerService] | None: def get_all_services(directory: str | Path) -> list[DockerContainer]:
path = Path(directory) path = Path(directory)
if not path.is_dir(): if not path.is_dir():
return None return []
services: list[DockerService] = [] services: list[DockerContainer] = []
for sub_folder in get_visible_folders(path): for sub_folder in _get_visible_folders(path):
sub_services = get_services(sub_folder) sub_services = get_services(sub_folder)
if sub_services: if sub_services:
services.extend(sub_services) services.extend(sub_services)
return services or None return services
+3 -1
View File
@@ -1,6 +1,8 @@
from pathlib import Path
from typing import TypedDict from typing import TypedDict
class DockerService(TypedDict): class DockerContainer(TypedDict):
name: str name: str
ports: list[int] ports: list[int]
folder: Path
+24
View File
@@ -1,13 +1,18 @@
import os
import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from api.auth.router import router as auth_router from api.auth.router import router as auth_router
from api.proxy.router import router as proxy_router from api.proxy.router import router as proxy_router
from api.services.router import router as services_router
from core.config import settings from core.config import settings
from core.mongodb import close_mongodb, init_mongodb from core.mongodb import close_mongodb, init_mongodb
from core.redis import close_redis, init_redis from core.redis import close_redis, init_redis
from core.security import hash_password from core.security import hash_password
from integrations.docker import get_all_services
from repositories.auth import ensure_auth_indexes, ensure_single_account from repositories.auth import ensure_auth_indexes, ensure_single_account
from repositories.proxy import ensure_proxy_indexes from repositories.proxy import ensure_proxy_indexes
@@ -22,19 +27,38 @@ async def lifespan(_: FastAPI):
password_hash=hash_password(settings.AUTH_PASSWORD), password_hash=hash_password(settings.AUTH_PASSWORD),
) )
await ensure_proxy_indexes() await ensure_proxy_indexes()
home_dir = str(Path.home())
app.state.services = get_all_services(home_dir) or []
if not app.state.services:
warning = (
"\n" + "!" * 60 + "\n"
" WARNING: No services found in home directory!\n"
f" Scanned: {home_dir}\n"
+ "!" * 60 + "\n"
)
print(warning, file=sys.stderr)
try: try:
yield yield
finally: finally:
await close_redis() await close_redis()
await close_mongodb() await close_mongodb()
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
app = FastAPI( app = FastAPI(
title="thePlugAPI", title="thePlugAPI",
lifespan=lifespan, lifespan=lifespan,
docs_url="/docs" if DEBUG else None,
redoc_url="/redoc" if DEBUG else None,
openapi_url="/openapi.json" if DEBUG else None,
) )
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(proxy_router) app.include_router(proxy_router)
app.include_router(services_router)
@app.get("/") @app.get("/")
-1
View File
@@ -4,5 +4,4 @@ from pydantic import BaseModel, ConfigDict, Field
class ServiceEndpointDocument(BaseModel): class ServiceEndpointDocument(BaseModel):
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
address: str = Field(..., description="Upstream service address")
port: int = Field(..., description="Upstream service port") port: int = Field(..., description="Upstream service port")
+1 -2
View File
@@ -4,11 +4,11 @@ from typing import Any
from motor.motor_asyncio import AsyncIOMotorCollection from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument from pymongo import ReturnDocument
from services.proxy.models import ServiceConfig
from core.mongodb import get_database from core.mongodb import get_database
from models.proxy import ProxyDocument, ProxyUpdateInput from models.proxy import ProxyDocument, ProxyUpdateInput
from models.service import ServiceEndpointDocument from models.service import ServiceEndpointDocument
from services.mitmproxy.models import ServiceConfig
COLLECTION_NAME = "proxies" COLLECTION_NAME = "proxies"
@@ -47,7 +47,6 @@ async def create_proxy(
name=name, name=name,
code=code, code=code,
service=ServiceEndpointDocument( service=ServiceEndpointDocument(
address=service["address"],
port=service["port"], port=service["port"],
), ),
created_at=now, created_at=now,
View File
+27
View File
@@ -0,0 +1,27 @@
import uuid
from pathlib import Path
from integrations.docker import DockerContainer
from services.mitmproxy.models import ServiceConfig
async def create_proxy(
port: int,
name: str,
service: ServiceConfig,
code: str,
services: list[DockerContainer]
) -> int:
"""
Utility function that will:
- run ``integrations/docker`` utilities to change the old port of original service to a random one
- write ``code`` into ``/tmp/theplug/{random}.py``
- start a subprocess via asyncio
- return the proxy infos (pid, )
"""
path = Path("/tmp/theplug/{random}.py".format(random=str(uuid.uuid4())))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(code, encoding="utf-8")
return 0
@@ -2,5 +2,4 @@ from typing import TypedDict
class ServiceConfig(TypedDict): class ServiceConfig(TypedDict):
name: str
port: int port: int
-10
View File
@@ -1,10 +0,0 @@
from services.proxy.models import ServiceConfig
async def create_proxy(
port: int,
name: str,
service: ServiceConfig,
code: str
):
return
+80
View File
@@ -0,0 +1,80 @@
services:
app:
build:
context: .
dockerfile: Dockerfile.test
container_name: theplug-app-test
restart: unless-stopped
env_file:
- .env
environment:
MONGODB_URI: mongodb://theplug:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017/${MONGO_DB:-theplug}?authSource=admin
MONGODB_DB_NAME: ${MONGO_DB:-theplug}
REDIS_URL: redis://redis:6379
API_PORT: ${API_PORT:-3671}
DEBUG: "true"
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${API_PORT:-3671}:${API_PORT:-3671}"
volumes:
- ./example-http-one:/root/example-http-one:ro
- ./example-http-two:/root/example-http-two:ro
mongodb:
image: mongo:8.2
container_name: theplug-mongodb-test
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-theplug}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
MONGO_INITDB_DATABASE: ${MONGO_DB:-theplug}
ports:
- "27017:27017"
volumes:
- mongodb_test_data:/data/db
healthcheck:
test: ["CMD-SHELL", "mongosh --quiet --username ${MONGO_USER:-theplug} --password ${MONGO_INITDB_ROOT_PASSWORD} --authenticationDatabase admin --eval \"db.adminCommand({ ping: 1 }).ok\" | grep 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:8-alpine
container_name: theplug-redis-test
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
ports:
- "6379:6379"
volumes:
- redis_test_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
example-http-one:
build:
context: ./example-http-one
container_name: theplug-example-http-one
restart: unless-stopped
ports:
- "18081:8080"
example-http-two:
build:
context: ./example-http-two
container_name: theplug-example-http-two
restart: unless-stopped
ports:
- "18082:8080"
volumes:
mongodb_test_data:
redis_test_data:
+3 -32
View File
@@ -1,47 +1,18 @@
services: services:
api:
build:
context: .
dockerfile: Dockerfile
container_name: theplug-api
restart: unless-stopped
ports:
- "${API_PORT:-3671}:3671"
environment:
MONGODB_URI: mongodb://${MONGO_USER:-theplug}:${MONGO_PASSWORD:-theplug_dev_password}@mongodb:27017/${MONGO_DB:-theplug}?authSource=admin
MONGODB_DB_NAME: ${MONGO_DB:-theplug}
REDIS_URL: redis://redis:6379/0
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-this-secret}
JWT_ALGORITHM: ${JWT_ALGORITHM:-HS256}
JWT_EXPIRE_MINUTES: ${JWT_EXPIRE_MINUTES:-30}
AUTH_USERNAME: ${AUTH_USERNAME:-admin}
AUTH_PASSWORD: ${AUTH_PASSWORD:-change-this-password}
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:3671/').read()\""]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
mongodb: mongodb:
image: mongo:8 image: mongo:8.2
container_name: theplug-mongodb container_name: theplug-mongodb
restart: unless-stopped restart: unless-stopped
environment: environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-theplug} MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-theplug}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-pass} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
MONGO_INITDB_DATABASE: ${MONGO_DB:-theplug} MONGO_INITDB_DATABASE: ${MONGO_DB:-theplug}
ports: ports:
- "27017:27017" - "27017:27017"
volumes: volumes:
- mongodb_data:/data/db - mongodb_data:/data/db
healthcheck: healthcheck:
test: ["CMD-SHELL", "mongosh --quiet --username ${MONGO_USER:-theplug} --password ${MONGO_PASSWORD:-theplug_dev_password} --authenticationDatabase admin --eval \"db.adminCommand({ ping: 1 }).ok\" | grep 1"] test: ["CMD-SHELL", "mongosh --quiet --username ${MONGO_USER:-theplug} --password ${MONGO_INITDB_ROOT_PASSWORD} --authenticationDatabase admin --eval \"db.adminCommand({ ping: 1 }).ok\" | grep 1"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.14-slim
WORKDIR /srv/www
COPY index.html ./
EXPOSE 8080
CMD ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0", "--directory", "/srv/www"]
+6
View File
@@ -0,0 +1,6 @@
services:
web:
build: .
container_name: example-http-one
ports:
- "18081:8080"
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example HTTP One</title>
</head>
<body>
<h1>Example HTTP One</h1>
<p>This container serves a static page with Python's http.server.</p>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.14-slim
WORKDIR /srv/www
COPY index.html ./
EXPOSE 8080
CMD ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0", "--directory", "/srv/www"]
+6
View File
@@ -0,0 +1,6 @@
services:
web:
build: .
container_name: example-http-two
ports:
- "18082:8080"
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example HTTP Two</title>
</head>
<body>
<h1>Example HTTP Two</h1>
<p>This container serves a static page with Python's http.server.</p>
</body>
</html>
+2 -1
View File
@@ -7,7 +7,8 @@ dependencies = [
"authlib>=1.6.0", "authlib>=1.6.0",
"fastapi[standard]>=0.135.3", "fastapi[standard]>=0.135.3",
"motor>=3.7.1", "motor>=3.7.1",
"passlib[bcrypt]>=1.7.4", "pwdlib[argon2,bcrypt]>=0.3.0",
"pydantic>=2.12.5",
"pydantic-settings>=2.13.1", "pydantic-settings>=2.13.1",
"pyyaml>=6.0.3", "pyyaml>=6.0.3",
"redis>=7.4.0", "redis>=7.4.0",
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
die() { echo -e "${RED}[✗] FATAL:${NC} $*" >&2; exit 1; }
install_service() {
local uv_bin
uv_bin=$(command -v uv)
local api_port
api_port=$(grep -E '^API_PORT=' .env 2>/dev/null | cut -d '=' -f2)
api_port=${api_port:-3671}
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/theplug.service <<EOF
[Unit]
Description=thePlug service
After=network.target
[Service]
WorkingDirectory=/srv/theplug
EnvironmentFile=/srv/theplug/.env
Environment=PYTHONPATH=app
ExecStartPre=/usr/bin/docker compose -f /srv/theplug/compose.yml up -d redis mongodb
ExecStart=$uv_bin run uvicorn app.main:app --host 0.0.0.0 --port $api_port
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable theplug
info "systemd user service installed (port $api_port)"
}
start_services() {
info "Starting Redis and MongoDB..."
docker compose up -d redis mongodb || die "docker compose failed — are Redis/MongoDB defined in compose.yml?"
info "Starting thePlug via systemctl..."
systemctl --user start theplug
warn "Use 'systemctl --user status theplug' to check / 'journalctl --user -u theplug -f' to follow logs"
}
# ── 1. ensure we're in thePlug ────────────────────────────────────────────────
if [[ -d "/srv/theplug" ]]; then
info "thePlug già presente — avvio diretto..."
cd /srv/theplug
start_services
exit 0
fi
cd /srv
git clone https://git.reloia.uk/reloia/thePlug theplug || die "git clone failed"
cd theplug
chmod +x ./setup.sh
PLUG_DIR="$(pwd)"
if [[ ! -L /usr/bin/theplug ]]; then
info "Creating symlink /usr/bin/theplug..."
sudo ln -sf "$PLUG_DIR/setup.sh" /usr/bin/theplug
sudo chmod +x /usr/bin/theplug
fi
# 2 env
if [[ ! -f .env ]]; then
warn ".env not found — copyinh from .env.example with random stuff..."
cp .env.example .env || die ".env.example not found (impossibile)"
MONGO_PASS=$(openssl rand -hex 8)
JWT_SECRET=$(openssl rand -hex 20)
AUTH_PASS=$(openssl rand -hex 10)
sed -i "s|^MONGO_INITDB_ROOT_PASSWORD=.*|MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASS|" .env
sed -i "s|^MONGODB_URI=.*|MONGODB_URI=mongodb://theplug:$MONGO_PASS@localhost:27017/theplug?authSource=admin|" .env
sed -i "s|^JWT_SECRET_KEY=.*|JWT_SECRET_KEY=$JWT_SECRET|" .env
sed -i "s|^AUTH_PASSWORD=.*|AUTH_PASSWORD=$AUTH_PASS|" .env
info "Resetting docker containers..."
docker compose down -v 2>/dev/null || true
fi
# ── 3. spin up dependencies ───────────────────────────────────────────────────
info "Starting Redis and MongoDB..."
docker compose up -d redis mongodb || die "docker compose failed — are Redis/MongoDB defined in compose.yml?"
# ── 4. ensure uv is available ─────────────────────────────────────────────────
if ! command -v uv &>/dev/null; then
warn "uv not found — installing..."
curl -LsSf https://astral.sh/uv/install.sh | sh || die "uv installation failed"
export PATH="$HOME/.local/bin:$PATH"
command -v uv &>/dev/null || die "uv installed but still not in PATH — open a new shell and retry"
info "uv installed: $(uv --version)"
fi
# ── 5. venv + deps ────────────────────────────────────────────────────────────
info "Pinning Python 3.14..."
uv python pin 3.14
info "Creating venv..."
uv venv
info "Syncing dependencies..."
uv sync
# ── 6. install service + start ────────────────────────────────────────────────
install_service
systemctl --user start theplug
echo -e ""
info "Automatically generated Secrets:"
info " MONGO_PASS: $MONGO_PASS"
info " JWT_SECRET: $JWT_SECRET"
info " AUTH_PASS: $AUTH_PASS - questa è importante"
warn "Save them — these won't be showed again (non è vero basta che leggi $PLUG_DIR/.env)"
echo -e ""
info "Installed thePlug in $PLUG_DIR"
warn "Use 'systemctl --user status theplug' to check / 'journalctl --user -u theplug -f' to follow logs"
echo -e ""
+6 -8
View File
@@ -3,7 +3,7 @@ import importlib
from core.security import get_auth_context, require_authenticated_user from core.security import get_auth_context, require_authenticated_user
from models.proxy import ProxyDocument from models.proxy import ProxyDocument
from models.service import ServiceEndpointDocument from models.service import ServiceEndpointDocument
from services.proxy.validator import ProxyCodeValidationError from services.mitmproxy.validator import ProxyCodeValidationError
proxy_router_module = importlib.import_module("api.proxy.router") proxy_router_module = importlib.import_module("api.proxy.router")
@@ -13,14 +13,13 @@ def make_proxy_document(
proxy_port: int, proxy_port: int,
name: str = "svc", name: str = "svc",
code: str = "proxy-code", code: str = "proxy-code",
address: str = "127.0.0.1",
port: int = 9000, port: int = 9000,
) -> ProxyDocument: ) -> ProxyDocument:
return ProxyDocument( return ProxyDocument(
proxy_port=proxy_port, proxy_port=proxy_port,
name=name, name=name,
code=code, code=code,
service=ServiceEndpointDocument(address=address, port=port), service=ServiceEndpointDocument(port=port),
) )
@@ -87,7 +86,7 @@ def test_create_proxy_returns_409_on_port_conflict(client, monkeypatch: object):
"proxy_port": 3671, "proxy_port": 3671,
"name": "new", "name": "new",
"code": "abc", "code": "abc",
"service": {"address": "localhost", "port": 8080}, "service": {"port": 8080},
}, },
) )
@@ -117,7 +116,6 @@ def test_create_proxy_returns_created_document(client, monkeypatch: object):
proxy_port=proxy_port, proxy_port=proxy_port,
name=name, name=name,
code=code, code=code,
address=str(service["address"]),
port=int(service["port"]), port=int(service["port"]),
) )
@@ -142,7 +140,7 @@ def test_create_proxy_returns_created_document(client, monkeypatch: object):
"proxy_port": 4040, "proxy_port": 4040,
"name": "edge", "name": "edge",
"code": "def proxy_handler():\n return 'ok'\n", "code": "def proxy_handler():\n return 'ok'\n",
"service": {"address": "service.local", "port": 8181}, "service": {"port": 8181},
}, },
) )
@@ -151,7 +149,7 @@ def test_create_proxy_returns_created_document(client, monkeypatch: object):
assert captured == { assert captured == {
"proxy_port": 4040, "proxy_port": 4040,
"name": "edge", "name": "edge",
"service": {"address": "service.local", "port": 8181}, "service": {"port": 8181},
"code": "def proxy_handler():\n return 'ok'\n", "code": "def proxy_handler():\n return 'ok'\n",
} }
@@ -208,7 +206,7 @@ def test_create_proxy_returns_422_when_code_validation_fails(
"proxy_port": 4000, "proxy_port": 4000,
"name": "broken", "name": "broken",
"code": "invalid-code", "code": "invalid-code",
"service": {"address": "service.local", "port": 9000}, "service": {"port": 9000},
}, },
) )
Generated
+64 -16
View File
@@ -32,6 +32,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
] ]
[[package]]
name = "argon2-cffi"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "argon2-cffi-bindings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" },
]
[[package]]
name = "argon2-cffi-bindings"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" },
{ url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" },
{ url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" },
{ url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" },
{ url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" },
{ url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" },
{ url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" },
{ url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" },
{ url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" },
{ url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" },
{ url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" },
{ url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" },
{ url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" },
{ url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" },
{ url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" },
{ url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" },
{ url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" },
]
[[package]] [[package]]
name = "authlib" name = "authlib"
version = "1.6.9" version = "1.6.9"
@@ -494,20 +537,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
] ]
[[package]]
name = "passlib"
version = "1.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
]
[package.optional-dependencies]
bcrypt = [
{ name = "bcrypt" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -517,6 +546,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "pwdlib"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" },
]
[package.optional-dependencies]
argon2 = [
{ name = "argon2-cffi" },
]
bcrypt = [
{ name = "bcrypt" },
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "3.0" version = "3.0"
@@ -853,7 +899,8 @@ dependencies = [
{ name = "authlib" }, { name = "authlib" },
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "motor" }, { name = "motor" },
{ name = "passlib", extra = ["bcrypt"] }, { name = "pwdlib", extra = ["argon2", "bcrypt"] },
{ name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "redis" }, { name = "redis" },
@@ -870,7 +917,8 @@ requires-dist = [
{ name = "authlib", specifier = ">=1.6.0" }, { name = "authlib", specifier = ">=1.6.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" },
{ name = "motor", specifier = ">=3.7.1" }, { name = "motor", specifier = ">=3.7.1" },
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, { name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-settings", specifier = ">=2.13.1" }, { name = "pydantic-settings", specifier = ">=2.13.1" },
{ name = "pyyaml", specifier = ">=6.0.3" }, { name = "pyyaml", specifier = ">=6.0.3" },
{ name = "redis", specifier = ">=7.4.0" }, { name = "redis", specifier = ">=7.4.0" },