Fixed findings
This commit is contained in:
14
.gitignore
vendored
14
.gitignore
vendored
@@ -5,7 +5,7 @@
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
# Passwort-Dateien NICHT committen!
|
# Passwort-Dateien NICHT committen!
|
||||||
passwords.txt
|
config/passwords.txt
|
||||||
|
|
||||||
# Wenn die Datei mit echten Passwörtern gefüllt ist
|
# Wenn die Datei mit echten Passwörtern gefüllt ist
|
||||||
# NIEMALS committen!
|
# NIEMALS committen!
|
||||||
@@ -13,9 +13,15 @@ passwords.txt
|
|||||||
# Mosquitto Logs
|
# Mosquitto Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Docker Volumes
|
# Docker Volumes / Persistente Daten
|
||||||
mosquitto_data/
|
data/*
|
||||||
mosquitto_log/
|
log/*
|
||||||
|
mqttui-data/*
|
||||||
|
|
||||||
|
# Erlaube Platzhalter-Dateien
|
||||||
|
!data/.gitkeep
|
||||||
|
!log/.gitkeep
|
||||||
|
!mqttui-data/.gitkeep
|
||||||
|
|
||||||
# Backup Files
|
# Backup Files
|
||||||
*.bak
|
*.bak
|
||||||
|
|||||||
62
CLAUDE.md
62
CLAUDE.md
@@ -22,7 +22,7 @@ This is a Docker-based MQTT server setup using Eclipse Mosquitto broker with a w
|
|||||||
|
|
||||||
### Authentication & Authorization
|
### Authentication & Authorization
|
||||||
- **Dual authentication model**: Supports both anonymous and authenticated users
|
- **Dual authentication model**: Supports both anonymous and authenticated users
|
||||||
- Anonymous users have limited access to `public/#` and `$SYS/#` topics only (defined in config/acl.conf:6-10)
|
- Anonymous users have read-only access to `public/#` and `$SYS/#` topics (defined in config/acl.conf:6-10)
|
||||||
- Authenticated users require username/password stored in `config/passwords.txt`
|
- Authenticated users require username/password stored in `config/passwords.txt`
|
||||||
- Six user types defined (config/acl.conf):
|
- Six user types defined (config/acl.conf):
|
||||||
- `admin`/`joachim`: Full access (readwrite #)
|
- `admin`/`joachim`: Full access (readwrite #)
|
||||||
@@ -35,6 +35,7 @@ This is a Docker-based MQTT server setup using Eclipse Mosquitto broker with a w
|
|||||||
- `config/mosquitto.conf`: Main broker configuration (listeners, persistence, auth, logging)
|
- `config/mosquitto.conf`: Main broker configuration (listeners, persistence, auth, logging)
|
||||||
- `config/acl.conf`: Access Control Lists defining per-user topic permissions
|
- `config/acl.conf`: Access Control Lists defining per-user topic permissions
|
||||||
- `config/passwords.txt`: Generated by setup.sh, stores hashed passwords (not in repo)
|
- `config/passwords.txt`: Generated by setup.sh, stores hashed passwords (not in repo)
|
||||||
|
- `config/passwords.txt.template`: Tracked placeholder copied to `passwords.txt` on bootstrap
|
||||||
- `.env`: Environment variables for credentials and settings (not in repo, use .env.example)
|
- `.env`: Environment variables for credentials and settings (not in repo, use .env.example)
|
||||||
- `mqtt-panel-config.json`: Legacy config file (current setup uses mqttui instead of mqtt-panel)
|
- `mqtt-panel-config.json`: Legacy config file (current setup uses mqttui instead of mqtt-panel)
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ cp .env.example .env
|
|||||||
chmod +x setup.sh test-mqtt.sh
|
chmod +x setup.sh test-mqtt.sh
|
||||||
|
|
||||||
# Start containers
|
# Start containers
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
# Create MQTT users from .env credentials
|
# Create MQTT users from .env credentials
|
||||||
./setup.sh
|
./setup.sh
|
||||||
@@ -59,34 +60,34 @@ docker-compose up -d
|
|||||||
### Container Management
|
### Container Management
|
||||||
```bash
|
```bash
|
||||||
# Start all services
|
# Start all services
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
docker-compose logs -f # All services
|
docker compose logs -f # All services
|
||||||
docker-compose logs -f mosquitto # Mosquitto only
|
docker compose logs -f mosquitto # Mosquitto only
|
||||||
docker-compose logs -f mqttui # MQTTUI only
|
docker compose logs -f mqttui # MQTTUI only
|
||||||
|
|
||||||
# Restart services
|
# Restart services
|
||||||
docker-compose restart # All
|
docker compose restart # All
|
||||||
docker-compose restart mosquitto # Mosquitto only
|
docker compose restart mosquitto # Mosquitto only
|
||||||
|
|
||||||
# Stop and remove containers
|
# Stop and remove containers
|
||||||
docker-compose down
|
docker compose down
|
||||||
|
|
||||||
# Stop and remove including volumes
|
# Stop and remove including volumes
|
||||||
docker-compose down -v
|
docker compose down -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### User Management
|
### User Management
|
||||||
```bash
|
```bash
|
||||||
# Add/update user password
|
# Add/update user password
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt USERNAME PASSWORD
|
docker compose exec -T mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt USERNAME PASSWORD
|
||||||
|
|
||||||
# Delete user
|
# Delete user
|
||||||
docker exec -it mosquitto mosquitto_passwd -D /mosquitto/config/passwords.txt USERNAME
|
docker compose exec -T mosquitto mosquitto_passwd -D /mosquitto/config/passwords.txt USERNAME
|
||||||
|
|
||||||
# After modifying users or ACL, restart Mosquitto
|
# After modifying users or ACL, restart Mosquitto
|
||||||
docker-compose restart mosquitto
|
docker compose restart mosquitto
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing & Debugging
|
### Testing & Debugging
|
||||||
@@ -94,24 +95,21 @@ docker-compose restart mosquitto
|
|||||||
# Run test script (sends sample messages to various topics)
|
# Run test script (sends sample messages to various topics)
|
||||||
./test-mqtt.sh
|
./test-mqtt.sh
|
||||||
|
|
||||||
# Publish to public topic (no auth required)
|
# Subscribe to public topic (no auth required)
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "public/test" -m "Hello"
|
docker compose exec -T mosquitto mosquitto_sub -h localhost -t "public/#" -v
|
||||||
|
|
||||||
# Publish with authentication
|
# Publish with authentication
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "sensors/temperature" -m "22.5" -u admin -P admin123
|
docker compose exec -T mosquitto mosquitto_pub -h localhost -t "sensors/temperature" -m "22.5" -u admin -P $MQTT_ADMIN_PASSWORD
|
||||||
|
|
||||||
# Subscribe to all topics
|
# Subscribe to all topics
|
||||||
docker exec mosquitto mosquitto_sub -h localhost -t '#' -v -u admin -P admin123
|
docker compose exec -T mosquitto mosquitto_sub -h localhost -t '#' -v -u admin -P $MQTT_ADMIN_PASSWORD
|
||||||
|
|
||||||
# Subscribe to public topics only (no auth)
|
|
||||||
docker exec mosquitto mosquitto_sub -h localhost -t 'public/#' -v
|
|
||||||
|
|
||||||
# Test Mosquitto configuration
|
# Test Mosquitto configuration
|
||||||
docker exec mosquitto mosquitto -c /mosquitto/config/mosquitto.conf -v
|
docker compose exec -T mosquitto mosquitto -c /mosquitto/config/mosquitto.conf -v
|
||||||
|
|
||||||
# Access container shell
|
# Access container shell
|
||||||
docker exec -it mosquitto sh
|
docker compose exec -it mosquitto sh
|
||||||
docker exec -it mqttui sh
|
docker compose exec -it mqttui sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
@@ -119,7 +117,7 @@ docker exec -it mqttui sh
|
|||||||
### Security Considerations
|
### Security Considerations
|
||||||
- `.env` file contains all credentials and MUST NOT be committed (already in .gitignore)
|
- `.env` file contains all credentials and MUST NOT be committed (already in .gitignore)
|
||||||
- Default passwords in .env.example must be changed for production
|
- Default passwords in .env.example must be changed for production
|
||||||
- Anonymous access is enabled but restricted to `public/#` topics via ACL
|
- Anonymous access is enabled but restricted to read-only `public/#` topics via ACL
|
||||||
- All credentials are loaded from .env by setup.sh (lines setup.sh:8-18)
|
- All credentials are loaded from .env by setup.sh (lines setup.sh:8-18)
|
||||||
- The SECRET_KEY in .env is used by mqttui for session management
|
- The SECRET_KEY in .env is used by mqttui for session management
|
||||||
|
|
||||||
@@ -128,10 +126,10 @@ docker exec -it mqttui sh
|
|||||||
- Anonymous users get explicit rules defined under `user anonymous`
|
- Anonymous users get explicit rules defined under `user anonymous`
|
||||||
- Authenticated users inherit their specific user rules
|
- Authenticated users inherit their specific user rules
|
||||||
- Pattern `#` is wildcard for all topics, `+` for single-level wildcard
|
- Pattern `#` is wildcard for all topics, `+` for single-level wildcard
|
||||||
- After ACL changes, always restart mosquitto: `docker-compose restart mosquitto`
|
- After ACL changes, always restart mosquitto: `docker compose restart mosquitto`
|
||||||
|
|
||||||
### MQTTUI Dashboard
|
### MQTTUI Dashboard
|
||||||
- The docker-compose.yml uses mqttui (not mqtt-panel as mentioned in README.md)
|
- The docker compose.yml uses mqttui (not mqtt-panel as mentioned in README.md)
|
||||||
- Dashboard connects to broker using credentials from .env: MQTT_PANEL_USERNAME/PASSWORD
|
- Dashboard connects to broker using credentials from .env: MQTT_PANEL_USERNAME/PASSWORD
|
||||||
- Database storage enabled (DB_PATH=/app/data/mqtt_messages.db) with cleanup after 30 days
|
- Database storage enabled (DB_PATH=/app/data/mqtt_messages.db) with cleanup after 30 days
|
||||||
- Max 10,000 messages retained in database (DB_MAX_MESSAGES)
|
- Max 10,000 messages retained in database (DB_MAX_MESSAGES)
|
||||||
@@ -140,12 +138,12 @@ docker exec -it mqttui sh
|
|||||||
### File Permissions
|
### File Permissions
|
||||||
- Mosquitto runs as UID:GID specified in .env (default 1000:1000)
|
- Mosquitto runs as UID:GID specified in .env (default 1000:1000)
|
||||||
- Ensure config/, data/, and log/ directories have correct permissions
|
- Ensure config/, data/, and log/ directories have correct permissions
|
||||||
- passwords.txt should be readable by the mosquitto user (chmod 644)
|
- config/passwords.txt should be readable by the mosquitto user (chmod 640)
|
||||||
|
|
||||||
### Persistence
|
### Persistence
|
||||||
- MQTT messages persist in `./data/mosquitto.db`
|
- MQTT messages persist in `./data/mosquitto.db`
|
||||||
- MQTTUI data stored in `./mqttui-data/mqtt_messages.db`
|
- MQTTUI data stored in `./mqttui-data/mqtt_messages.db`
|
||||||
- To completely reset: `docker-compose down -v` and remove data/log directories
|
- To completely reset: `docker compose down -v` and remove data/log directories
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
@@ -160,16 +158,16 @@ docker exec -it mqttui sh
|
|||||||
2. Update setup.sh to create the user (lines setup.sh:59-86)
|
2. Update setup.sh to create the user (lines setup.sh:59-86)
|
||||||
3. Add ACL rules in config/acl.conf
|
3. Add ACL rules in config/acl.conf
|
||||||
4. Run `./setup.sh` to create user
|
4. Run `./setup.sh` to create user
|
||||||
5. Restart mosquitto: `docker-compose restart mosquitto`
|
5. Restart mosquitto: `docker compose restart mosquitto`
|
||||||
|
|
||||||
### Debugging ACL Issues
|
### Debugging ACL Issues
|
||||||
1. Enable verbose logging in config/mosquitto.conf (add `log_type all`)
|
1. Enable verbose logging in config/mosquitto.conf (add `log_type all`)
|
||||||
2. Restart mosquitto: `docker-compose restart mosquitto`
|
2. Restart mosquitto: `docker compose restart mosquitto`
|
||||||
3. Check logs: `docker-compose logs -f mosquitto`
|
3. Check logs: `docker compose logs -f mosquitto`
|
||||||
4. Look for "DENIED" messages indicating ACL blocks
|
4. Look for "DENIED" messages indicating ACL blocks
|
||||||
|
|
||||||
### Client Integration
|
### Client Integration
|
||||||
- Use MQTT port 1883 for native MQTT clients (Python paho-mqtt, etc.)
|
- Use MQTT port 1883 for native MQTT clients (Python paho-mqtt, etc.)
|
||||||
- Use WebSocket port 9001 for browser-based clients
|
- Use WebSocket port 9001 for browser-based clients
|
||||||
- Provide username/password from .env for authenticated topics
|
- Provide username/password from .env for authenticated topics
|
||||||
- Use anonymous connection only for public/* topics
|
- Use anonymous connection only for read-only public/* topics
|
||||||
|
|||||||
60
README.md
60
README.md
@@ -9,7 +9,7 @@ Komplettes MQTT Setup mit Eclipse Mosquitto Broker und MQTTUI Web Dashboard.
|
|||||||
- WebSocket auf Port 9001
|
- WebSocket auf Port 9001
|
||||||
- Passwort-Authentifizierung
|
- Passwort-Authentifizierung
|
||||||
- ACL (Access Control Lists)
|
- ACL (Access Control Lists)
|
||||||
- Öffentliches Topic ohne Anmeldung (`public/*`)
|
- Öffentliches Read-Only Topic ohne Anmeldung (`public/*`)
|
||||||
- Persistenz aktiviert
|
- Persistenz aktiviert
|
||||||
|
|
||||||
✅ **Web Dashboard (MQTTUI)**
|
✅ **Web Dashboard (MQTTUI)**
|
||||||
@@ -25,6 +25,7 @@ mqtt/
|
|||||||
├── config/ # Konfigurationsdateien
|
├── config/ # Konfigurationsdateien
|
||||||
│ ├── mosquitto.conf # Mosquitto Hauptkonfiguration
|
│ ├── mosquitto.conf # Mosquitto Hauptkonfiguration
|
||||||
│ ├── acl.conf # Access Control Lists
|
│ ├── acl.conf # Access Control Lists
|
||||||
|
│ ├── passwords.txt.template # Platzhalter zum Bootstrap
|
||||||
│ └── passwords.txt # User/Passwort Datei (wird generiert)
|
│ └── passwords.txt # User/Passwort Datei (wird generiert)
|
||||||
├── data/ # Mosquitto Persistenz Daten
|
├── data/ # Mosquitto Persistenz Daten
|
||||||
├── log/ # Mosquitto Log-Dateien
|
├── log/ # Mosquitto Log-Dateien
|
||||||
@@ -52,9 +53,20 @@ cp .env.example .env
|
|||||||
nano .env
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> `UID` und `GID` in der `.env` bestimmen jetzt den User, unter dem der Mosquitto-Container läuft. Passe die Werte an deinen Host-User an, falls du nicht `1000:1000` bist.
|
||||||
|
|
||||||
⚠️ **WICHTIG**: Setze sichere Passwörter in der `.env` Datei!
|
⚠️ **WICHTIG**: Setze sichere Passwörter in der `.env` Datei!
|
||||||
|
|
||||||
### 2. Server starten
|
### 2. Passwort-Datei vorbereiten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Platzhalter kopieren, damit der Container starten kann
|
||||||
|
cp config/passwords.txt.template config/passwords.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
> Das Setup-Script erstellt die Datei bei Bedarf automatisch. Wenn du manuell startest, führe den Kopier-Befehl einmalig aus.
|
||||||
|
|
||||||
|
### 3. Server starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Container starten
|
# Container starten
|
||||||
@@ -64,7 +76,7 @@ docker compose up -d
|
|||||||
docker compose logs -f
|
docker compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Benutzer erstellen
|
### 4. Benutzer erstellen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Setup-Script ausführbar machen
|
# Setup-Script ausführbar machen
|
||||||
@@ -95,7 +107,7 @@ Das Script erstellt folgende User (Credentials aus .env):
|
|||||||
|
|
||||||
Ohne ACL-Eintrag hat der neue User keine Zugriffsrechte!
|
Ohne ACL-Eintrag hat der neue User keine Zugriffsrechte!
|
||||||
|
|
||||||
### 4. Dashboard öffnen
|
### 5. Dashboard öffnen
|
||||||
|
|
||||||
Web Dashboard: **http://localhost:5000**
|
Web Dashboard: **http://localhost:5000**
|
||||||
|
|
||||||
@@ -107,17 +119,16 @@ Web Dashboard: **http://localhost:5000**
|
|||||||
- **WebSocket**: `ws://localhost:9001`
|
- **WebSocket**: `ws://localhost:9001`
|
||||||
- **Web Dashboard**: `http://localhost:5000`
|
- **Web Dashboard**: `http://localhost:5000`
|
||||||
|
|
||||||
### Öffentliches Topic (ohne Anmeldung)
|
### Öffentliche Topics (ohne Anmeldung, read-only)
|
||||||
|
|
||||||
Topic: `public/*`
|
Topic: `public/*`
|
||||||
|
|
||||||
**Beispiel mit mosquitto_pub:**
|
|
||||||
```bash
|
```bash
|
||||||
# Nachricht an öffentliches Topic senden (KEINE Authentifizierung)
|
# Öffentliches Topic abhören (keine Auth nötig)
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "public/test" -m "Hallo Welt!"
|
docker compose exec -T mosquitto mosquitto_sub -h localhost -t "public/#" -v
|
||||||
|
|
||||||
# Öffentliches Topic abhören
|
# Zum Publishen sind Credentials erforderlich, z.B. Admin:
|
||||||
docker exec mosquitto mosquitto_sub -h localhost -t "public/#" -v
|
docker compose exec -T mosquitto mosquitto_pub -h localhost -t "public/test" -m "Hallo Welt" -u admin -P <ADMIN_PASSWORD>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mit Authentifizierung
|
### Mit Authentifizierung
|
||||||
@@ -125,10 +136,10 @@ docker exec mosquitto mosquitto_sub -h localhost -t "public/#" -v
|
|||||||
**Beispiel mit Admin User:**
|
**Beispiel mit Admin User:**
|
||||||
```bash
|
```bash
|
||||||
# Mit Authentifizierung publishen (Credentials aus .env verwenden)
|
# Mit Authentifizierung publishen (Credentials aus .env verwenden)
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "devices/device1/status" -m "online" -u admin -P <ADMIN_PASSWORD>
|
docker compose exec -T mosquitto mosquitto_pub -h localhost -t "devices/device1/status" -m "online" -u admin -P <ADMIN_PASSWORD>
|
||||||
|
|
||||||
# Mit Authentifizierung subscriben (Credentials aus .env verwenden)
|
# Mit Authentifizierung subscriben (Credentials aus .env verwenden)
|
||||||
docker exec mosquitto mosquitto_sub -h localhost -t "#" -v -u admin -P <ADMIN_PASSWORD>
|
docker compose exec -T mosquitto mosquitto_sub -h localhost -t "#" -v -u admin -P <ADMIN_PASSWORD>
|
||||||
```
|
```
|
||||||
|
|
||||||
💡 **Hinweis**: Ersetze `<ADMIN_PASSWORD>` mit dem Passwort aus deiner `.env` Datei.
|
💡 **Hinweis**: Ersetze `<ADMIN_PASSWORD>` mit dem Passwort aus deiner `.env` Datei.
|
||||||
@@ -151,7 +162,7 @@ docker compose restart mosquitto
|
|||||||
**Alternativ - Manuell:**
|
**Alternativ - Manuell:**
|
||||||
```bash
|
```bash
|
||||||
# User hinzufügen/ändern
|
# User hinzufügen/ändern
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt USERNAME PASSWORD
|
docker compose exec -T mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt USERNAME PASSWORD
|
||||||
|
|
||||||
# ACL in config/acl.conf anpassen und Container neu starten
|
# ACL in config/acl.conf anpassen und Container neu starten
|
||||||
docker compose restart mosquitto
|
docker compose restart mosquitto
|
||||||
@@ -160,7 +171,7 @@ docker compose restart mosquitto
|
|||||||
### User löschen
|
### User löschen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec -it mosquitto mosquitto_passwd -D /mosquitto/config/passwords.txt USERNAME
|
docker compose exec -T mosquitto mosquitto_passwd -D /mosquitto/config/passwords.txt USERNAME
|
||||||
|
|
||||||
# Container neu starten
|
# Container neu starten
|
||||||
docker compose restart mosquitto
|
docker compose restart mosquitto
|
||||||
@@ -257,19 +268,20 @@ Du kannst den Workflow nach dem Import anpassen:
|
|||||||
### Via Docker
|
### Via Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Öffentlich (ohne Auth)
|
# Öffentlich (ohne Auth, read-only → Sub only)
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "public/message" -m "Test Nachricht"
|
docker compose exec -T mosquitto mosquitto_sub -h localhost -t "public/#" -v
|
||||||
|
|
||||||
# Mit Auth (Passwort aus .env verwenden)
|
# Mit Auth (Passwort aus .env verwenden)
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "sensors/temperature" -m "22.5" -u admin -P <ADMIN_PASSWORD>
|
docker compose exec -T mosquitto mosquitto_pub -h localhost -t "sensors/temperature" -m "22.5" -u admin -P <ADMIN_PASSWORD>
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "sensors/humidity" -m "65" -u admin -P <ADMIN_PASSWORD>
|
docker compose exec -T mosquitto mosquitto_pub -h localhost -t "sensors/humidity" -m "65" -u admin -P <ADMIN_PASSWORD>
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "system/cpu" -m "45" -u admin -P <ADMIN_PASSWORD>
|
docker compose exec -T mosquitto mosquitto_pub -h localhost -t "system/cpu" -m "45" -u admin -P <ADMIN_PASSWORD>
|
||||||
```
|
```
|
||||||
|
|
||||||
Oder verwende das Test-Script:
|
Oder verwende das Test-Script:
|
||||||
```bash
|
```bash
|
||||||
./test-mqtt.sh
|
./test-mqtt.sh
|
||||||
```
|
```
|
||||||
|
> Das Script lädt Benutzername/Passwort aus `.env` (Variablen `MQTT_ADMIN_USERNAME`/`MQTT_ADMIN_PASSWORD`).
|
||||||
|
|
||||||
### Via Python (paho-mqtt)
|
### Via Python (paho-mqtt)
|
||||||
|
|
||||||
@@ -343,23 +355,23 @@ docker compose logs -f mqttui
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Mosquitto Container
|
# Mosquitto Container
|
||||||
docker exec -it mosquitto sh
|
docker compose exec -it mosquitto sh
|
||||||
|
|
||||||
# MQTTUI Container
|
# MQTTUI Container
|
||||||
docker exec -it mqttui sh
|
docker compose exec -it mqttui sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mosquitto Konfiguration testen
|
### Mosquitto Konfiguration testen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec mosquitto mosquitto -c /mosquitto/config/mosquitto.conf -v
|
docker compose exec -T mosquitto mosquitto -c /mosquitto/config/mosquitto.conf -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### Permissions Fehler
|
### Permissions Fehler
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Berechtigungen für passwords.txt setzen
|
# Berechtigungen für config/passwords.txt setzen
|
||||||
chmod 644 passwords.txt
|
chmod 640 config/passwords.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### ACL Debug
|
### ACL Debug
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
# Anonymous User haben Zugriff auf public/* Topics
|
# Anonymous User haben Zugriff auf public/* Topics
|
||||||
user anonymous
|
user anonymous
|
||||||
topic read public/#
|
topic read public/#
|
||||||
topic write public/#
|
|
||||||
# Erlaube anonymous Lesezugriff auf $SYS/# für Healthchecks und Monitoring
|
# Erlaube anonymous Lesezugriff auf $SYS/# für Healthchecks und Monitoring
|
||||||
topic read $SYS/#
|
topic read $SYS/#
|
||||||
|
|
||||||
|
|||||||
3
config/passwords.txt.template
Normal file
3
config/passwords.txt.template
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Placeholder password file for Mosquitto.
|
||||||
|
# This file is copied to config/passwords.txt automatically when you run setup.sh.
|
||||||
|
# It intentionally contains no credentials. Run ./setup.sh after editing .env to populate it.
|
||||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@ services:
|
|||||||
image: eclipse-mosquitto:2
|
image: eclipse-mosquitto:2
|
||||||
container_name: mosquitto
|
container_name: mosquitto
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
user: "1000:1000"
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
ports:
|
ports:
|
||||||
- "1883:1883"
|
- "1883:1883"
|
||||||
- "9001:9001"
|
- "9001:9001"
|
||||||
|
|||||||
0
log/.gitkeep
Normal file
0
log/.gitkeep
Normal file
0
mqttui-data/.gitkeep
Normal file
0
mqttui-data/.gitkeep
Normal file
Binary file not shown.
158
setup.sh
Normal file → Executable file
158
setup.sh
Normal file → Executable file
@@ -1,40 +1,85 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# MQTT Setup Script - Erstellt User für Mosquitto
|
set -euo pipefail
|
||||||
# ================================================
|
|
||||||
#
|
|
||||||
# Verwendung:
|
|
||||||
# ./setup.sh - Erstellt alle User aus .env
|
|
||||||
# ./setup.sh create <username> <password> - Erstellt einzelnen User
|
|
||||||
|
|
||||||
set -e
|
COMPOSE_CMD=${COMPOSE_CMD:-"docker compose"}
|
||||||
|
PASSWORD_FILE="config/passwords.txt"
|
||||||
|
PASSWORD_TEMPLATE="config/passwords.txt.template"
|
||||||
|
|
||||||
|
ensure_password_file() {
|
||||||
|
if [ -f "$PASSWORD_FILE" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$PASSWORD_TEMPLATE" ]; then
|
||||||
|
cp "$PASSWORD_TEMPLATE" "$PASSWORD_FILE"
|
||||||
|
else
|
||||||
|
touch "$PASSWORD_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 640 "$PASSWORD_FILE" 2>/dev/null || true
|
||||||
|
echo "🆕 Passwort-Datei $PASSWORD_FILE angelegt."
|
||||||
|
}
|
||||||
|
|
||||||
|
require_docker() {
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "❌ Fehler: Docker ist nicht gestartet!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
mosquitto_running() {
|
||||||
|
docker ps --format '{{.Names}}' | grep -qx 'mosquitto'
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_mosquitto_service() {
|
||||||
|
if mosquitto_running; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "⚠️ Mosquitto Container läuft nicht. Starte Container..."
|
||||||
|
ensure_password_file
|
||||||
|
$COMPOSE_CMD up -d mosquitto
|
||||||
|
echo "⏳ Warte 5 Sekunden bis Mosquitto gestartet ist..."
|
||||||
|
sleep 5
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_exec_mosquitto() {
|
||||||
|
$COMPOSE_CMD exec -T mosquitto "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_user() {
|
||||||
|
local USERNAME=$1
|
||||||
|
local PASSWORD=$2
|
||||||
|
local LABEL=$3
|
||||||
|
|
||||||
|
echo "➡️ Erstelle $LABEL"
|
||||||
|
compose_exec_mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$USERNAME" "$PASSWORD"
|
||||||
|
echo " ✅ User: $USERNAME erstellt"
|
||||||
|
}
|
||||||
|
|
||||||
# CLI-Modus: Einzelnen User erstellen
|
# CLI-Modus: Einzelnen User erstellen
|
||||||
if [ "$1" == "create" ]; then
|
if [ "${1:-}" == "create" ]; then
|
||||||
if [ -z "$2" ] || [ -z "$3" ]; then
|
if [ -z "${2:-}" ] || [ -z "${3:-}" ]; then
|
||||||
echo "❌ Fehler: Username und Passwort erforderlich!"
|
echo "❌ Fehler: Username und Passwort erforderlich!"
|
||||||
echo "Verwendung: $0 create <username> <password>"
|
echo "Verwendung: $0 create <username> <password>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
require_docker
|
||||||
|
ensure_password_file
|
||||||
|
|
||||||
|
if ! mosquitto_running; then
|
||||||
|
echo "❌ Fehler: Mosquitto Container läuft nicht!"
|
||||||
|
echo "Starte zuerst die Container mit: docker compose up -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
USERNAME="$2"
|
USERNAME="$2"
|
||||||
PASSWORD="$3"
|
PASSWORD="$3"
|
||||||
|
|
||||||
# Prüfe ob Docker läuft
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
echo "❌ Fehler: Docker ist nicht gestartet!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe ob Container läuft
|
|
||||||
if ! docker ps | grep -q mosquitto; then
|
|
||||||
echo "❌ Fehler: Mosquitto Container läuft nicht!"
|
|
||||||
echo "Starte zuerst die Container mit: docker compose up -d"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📝 Erstelle User: $USERNAME"
|
echo "📝 Erstelle User: $USERNAME"
|
||||||
docker exec mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$USERNAME" "$PASSWORD"
|
compose_exec_mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$USERNAME" "$PASSWORD"
|
||||||
echo "✅ User $USERNAME erfolgreich erstellt/aktualisiert!"
|
echo "✅ User $USERNAME erfolgreich erstellt/aktualisiert!"
|
||||||
echo ""
|
echo ""
|
||||||
echo "⚠️ Vergiss nicht, ACL-Regeln in config/acl.conf anzupassen!"
|
echo "⚠️ Vergiss nicht, ACL-Regeln in config/acl.conf anzupassen!"
|
||||||
@@ -42,24 +87,25 @@ if [ "$1" == "create" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Lade Umgebungsvariablen aus .env
|
|
||||||
if [ ! -f .env ]; then
|
if [ ! -f .env ]; then
|
||||||
echo "❌ Fehler: .env Datei nicht gefunden!"
|
echo "❌ Fehler: .env Datei nicht gefunden!"
|
||||||
echo "Bitte erstelle eine .env Datei basierend auf .env.example"
|
echo "Bitte erstelle eine .env Datei basierend auf .env.example"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Lade .env und exportiere Variablen
|
|
||||||
set -a
|
set -a
|
||||||
source .env
|
source .env
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
|
ensure_password_file
|
||||||
|
require_docker
|
||||||
|
ensure_mosquitto_service
|
||||||
|
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo "MQTT Server Setup - Benutzer erstellen"
|
echo "MQTT Server Setup - Benutzer erstellen"
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Validiere dass alle benötigten Variablen gesetzt sind
|
|
||||||
required_vars=(
|
required_vars=(
|
||||||
"MQTT_ADMIN_USERNAME" "MQTT_ADMIN_PASSWORD"
|
"MQTT_ADMIN_USERNAME" "MQTT_ADMIN_PASSWORD"
|
||||||
"MQTT_PANEL_USERNAME" "MQTT_PANEL_PASSWORD"
|
"MQTT_PANEL_USERNAME" "MQTT_PANEL_PASSWORD"
|
||||||
@@ -76,51 +122,12 @@ for var in "${required_vars[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Prüfe ob Docker läuft
|
create_user "$MQTT_ADMIN_USERNAME" "$MQTT_ADMIN_PASSWORD" "Admin User (Vollzugriff)"
|
||||||
if ! docker info > /dev/null 2>&1; then
|
create_user "$MQTT_PANEL_USERNAME" "$MQTT_PANEL_PASSWORD" "Panel User (für Web Dashboard)"
|
||||||
echo "❌ Fehler: Docker ist nicht gestartet!"
|
create_user "$MQTT_TESTUSER_USERNAME" "$MQTT_TESTUSER_PASSWORD" "Test User"
|
||||||
exit 1
|
create_user "$MQTT_DEVICE1_USERNAME" "$MQTT_DEVICE1_PASSWORD" "Device1 User"
|
||||||
fi
|
create_user "$MQTT_DEVICE2_USERNAME" "$MQTT_DEVICE2_PASSWORD" "Device2 User"
|
||||||
|
create_user "$MQTT_MONITOR_USERNAME" "$MQTT_MONITOR_PASSWORD" "Monitor User (Read-Only)"
|
||||||
# Prüfe ob Container läuft
|
|
||||||
if ! docker ps | grep -q mosquitto; then
|
|
||||||
echo "⚠️ Mosquitto Container läuft nicht. Starte Container..."
|
|
||||||
docker compose up -d mosquitto
|
|
||||||
echo "⏳ Warte 5 Sekunden bis Mosquitto gestartet ist..."
|
|
||||||
sleep 5
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📝 Erstelle Benutzer in der Passwort-Datei..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Admin User
|
|
||||||
echo "➡️ Erstelle Admin User (Vollzugriff)"
|
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$MQTT_ADMIN_USERNAME" "$MQTT_ADMIN_PASSWORD"
|
|
||||||
echo " ✅ User: $MQTT_ADMIN_USERNAME erstellt"
|
|
||||||
|
|
||||||
# Panel User für Web UI
|
|
||||||
echo "➡️ Erstelle Panel User (für Web Dashboard)"
|
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$MQTT_PANEL_USERNAME" "$MQTT_PANEL_PASSWORD"
|
|
||||||
echo " ✅ User: $MQTT_PANEL_USERNAME erstellt"
|
|
||||||
|
|
||||||
# Test User
|
|
||||||
echo "➡️ Erstelle Test User"
|
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$MQTT_TESTUSER_USERNAME" "$MQTT_TESTUSER_PASSWORD"
|
|
||||||
echo " ✅ User: $MQTT_TESTUSER_USERNAME erstellt"
|
|
||||||
|
|
||||||
# Device User
|
|
||||||
echo "➡️ Erstelle Device1 User"
|
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$MQTT_DEVICE1_USERNAME" "$MQTT_DEVICE1_PASSWORD"
|
|
||||||
echo " ✅ User: $MQTT_DEVICE1_USERNAME erstellt"
|
|
||||||
|
|
||||||
echo "➡️ Erstelle Device2 User"
|
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$MQTT_DEVICE2_USERNAME" "$MQTT_DEVICE2_PASSWORD"
|
|
||||||
echo " ✅ User: $MQTT_DEVICE2_USERNAME erstellt"
|
|
||||||
|
|
||||||
# Monitor User (Read-Only)
|
|
||||||
echo "➡️ Erstelle Monitor User (Read-Only)"
|
|
||||||
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwords.txt "$MQTT_MONITOR_USERNAME" "$MQTT_MONITOR_PASSWORD"
|
|
||||||
echo " ✅ User: $MQTT_MONITOR_USERNAME erstellt"
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
@@ -137,15 +144,14 @@ echo " - $MQTT_MONITOR_USERNAME (Read-Only)"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "⚠️ Passwörter sind in der .env Datei definiert"
|
echo "⚠️ Passwörter sind in der .env Datei definiert"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Mosquitto neu laden..."
|
|
||||||
docker exec mosquitto mosquitto -c /mosquitto/config/mosquitto.conf &
|
|
||||||
|
|
||||||
echo ""
|
$COMPOSE_CMD restart mosquitto >/dev/null
|
||||||
|
|
||||||
echo "🚀 MQTT Broker läuft auf:"
|
echo "🚀 MQTT Broker läuft auf:"
|
||||||
echo " - MQTT: localhost:1883"
|
echo " - MQTT: localhost:1883"
|
||||||
echo " - WebSocket: ws://localhost:9001"
|
echo " - WebSocket: ws://localhost:9001"
|
||||||
echo " - Web Dashboard: http://localhost:8080"
|
echo " - Web Dashboard: http://localhost:5000"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📡 Öffentliches Topic ohne Authentifizierung:"
|
echo "📡 Öffentliches Topic ohne Authentifizierung:"
|
||||||
echo " - public/*"
|
echo " - public/* (read-only)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
90
test-mqtt.sh
Normal file → Executable file
90
test-mqtt.sh
Normal file → Executable file
@@ -1,69 +1,73 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# MQTT Test Script - Sendet Test-Nachrichten an verschiedene Topics
|
set -euo pipefail
|
||||||
# ===================================================================
|
|
||||||
|
COMPOSE_CMD=${COMPOSE_CMD:-"docker compose"}
|
||||||
|
CONTAINER_NAME="mosquitto"
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "❌ Fehler: .env nicht gefunden. Bitte kopiere .env.example und setze die Passwörter."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
ADMIN_USER=${MQTT_ADMIN_USERNAME:-}
|
||||||
|
ADMIN_PASS=${MQTT_ADMIN_PASSWORD:-}
|
||||||
|
|
||||||
|
if [ -z "$ADMIN_USER" ] || [ -z "$ADMIN_PASS" ]; then
|
||||||
|
echo "❌ Fehler: MQTT_ADMIN_USERNAME und MQTT_ADMIN_PASSWORD müssen in .env gesetzt sein."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
|
||||||
|
echo "❌ Fehler: Mosquitto Container läuft nicht!"
|
||||||
|
echo " Starte mit: docker compose up -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish() {
|
||||||
|
local topic=$1
|
||||||
|
local payload=$2
|
||||||
|
echo " → $topic : $payload"
|
||||||
|
$COMPOSE_CMD exec -T "$CONTAINER_NAME" mosquitto_pub -h localhost -t "$topic" -m "$payload" -u "$ADMIN_USER" -P "$ADMIN_PASS"
|
||||||
|
}
|
||||||
|
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo "MQTT Server Test"
|
echo "MQTT Server Test"
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Prüfe ob Container läuft
|
|
||||||
if ! docker ps | grep -q mosquitto; then
|
|
||||||
echo "❌ Fehler: Mosquitto Container läuft nicht!"
|
|
||||||
echo " Starte mit: docker-compose up -d"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📡 Sende Test-Nachrichten..."
|
echo "📡 Sende Test-Nachrichten..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Test 1: Öffentliches Topic (OHNE Authentifizierung)
|
echo "1️⃣ Temperature Sensor: sensors/temperature"
|
||||||
echo "1️⃣ Öffentliches Topic (ohne Auth): public/message"
|
publish "sensors/temperature" "22.5"
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "public/message" -m "Hallo von public!"
|
|
||||||
echo " ✅ Gesendet"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 2: Temperature Sensor (MIT Authentifizierung)
|
echo "2️⃣ Humidity Sensor: sensors/humidity"
|
||||||
echo "2️⃣ Temperature Sensor: sensors/temperature"
|
publish "sensors/humidity" "65"
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "sensors/temperature" -m "22.5" -u admin -P admin123
|
|
||||||
echo " ✅ Gesendet: 22.5°C"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 3: Humidity Sensor
|
echo "3️⃣ CPU Usage: system/cpu"
|
||||||
echo "3️⃣ Humidity Sensor: sensors/humidity"
|
publish "system/cpu" "45"
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "sensors/humidity" -m "65" -u admin -P admin123
|
|
||||||
echo " ✅ Gesendet: 65%"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 4: CPU Usage
|
echo "4️⃣ Device Power: devices/device1/power"
|
||||||
echo "4️⃣ CPU Usage: system/cpu"
|
publish "devices/device1/power" "ON"
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "system/cpu" -m "45" -u admin -P admin123
|
|
||||||
echo " ✅ Gesendet: 45%"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 5: Device Power Toggle
|
echo "5️⃣ Device Brightness: devices/device1/brightness"
|
||||||
echo "5️⃣ Device Power: devices/device1/power"
|
publish "devices/device1/brightness" "75"
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "devices/device1/power" -m "ON" -u admin -P admin123
|
|
||||||
echo " ✅ Gesendet: ON"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test 6: Device Brightness
|
|
||||||
echo "6️⃣ Device Brightness: devices/device1/brightness"
|
|
||||||
docker exec mosquitto mosquitto_pub -h localhost -t "devices/device1/brightness" -m "75" -u admin -P admin123
|
|
||||||
echo " ✅ Gesendet: 75%"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo "✅ Test abgeschlossen!"
|
echo "✅ Test abgeschlossen!"
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🌐 Öffne das Dashboard: http://localhost:8080"
|
echo "🌐 Öffne das Dashboard: http://localhost:5000"
|
||||||
echo " Die Test-Daten sollten jetzt in den Widgets sichtbar sein."
|
echo " Die Test-Daten sollten jetzt in den Widgets sichtbar sein."
|
||||||
echo ""
|
echo ""
|
||||||
echo "📊 Subscribe auf alle Topics:"
|
echo "📊 Subscribe auf alle Topics:"
|
||||||
echo " docker exec mosquitto mosquitto_sub -h localhost -t '#' -v -u admin -P admin123"
|
echo " $COMPOSE_CMD exec -T mosquitto mosquitto_sub -h localhost -t '#' -v -u $ADMIN_USER -P <ADMIN_PASSWORD>"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📡 Subscribe auf öffentliche Topics (ohne Auth):"
|
echo "📡 Subscribe auf öffentliche Topics (ohne Auth):"
|
||||||
echo " docker exec mosquitto mosquitto_sub -h localhost -t 'public/#' -v"
|
echo " $COMPOSE_CMD exec -T mosquitto mosquitto_sub -h localhost -t 'public/#' -v"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
Reference in New Issue
Block a user