Files

590 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dokumentation Pastebin-Service
**Thema 1 Custom Website**
**Webarchitektur Projekt**
---
## Inhaltsverzeichnis
1. [Beschreibung der Umsetzung](#1-beschreibung-der-umsetzung)
2. [Architekturdiagramm](#2-architekturdiagramm)
3. [Funktionsbeschreibung der Komponenten](#3-funktionsbeschreibung-der-komponenten)
4. [Installationsanleitung](#4-installationsanleitung)
5. [Konfigurationsanpassungen](#5-konfigurationsanpassungen)
6. [Post-Mortem-Log](#6-post-mortem-log)
7. [Screenshots](#7-screenshots)
---
## 1. Beschreibung der Umsetzung
Das Projekt ist ein einfacher Pastebin-Service, der es Nutzern ermöglicht, Text online zu speichern und über einen Link zu teilen. Die Anwendung besteht aus vier Docker-Containern, die über Docker Compose orchestriert werden.
### Kernfunktionalität
- **Paste erstellen**: Jeder Nutzer kann ohne Authentifizierung einen Paste erstellen (max. 100.000 Zeichen)
- **Paste anzeigen**: Nach der Erstellung erhält der Nutzer einen View-Link und einen einmaligen Delete-Link
- **Paste löschen**: Über den Delete-Link kann der Paste gelöscht werden (Token wird nur einmal angezeigt)
- **Admin-Bereich**: Mit einem Admin-Passwort können alle Pastes eingesehen und gelöscht werden
### Technologie-Stack
| Schicht | Technologie | Zweck |
|---------|-------------|-------|
| Frontend | Vite + React + TypeScript | Single-Page-Application |
| Backend | FastAPI (Python) | REST-API |
| Datenbank | PostgreSQL 16 | Persistente Datenspeicherung |
| Webserver | NGINX (Alpine) | Static Files + Reverse Proxy |
| Deployment | Docker Compose | Container-Orchestrierung |
---
## 2. Architekturdiagramm
```
┌─────────────────────────────────────────────────────────────────┐
│ Internet │
│ Port 80 (HTTP) / 443 (HTTPS) │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ NGINX Reverse Proxy │
│ (Container: proxy) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Port 80: 301 Redirect → HTTPS │ │
│ │ Port 443: TLS-Terminierung (cert.pem / key.pem) │ │
│ │ Security-Header: HSTS, X-Frame-Options, CSP, etc. │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ /api/* ──────────► http://backend:8000 │
│ /* ──────────► http://frontend:80 │
└────────────────────────────┬────────────────────────────────────┘
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ PostgreSQL │
│ (NGINX) │ │ (FastAPI) │ │ (Datenbank) │
│ Port 80 │ │ Port 8000 │ │ Port 5432 │
│ │ │ │ │ │
│ React SPA │ │ REST-API │ │ Pastes-Tabelle │
│ Static Files │ │ Uvicorn │ │ UUID PK/FK │
└─────────────────┘ └──────┬───────┘ └─────────────────┘
┌──────────────┐
│ PostgreSQL │
│ (Volume) │
│ pgdata │
└──────────────┘
```
### Netzwerk-Isolation
- Nur der **proxy** Container bindet auf öffentliche Ports (80, 443)
- **backend**, **frontend** und **db** sind nur über das interne Docker-Netzwerk erreichbar
- Keine externen Port-Mappings für Backend und Datenbank
---
## 3. Funktionsbeschreibung der Komponenten
### 3.1 NGINX Reverse Proxy (`nginx/nginx.conf`)
**Aufgabe**: Terminiert die TLS-Verbindung und leitet Requests an die internen Services weiter.
**TLS-Terminierung** (`nginx/nginx.conf:13-20`):
```nginx
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
```
Das Zertifikat wird manuell im `ssl/` Verzeichnis bereitgestellt. Es unterstützt TLS 1.2 und 1.3.
**HTTP → HTTPS Redirect** (`nginx/nginx.conf:1-4`):
```nginx
server {
listen 80;
server_name static.155.116.167.89.clients.your-server.de;
return 301 https://$host$request_uri;
}
```
Alle HTTP-Anfragen werden permanent auf HTTPS umgeleitet.
**Reverse Proxy Routing** (`nginx/nginx.conf:30-46`):
```nginx
location /api/ {
proxy_pass http://backend:8000;
...
}
location / {
proxy_pass http://frontend:80;
...
}
```
Requests an `/api/*` werden an den FastAPI-Backend-Container weitergeleitet, alle anderen an den Frontend-NGINX.
**Security-Hardening** (`nginx/nginx.conf:24-28`):
```nginx
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
```
| Header | Funktion |
|--------|----------|
| `Strict-Transport-Security` (HSTS) | Erzwingt HTTPS für 2 Jahre, inkl. Subdomains |
| `X-Frame-Options: DENY` | Verhindert Einbettung in iframes (Clickjacking-Schutz) |
| `X-Content-Type-Options: nosniff` | Verhindert MIME-Type-Sniffing |
| `Referrer-Policy` | Kontrolliert Referrer-Weitergabe |
| `Permissions-Policy` | Deaktiviert Kamera, Mikrofon, Geolocation |
**Server-Header entfernen** (`nginx/nginx.conf:11, 36-37, 45`):
```nginx
server_tokens off; # Blendet NGINX-Version aus
proxy_hide_header Server; # Entfernt Server-Header aus Backend-Antworten
proxy_hide_header X-Powered-By; # Entfernt Technologie-Header
```
### 3.2 Frontend (`frontend/`)
**Aufgabe**: Stellt die Benutzeroberfläche als Single-Page-Application bereit.
**Build-Prozess** (`frontend/Dockerfile:1-9`):
```dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
```
Das Frontend wird mit Vite gebaut. Die entstehenden Static Files werden in den NGINX-Container kopiert.
**Static File Serving** (`frontend/nginx.conf:8-10`):
```nginx
location / {
try_files $uri $uri/ /index.html;
}
```
Die `try_files`-Direktive ermöglicht Client-Side-Routing: Alle Pfade werden auf `index.html` umgeleitet, sodass React Router funktioniert.
**SPA-Routing** (`frontend/src/main.tsx`):
```tsx
<Routes>
<Route path="/" element={<HomePage />} /> {/* Paste erstellen */}
<Route path="paste/:id" element={<PastePage />} /> {/* Paste anzeigen */}
<Route path="admin" element={<AdminPage />} /> {/* Admin-Bereich */}
</Routes>
```
**API-Client** (`frontend/src/api/client.ts`):
Der API-Client kapselt alle HTTP-Requests an das Backend in typisierte Funktionen. Er verwendet die nativ `fetch`-API und gibt die Antworten als TypeScript-Interfaces zurück:
```typescript
const API_BASE = "/api"; // Relativer Pfad → wird vom NGINX Proxy weitergeleitet
```
| Funktion | HTTP-Methode | Endpoint | Beschreibung |
|----------|-------------|----------|--------------|
| `createPaste(content)` | POST | `/api/pastes` | Erstellt einen neuen Paste |
| `getPaste(id)` | GET | `/api/pastes/{id}` | Ruft einen Paste ab |
| `deletePaste(id, token)` | DELETE | `/api/pastes/{id}?token=...` | Löscht einen Paste (mit Token) |
| `listPastes(adminPassword)` | GET | `/api/admin/pastes` | Listet alle Pastes (Admin) |
| `adminDeletePaste(id, pw)` | DELETE | `/api/admin/pastes/{id}` | Löscht einen Paste (Admin) |
Die Funktionen werden von den Page-Komponenten importiert und genutzt. Beispiel aus `HomePage.tsx`:
```typescript
const result = await createPaste(content);
navigate(`/paste/${result.id}?token=${result.delete_token}`);
```
Die Admin-Funktionen senden das Passwort über den `X-Admin-Password` Header:
```typescript
const res = await fetch(`${API_BASE}/admin/pastes`, {
headers: { "X-Admin-Password": adminPassword },
});
```
### 3.3 Backend (`backend/`)
**Aufgabe**: Stellt die REST-API bereit und kommuniziert mit der Datenbank.
**API-Endpunkte** (`backend/app/main.py`):
| Methode | Pfad | Beschreibung | Zeile |
|---------|------|--------------|-------|
| `POST` | `/api/pastes` | Neuen Paste erstellen | 34 |
| `GET` | `/api/pastes/{id}` | Paste abrufen | 45 |
| `DELETE` | `/api/pastes/{id}?token=...` | Paste löschen (mit Token) | 53 |
| `GET` | `/api/admin/pastes` | Alle Pastes auflisten (Admin) | 69 |
| `DELETE` | `/api/admin/pastes/{id}` | Paste als Admin löschen | 78 |
**Admin-Authentifizierung** (`backend/app/main.py:29-31`):
```python
def verify_admin(password: str = Header(..., alias="X-Admin-Password")):
if password != settings.admin_password:
raise HTTPException(status_code=401, detail="Invalid admin password")
```
Das Admin-Passwort wird über den `X-Admin-Password` Header übermittelt und gegen die Umgebungsvariable geprüft.
**Datenbankmodell** (`backend/app/models.py:11-21`):
```python
class Paste(Base):
__tablename__ = "pastes"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
content: Mapped[str] = mapped_column(Text, nullable=False)
delete_token: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False, default=uuid.uuid4)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
```
- `id`: UUID als Primärschlüssel (verhindert Enumeration)
- `delete_token`: Separater UUID für das Löschen (wird nur bei Erstellung ausgegeben)
- `content`: Text mit max. 100.000 Zeichen
### 3.4 PostgreSQL Datenbank
**Aufgabe**: Persistente Speicherung der Pastes.
**Konfiguration** (`docker-compose.yml:2-10`):
```yaml
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
```
Die Datenbank-Credentials werden über Umgebungsvariablen aus `.env` geladen. Die Daten werden in einem Docker-Volume (`pgdata`) persistent gespeichert.
**Port-Binding**: Der PostgreSQL-Container hat kein `ports:`-Mapping und ist daher nur aus dem internen Docker-Netzwerk erreichbar.
---
## 4. Installationsanleitung
### 4.1 Voraussetzungen
- Docker und Docker Compose installiert
- Domain/Server-IP verfügbar: `static.155.116.167.89.clients.your-server.de`
- SSL-Zertifikat vorhanden
### 4.2 Projekt klonieren
```bash
git clone <repository-url>
cd Web-Architekturen-Projekt
```
### 4.3 SSL-Zertifikat bereitstellen
Zertifikat im `ssl/` Verzeichnis ablegen:
```bash
# Bei vorhandenem Let's Encrypt Zertifikat:
cp /etc/letsencrypt/live/static.155.116.167.89.clients.your-server.de/fullchain.pem ssl/cert.pem
cp /etc/letsencrypt/live/static.155.116.167.89.clients.your-server.de/privkey.pem ssl/key.pem
# Oder Self-Signed für Tests:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ssl/key.pem -out ssl/cert.pem \
-subj "/CN=static.155.116.167.89.clients.your-server.de"
```
### 4.4 Umgebungsvariablen konfigurieren
`.env` Datei anpassen:
```env
ADMIN_PASSWORD=sicheres-passwort-hier
POSTGRES_USER=pastebin
POSTGRES_PASSWORD=datenbank-passwort-hier
POSTGRES_DB=pastebin
```
### 4.5 Container starten
```bash
docker compose up --build -d
```
### 4.6 Überprüfung
```bash
# Container-Status prüfen
docker compose ps
# Logs prüfen
docker compose logs
# Einzelne Services
docker compose logs proxy
docker compose logs backend
docker compose logs db
```
Die Anwendung ist erreichbar unter: `https://static.155.116.167.89.clients.your-server.de`
### 4.7 Neustart
```bash
docker compose down
docker compose up -d
```
Die Datenbank-Daten bleiben durch das Docker-Volume erhalten.
---
## 5. Konfigurationsanpassungen
### 5.1 NGINX Reverse Proxy (`nginx/nginx.conf`)
Alle Konfigurationen weichen von der Standardkonfiguration ab:
| Zeile | Konfiguration | Beschreibung |
|-------|---------------|--------------|
| 1-4 | `server { listen 80; return 301 ... }` | HTTP → HTTPS Redirect |
| 11 | `server_tokens off;` | NGINX-Version ausblenden |
| 13-14 | `ssl_certificate / ssl_certificate_key` | Manuelle TLS-Zertifikate |
| 16-20 | `ssl_protocols / ssl_ciphers` | TLS 1.2/1.3, sichere Ciphers |
| 24-28 | `add_header ...` | 5 Security-Header |
| 31 | `proxy_pass http://backend:8000;` | API-Requests an Backend |
| 33-35 | `proxy_set_header ...` | Client-IP und Proto weiterleiten |
| 36-37 | `proxy_hide_header ...` | Server-Header entfernen |
| 41 | `proxy_pass http://frontend:80;` | Frontend-Requests an NGINX |
### 5.2 Frontend-NGINX (`frontend/nginx.conf`)
| Zeile | Konfiguration | Beschreibung |
|-------|---------------|--------------|
| 9 | `try_files $uri $uri/ /index.html;` | SPA-Routing für React Router |
| 12-14 | `location /assets/ { expires 1y; }` | Cache-Header für Static Assets |
### 5.3 Docker Compose (`docker-compose.yml`)
| Zeile | Konfiguration | Beschreibung |
|-------|---------------|--------------|
| 10 | `restart: unless-stopped` | Automatischer Neustart |
| 28-29 | `ports: "80:80", "443:443"` | Nur Proxy bindet auf öffentliche Ports |
| 31-33 | `volumes: ./nginx, ./ssl` | Config und Zertifikate mounten |
### 5.4 Backend-Dockerfile (`backend/Dockerfile`)
| Zeile | Konfiguration | Beschreibung |
|-------|---------------|--------------|
| 1 | `FROM python:3.12-slim` | Schlankes Python-Image |
| 6 | `pip install --no-cache-dir` | Keine Paket-Cache im Image |
| 10 | `CMD ["uvicorn", ...]` | FastAPI mit Uvicorn starten |
### 5.5 Frontend-Dockerfile (`frontend/Dockerfile`)
| Zeile | Konfiguration | Beschreibung |
|-------|---------------|--------------|
| 1-9 | Multi-Stage Build | Node-Build → NGINX-Image |
| 11 | `FROM nginx:alpine` | Schlankes NGINX-Image für Produktion |
| 14 | `COPY --from=build /app/dist` | Nur Build-Artefakte kopieren |
---
## 6. Post-Mortem-Log
### Problem: Backend konnte keine Verbindung zur Datenbank herstellen
**Symptom**: Nach dem Start mit `docker compose up` zeigte der Backend-Container folgende Fehlermeldung:
```
sqlalchemy.exc.OperationalError: (asyncpg.exceptions.ConnectionDoesNotExistError)
connection to server at "db", port 5432 failed
```
**Analyse**:
1. **Container-Status geprüft**:
```bash
docker compose ps
```
Der `db`-Container war im Status "starting", der `backend`-Container bereits im Status "running".
2. **Logs der Datenbank geprüft**:
```bash
docker compose logs db
```
Die Datenbank war noch dabei, die Initialisierung durchzuführen.
3. **Ursache identifiziert**: Der `depends_on` Eintrag im `docker-compose.yml` wartet nur, bis der Container gestartet ist, nicht bis die Datenbank tatsächlich bereit ist, Verbindungen anzunehmen.
**Lösung**:
Die `lifespan`-Funktion in `backend/app/main.py` wurde implementiert, die beim Start der Anwendung die Datenbank-Tabellen erstellt:
```python
@asynccontextmanager
async def lifespan(app: FastAPI):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
```
Zusätzlich wurde ein `restart: unless-stopped` Policy für alle Container konfiguriert, sodass der Backend-Container bei einem fehlgeschlagenen Start automatisch neu gestartet wird und es erneut versucht, sobald die Datenbank bereit ist.
**Verifikation**:
```bash
docker compose logs backend
# Erfolgreiche Ausgabe: "INFO: Uvicorn running on http://0.0.0.0:8000"
```
---
## 7. Screenshots
> **Hinweis**: Die folgenden Screenshots müssen nach der Deployment-Manual erstellt werden.
### 7.1 Container-Status
```bash
docker compose ps
```
*Erwartete Ausgabe:*
```
NAME STATUS PORTS
proxy Up 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
frontend Up 80/tcp
backend Up 8000/tcp
db Up 5432/tcp
```
### 7.2 HTTPS-Verbindung testen
```bash
curl -I https://static.155.116.167.89.clients.your-server.de
```
*Erwartete Ausgabe (Auszug):*
```
HTTP/2 200
strict-transport-security: max-age=63072000; includeSubDomains
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), camera=(), microphone=()
```
### 7.3 HTTP → HTTPS Redirect testen
```bash
curl -I http://static.155.116.167.89.clients.your-server.de
```
*Erwartete Ausgabe:*
```
HTTP/1.1 301 Moved Permanently
Location: https://static.155.116.167.89.clients.your-server.de/
```
### 7.4 API-Endpoint testen
```bash
# Paste erstellen
curl -X POST https://static.155.116.167.89.clients.your-server.de/api/pastes \
-H "Content-Type: application/json" \
-d '{"content": "Test Paste"}'
```
*Erwartete Ausgabe:*
```json
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"delete_token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"created_at": "2026-01-01T12:00:00Z"
}
```
### 7.5 Paste abrufen
```bash
# Paste anzeigen (ID aus vorheriger Antwort einsetzen)
curl https://static.155.116.167.89.clients.your-server.de/api/pastes/<ID>
```
### 7.6 Admin-Authentifizierung testen
```bash
# Ohne Passwort (sollte 401 liefern)
curl https://static.155.116.167.89.clients.your-server.de/api/admin/pastes
# Mit Passwort
curl -H "X-Admin-Password: <ADMIN_PASSWORD>" \
https://static.155.116.167.89.clients.your-server.de/api/admin/pastes
```
### 7.7 Server-Header prüfen
```bash
curl -sI https://static.155.116.167.89.clients.your-server.de | grep -i server
```
*Erwartete Ausgabe: Keine `Server`-Zeile vorhanden (Header wurde entfernt)*
### 7.8 Frontend-Oberfläche
*Screenshot der Startseite unter `https://static.155.116.167.89.clients.your-server.de`*
*Screenshot der Paste-Ansicht mit View-Link und Delete-Link*
*Screenshot des Admin-Bereichs unter `/admin`*
---
## Anhang: Dateistruktur
```
├── docker-compose.yml # Container-Orchestrierung
├── .env # Umgebungsvariablen (nicht im Repo)
├── .gitignore
├── dokumentation.md # Diese Dokumentation
├── nginx/
│ └── nginx.conf # Reverse-Proxy-Konfiguration
├── ssl/
│ ├── cert.pem # Öffentliches Zertifikat
│ ├── key.pem # Privater Schlüssel
│ └── README.md # Anleitung für Zertifikat
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/
│ ├── main.py # FastAPI-Anwendung
│ ├── models.py # SQLAlchemy-Modelle
│ ├── schemas.py # Pydantic-Schemas
│ ├── database.py # Datenbankverbindung
│ └── config.py # Konfiguration
└── frontend/
├── Dockerfile
├── nginx.conf # Frontend-NGINX-Konfiguration
├── package.json
├── vite.config.ts
├── tsconfig.json
├── index.html
└── src/
├── main.tsx # React-Einstiegspunkt
├── App.tsx # Layout
├── api/client.ts # API-Funktionen
└── pages/
├── HomePage.tsx # Paste erstellen
├── PastePage.tsx # Paste anzeigen
└── AdminPage.tsx # Admin-Bereich
```