Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8fb95b68e | ||
|
|
c0b6865790 | ||
|
|
6e7ae0d6f9 | ||
|
|
6a5b12f98e | ||
|
|
d8eb7b0160 | ||
|
|
962c04084b |
@@ -22,8 +22,10 @@ tests/
|
|||||||
# Pi-specific
|
# Pi-specific
|
||||||
rpi/
|
rpi/
|
||||||
*.img
|
*.img
|
||||||
|
*.img.xz
|
||||||
*.img.zst
|
*.img.zst
|
||||||
*.img.zst.zip
|
*.img.zst.zip
|
||||||
|
pishrink.sh
|
||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
*.md
|
*.md
|
||||||
@@ -37,3 +39,15 @@ docs/
|
|||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Dev scripts and old files
|
||||||
|
scripts/
|
||||||
|
old_files/
|
||||||
|
*_old
|
||||||
|
*_old.*
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
frontends/web/temp_files/
|
||||||
|
*.db
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -80,3 +80,7 @@ tests/
|
|||||||
*.img.xz
|
*.img.xz
|
||||||
*.img.zst
|
*.img.zst
|
||||||
pishrink.sh
|
pishrink.sh
|
||||||
|
*.img.zst.zip
|
||||||
|
|
||||||
|
# Temp file storage
|
||||||
|
frontends/web/temp_files/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ All notable changes to Stegasoo will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org).
|
and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
## [4.1.2] - 2026-01-05
|
## [4.1.3] - 2026-01-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Docker Deployment**: Production-ready containerization
|
- **Docker Deployment**: Production-ready containerization
|
||||||
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||||||
- Validation, generation, compression, edge cases
|
- Validation, generation, compression, edge cases
|
||||||
- 29 tests covering core library functionality
|
- 29 tests covering core library functionality
|
||||||
- **Release Validation**: `scripts/validate-release.sh` for pre-release checks
|
- **Release Validation**: `scripts/validate-release.sh` for pre-release checks
|
||||||
|
- **Custom SSL Documentation**: Guide for replacing certs, Let's Encrypt setup
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Pi MOTD shows CPU speed and temperature when overclocked
|
- Pi MOTD shows CPU speed and temperature when overclocked
|
||||||
@@ -36,9 +37,11 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||||||
- Setup script uses pyenv for Python 3.12 (Pi OS ships 3.13)
|
- Setup script uses pyenv for Python 3.12 (Pi OS ships 3.13)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- **SSL certificate generation**: Wizard and setup now generate certs when HTTPS enabled
|
||||||
- DCT decode reliability improvements
|
- DCT decode reliability improvements
|
||||||
- Fixed `gum --inline` flag compatibility (not supported in all versions)
|
- Fixed `gum --inline` flag compatibility (not supported in all versions)
|
||||||
- Wizard banner alignment and spacing issues
|
- Wizard banner alignment and spacing issues
|
||||||
|
- Better error handling in app.py for SSL failures
|
||||||
|
|
||||||
## [4.1.0] - 2026-01-04
|
## [4.1.0] - 2026-01-04
|
||||||
|
|
||||||
@@ -177,7 +180,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||||||
- CLI interface
|
- CLI interface
|
||||||
- Basic PIN authentication
|
- Basic PIN authentication
|
||||||
|
|
||||||
[4.1.2]: https://github.com/adlee-was-taken/stegasoo/compare/v4.1.0...v4.1.2
|
[4.1.3]: https://github.com/adlee-was-taken/stegasoo/compare/v4.1.0...v4.1.3
|
||||||
[4.1.0]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.2...v4.1.0
|
[4.1.0]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.2...v4.1.0
|
||||||
[4.0.2]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.1...v4.0.2
|
[4.0.2]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.1...v4.0.2
|
||||||
[4.0.1]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.0...v4.0.1
|
[4.0.1]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.0...v4.0.1
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ COPY data/ data/
|
|||||||
COPY frontends/web/ frontends/web/
|
COPY frontends/web/ frontends/web/
|
||||||
|
|
||||||
# Create upload directory and instance directories (for volumes)
|
# Create upload directory and instance directories (for volumes)
|
||||||
RUN mkdir -p /tmp/stego_uploads /app/frontends/web/instance /app/frontends/web/certs
|
# temp_files is for multi-worker temp file sharing
|
||||||
|
RUN mkdir -p /tmp/stego_uploads /app/frontends/web/instance /app/frontends/web/certs /app/frontends/web/temp_files
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads
|
RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads
|
||||||
|
|||||||
79
INSTALL.md
79
INSTALL.md
@@ -553,6 +553,85 @@ print(f'jpegio: {has_jpegio_support()}')
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Custom SSL Certificates
|
||||||
|
|
||||||
|
By default, Stegasoo generates a self-signed certificate for HTTPS. To use your own certificate (e.g., from Let's Encrypt or your organization's CA):
|
||||||
|
|
||||||
|
### Replace Self-Signed Certificates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop the service
|
||||||
|
sudo systemctl stop stegasoo
|
||||||
|
|
||||||
|
# Backup existing certs (optional)
|
||||||
|
mv /opt/stegasoo/frontends/web/certs /opt/stegasoo/frontends/web/certs.bak
|
||||||
|
|
||||||
|
# Create new certs directory
|
||||||
|
mkdir -p /opt/stegasoo/frontends/web/certs
|
||||||
|
|
||||||
|
# Copy your certificates (adjust paths as needed)
|
||||||
|
cp /path/to/your/certificate.crt /opt/stegasoo/frontends/web/certs/server.crt
|
||||||
|
cp /path/to/your/private.key /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
|
||||||
|
# Set permissions (key must be readable by service user)
|
||||||
|
chmod 600 /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
chown -R $(whoami):$(whoami) /opt/stegasoo/frontends/web/certs
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
sudo systemctl start stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate New Self-Signed Certificate
|
||||||
|
|
||||||
|
If your certificate expires or you need to regenerate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop service
|
||||||
|
sudo systemctl stop stegasoo
|
||||||
|
|
||||||
|
# Generate new cert with SANs
|
||||||
|
CERT_DIR="/opt/stegasoo/frontends/web/certs"
|
||||||
|
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
|
||||||
|
openssl req -x509 -newkey rsa:2048 \
|
||||||
|
-keyout "$CERT_DIR/server.key" \
|
||||||
|
-out "$CERT_DIR/server.crt" \
|
||||||
|
-days 365 -nodes \
|
||||||
|
-subj "/O=Stegasoo/CN=$HOSTNAME" \
|
||||||
|
-addext "subjectAltName=DNS:$HOSTNAME,DNS:$HOSTNAME.local,DNS:localhost,IP:$LOCAL_IP,IP:127.0.0.1"
|
||||||
|
|
||||||
|
chmod 600 "$CERT_DIR/server.key"
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Let's Encrypt with Certbot
|
||||||
|
|
||||||
|
For publicly accessible servers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install certbot
|
||||||
|
sudo apt install certbot
|
||||||
|
|
||||||
|
# Get certificate (standalone mode)
|
||||||
|
sudo certbot certonly --standalone -d yourdomain.com
|
||||||
|
|
||||||
|
# Copy to Stegasoo
|
||||||
|
sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /opt/stegasoo/frontends/web/certs/server.crt
|
||||||
|
sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
sudo chown $(whoami):$(whoami) /opt/stegasoo/frontends/web/certs/*
|
||||||
|
sudo chmod 600 /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
sudo systemctl restart stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Set up a cron job or systemd timer to copy renewed certificates and restart Stegasoo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
### Check Installation
|
### Check Installation
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
# Shared environment variables
|
# Shared environment variables
|
||||||
x-common-env: &common-env
|
x-common-env: &common-env
|
||||||
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
|
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
|
||||||
@@ -30,9 +28,9 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 768M
|
memory: 2048M
|
||||||
reservations:
|
reservations:
|
||||||
memory: 384M
|
memory: 1024M
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# REST API (FastAPI)
|
# REST API (FastAPI)
|
||||||
@@ -50,9 +48,9 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 768M
|
memory: 2048M
|
||||||
reservations:
|
reservations:
|
||||||
memory: 384M
|
memory: 1024M
|
||||||
|
|
||||||
# Named volumes for persistent data
|
# Named volumes for persistent data
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from ssl_utils import ensure_certs
|
from ssl_utils import ensure_certs
|
||||||
|
import temp_storage
|
||||||
|
|
||||||
os.environ["NUMPY_MADVISE_HUGEPAGE"] = "0"
|
os.environ["NUMPY_MADVISE_HUGEPAGE"] = "0"
|
||||||
os.environ["OMP_NUM_THREADS"] = "1"
|
os.environ["OMP_NUM_THREADS"] = "1"
|
||||||
@@ -257,9 +258,10 @@ def require_setup():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
|
# DEPRECATED: In-memory storage replaced by file-based temp_storage module
|
||||||
TEMP_FILES: dict[str, dict] = {}
|
# Kept for backwards compatibility during transition
|
||||||
THUMBNAIL_FILES: dict[str, bytes] = {}
|
TEMP_FILES: dict[str, dict] = {} # Not used - see temp_storage.py
|
||||||
|
THUMBNAIL_FILES: dict[str, bytes] = {} # Not used - see temp_storage.py
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -397,16 +399,7 @@ def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes
|
|||||||
|
|
||||||
def cleanup_temp_files():
|
def cleanup_temp_files():
|
||||||
"""Remove expired temporary files."""
|
"""Remove expired temporary files."""
|
||||||
now = time.time()
|
temp_storage.cleanup_expired(TEMP_FILE_EXPIRY)
|
||||||
expired = [
|
|
||||||
fid for fid, info in TEMP_FILES.items() if now - info["timestamp"] > TEMP_FILE_EXPIRY
|
|
||||||
]
|
|
||||||
|
|
||||||
for fid in expired:
|
|
||||||
TEMP_FILES.pop(fid, None)
|
|
||||||
# Also clean up corresponding thumbnail
|
|
||||||
thumb_id = f"{fid}_thumb"
|
|
||||||
THUMBNAIL_FILES.pop(thumb_id, None)
|
|
||||||
|
|
||||||
|
|
||||||
def allowed_image(filename: str) -> bool:
|
def allowed_image(filename: str) -> bool:
|
||||||
@@ -563,13 +556,11 @@ def generate():
|
|||||||
if not qr_too_large:
|
if not qr_too_large:
|
||||||
qr_token = secrets.token_urlsafe(16)
|
qr_token = secrets.token_urlsafe(16)
|
||||||
cleanup_temp_files()
|
cleanup_temp_files()
|
||||||
TEMP_FILES[qr_token] = {
|
temp_storage.save_temp_file(qr_token, creds.rsa_key_pem.encode(), {
|
||||||
"data": creds.rsa_key_pem.encode(),
|
|
||||||
"filename": "rsa_key.pem",
|
"filename": "rsa_key.pem",
|
||||||
"timestamp": time.time(),
|
|
||||||
"type": "rsa_key",
|
"type": "rsa_key",
|
||||||
"compress": qr_needs_compression,
|
"compress": qr_needs_compression,
|
||||||
}
|
})
|
||||||
|
|
||||||
# v3.2.0: Single passphrase instead of daily phrases
|
# v3.2.0: Single passphrase instead of daily phrases
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -606,10 +597,10 @@ def generate_qr(token):
|
|||||||
if not HAS_QRCODE:
|
if not HAS_QRCODE:
|
||||||
return "QR code support not available", 501
|
return "QR code support not available", 501
|
||||||
|
|
||||||
if token not in TEMP_FILES:
|
file_info = temp_storage.get_temp_file(token)
|
||||||
|
if not file_info:
|
||||||
return "Token expired or invalid", 404
|
return "Token expired or invalid", 404
|
||||||
|
|
||||||
file_info = TEMP_FILES[token]
|
|
||||||
if file_info.get("type") != "rsa_key":
|
if file_info.get("type") != "rsa_key":
|
||||||
return "Invalid token type", 400
|
return "Invalid token type", 400
|
||||||
|
|
||||||
@@ -630,10 +621,10 @@ def generate_qr_download(token):
|
|||||||
if not HAS_QRCODE:
|
if not HAS_QRCODE:
|
||||||
return "QR code support not available", 501
|
return "QR code support not available", 501
|
||||||
|
|
||||||
if token not in TEMP_FILES:
|
file_info = temp_storage.get_temp_file(token)
|
||||||
|
if not file_info:
|
||||||
return "Token expired or invalid", 404
|
return "Token expired or invalid", 404
|
||||||
|
|
||||||
file_info = TEMP_FILES[token]
|
|
||||||
if file_info.get("type") != "rsa_key":
|
if file_info.get("type") != "rsa_key":
|
||||||
return "Invalid token type", 400
|
return "Invalid token type", 400
|
||||||
|
|
||||||
@@ -933,17 +924,15 @@ def _run_encode_job(job_id: str, encode_params: dict) -> None:
|
|||||||
|
|
||||||
# Store result
|
# Store result
|
||||||
file_id = secrets.token_urlsafe(16)
|
file_id = secrets.token_urlsafe(16)
|
||||||
TEMP_FILES[file_id] = {
|
temp_storage.save_temp_file(file_id, encode_result.stego_data, {
|
||||||
"data": encode_result.stego_data,
|
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"timestamp": time.time(),
|
|
||||||
"embed_mode": embed_mode,
|
"embed_mode": embed_mode,
|
||||||
"output_format": dct_output_format if embed_mode == "dct" else "png",
|
"output_format": dct_output_format if embed_mode == "dct" else "png",
|
||||||
"color_mode": dct_color_mode if embed_mode == "dct" else None,
|
"color_mode": dct_color_mode if embed_mode == "dct" else None,
|
||||||
"mime_type": output_mime,
|
"mime_type": output_mime,
|
||||||
"channel_mode": encode_result.channel_mode,
|
"channel_mode": encode_result.channel_mode,
|
||||||
"channel_fingerprint": encode_result.channel_fingerprint,
|
"channel_fingerprint": encode_result.channel_fingerprint,
|
||||||
}
|
})
|
||||||
|
|
||||||
_store_job(
|
_store_job(
|
||||||
job_id,
|
job_id,
|
||||||
@@ -1212,10 +1201,8 @@ def encode_page():
|
|||||||
# Store temporarily
|
# Store temporarily
|
||||||
file_id = secrets.token_urlsafe(16)
|
file_id = secrets.token_urlsafe(16)
|
||||||
cleanup_temp_files()
|
cleanup_temp_files()
|
||||||
TEMP_FILES[file_id] = {
|
temp_storage.save_temp_file(file_id, encode_result.stego_data, {
|
||||||
"data": encode_result.stego_data,
|
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"timestamp": time.time(),
|
|
||||||
"embed_mode": embed_mode,
|
"embed_mode": embed_mode,
|
||||||
"output_format": dct_output_format if embed_mode == "dct" else "png",
|
"output_format": dct_output_format if embed_mode == "dct" else "png",
|
||||||
"color_mode": dct_color_mode if embed_mode == "dct" else None,
|
"color_mode": dct_color_mode if embed_mode == "dct" else None,
|
||||||
@@ -1223,7 +1210,7 @@ def encode_page():
|
|||||||
# Channel info (v4.0.0)
|
# Channel info (v4.0.0)
|
||||||
"channel_mode": encode_result.channel_mode,
|
"channel_mode": encode_result.channel_mode,
|
||||||
"channel_fingerprint": encode_result.channel_fingerprint,
|
"channel_fingerprint": encode_result.channel_fingerprint,
|
||||||
}
|
})
|
||||||
|
|
||||||
return redirect(url_for("encode_result", file_id=file_id))
|
return redirect(url_for("encode_result", file_id=file_id))
|
||||||
|
|
||||||
@@ -1290,19 +1277,18 @@ def encode_progress(job_id):
|
|||||||
@app.route("/encode/result/<file_id>")
|
@app.route("/encode/result/<file_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def encode_result(file_id):
|
def encode_result(file_id):
|
||||||
if file_id not in TEMP_FILES:
|
file_info = temp_storage.get_temp_file(file_id)
|
||||||
|
if not file_info:
|
||||||
flash("File expired or not found. Please encode again.", "error")
|
flash("File expired or not found. Please encode again.", "error")
|
||||||
return redirect(url_for("encode_page"))
|
return redirect(url_for("encode_page"))
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
|
|
||||||
# Generate thumbnail
|
# Generate thumbnail
|
||||||
thumbnail_data = generate_thumbnail(file_info["data"])
|
thumbnail_data = generate_thumbnail(file_info["data"])
|
||||||
thumbnail_id = None
|
thumbnail_id = None
|
||||||
|
|
||||||
if thumbnail_data:
|
if thumbnail_data:
|
||||||
thumbnail_id = f"{file_id}_thumb"
|
thumbnail_id = f"{file_id}_thumb"
|
||||||
THUMBNAIL_FILES[thumbnail_id] = thumbnail_data
|
temp_storage.save_thumbnail(thumbnail_id, thumbnail_data)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"encode_result.html",
|
"encode_result.html",
|
||||||
@@ -1322,22 +1308,23 @@ def encode_result(file_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def encode_thumbnail(thumb_id):
|
def encode_thumbnail(thumb_id):
|
||||||
"""Serve thumbnail image."""
|
"""Serve thumbnail image."""
|
||||||
if thumb_id not in THUMBNAIL_FILES:
|
thumb_data = temp_storage.get_thumbnail(thumb_id)
|
||||||
|
if not thumb_data:
|
||||||
return "Thumbnail not found", 404
|
return "Thumbnail not found", 404
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
io.BytesIO(THUMBNAIL_FILES[thumb_id]), mimetype="image/jpeg", as_attachment=False
|
io.BytesIO(thumb_data), mimetype="image/jpeg", as_attachment=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/encode/download/<file_id>")
|
@app.route("/encode/download/<file_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def encode_download(file_id):
|
def encode_download(file_id):
|
||||||
if file_id not in TEMP_FILES:
|
file_info = temp_storage.get_temp_file(file_id)
|
||||||
|
if not file_info:
|
||||||
flash("File expired or not found.", "error")
|
flash("File expired or not found.", "error")
|
||||||
return redirect(url_for("encode_page"))
|
return redirect(url_for("encode_page"))
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
mime_type = file_info.get("mime_type", "image/png")
|
mime_type = file_info.get("mime_type", "image/png")
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
@@ -1352,10 +1339,10 @@ def encode_download(file_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def encode_file_route(file_id):
|
def encode_file_route(file_id):
|
||||||
"""Serve file for Web Share API."""
|
"""Serve file for Web Share API."""
|
||||||
if file_id not in TEMP_FILES:
|
file_info = temp_storage.get_temp_file(file_id)
|
||||||
|
if not file_info:
|
||||||
return "Not found", 404
|
return "Not found", 404
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
mime_type = file_info.get("mime_type", "image/png")
|
mime_type = file_info.get("mime_type", "image/png")
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
@@ -1370,11 +1357,11 @@ def encode_file_route(file_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def encode_cleanup(file_id):
|
def encode_cleanup(file_id):
|
||||||
"""Manually cleanup a file after sharing."""
|
"""Manually cleanup a file after sharing."""
|
||||||
TEMP_FILES.pop(file_id, None)
|
temp_storage.delete_temp_file(file_id)
|
||||||
|
|
||||||
# Also cleanup thumbnail if exists
|
# Also cleanup thumbnail if exists
|
||||||
thumb_id = f"{file_id}_thumb"
|
thumb_id = f"{file_id}_thumb"
|
||||||
THUMBNAIL_FILES.pop(thumb_id, None)
|
temp_storage.delete_thumbnail(thumb_id)
|
||||||
|
|
||||||
return jsonify({"status": "ok"})
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
@@ -1497,12 +1484,10 @@ def decode_page():
|
|||||||
cleanup_temp_files()
|
cleanup_temp_files()
|
||||||
|
|
||||||
filename = decode_result.filename or "decoded_file"
|
filename = decode_result.filename or "decoded_file"
|
||||||
TEMP_FILES[file_id] = {
|
temp_storage.save_temp_file(file_id, decode_result.file_data, {
|
||||||
"data": decode_result.file_data,
|
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"mime_type": decode_result.mime_type,
|
"mime_type": decode_result.mime_type,
|
||||||
"timestamp": time.time(),
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"decode.html",
|
"decode.html",
|
||||||
@@ -1559,11 +1544,11 @@ def decode_page():
|
|||||||
@login_required
|
@login_required
|
||||||
def decode_download(file_id):
|
def decode_download(file_id):
|
||||||
"""Download decoded file."""
|
"""Download decoded file."""
|
||||||
if file_id not in TEMP_FILES:
|
file_info = temp_storage.get_temp_file(file_id)
|
||||||
|
if not file_info:
|
||||||
flash("File expired or not found.", "error")
|
flash("File expired or not found.", "error")
|
||||||
return redirect(url_for("decode_page"))
|
return redirect(url_for("decode_page"))
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
mime_type = file_info.get("mime_type", "application/octet-stream")
|
mime_type = file_info.get("mime_type", "application/octet-stream")
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
@@ -2320,13 +2305,31 @@ def admin_user_password_reset():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
base_dir = Path(__file__).parent
|
base_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
# Clean up any leftover temp files from previous runs
|
||||||
|
temp_storage.init(base_dir / "temp_files")
|
||||||
|
cleaned = temp_storage.cleanup_all()
|
||||||
|
if cleaned > 0:
|
||||||
|
print(f"Cleaned up {cleaned} leftover temp files from previous run")
|
||||||
|
|
||||||
# HTTPS configuration
|
# HTTPS configuration
|
||||||
ssl_context = None
|
ssl_context = None
|
||||||
if app.config.get("HTTPS_ENABLED", False):
|
if app.config.get("HTTPS_ENABLED", False):
|
||||||
hostname = os.environ.get("STEGASOO_HOSTNAME", "localhost")
|
hostname = os.environ.get("STEGASOO_HOSTNAME", "localhost")
|
||||||
cert_path, key_path = ensure_certs(base_dir, hostname)
|
try:
|
||||||
ssl_context = (str(cert_path), str(key_path))
|
cert_path, key_path = ensure_certs(base_dir, hostname)
|
||||||
print(f"HTTPS enabled with self-signed certificate for {hostname}")
|
if cert_path.exists() and key_path.exists():
|
||||||
|
ssl_context = (str(cert_path), str(key_path))
|
||||||
|
print(f"HTTPS enabled with self-signed certificate for {hostname}")
|
||||||
|
else:
|
||||||
|
print("ERROR: SSL certificates not found after generation attempt")
|
||||||
|
print(f" Expected: {cert_path}, {key_path}")
|
||||||
|
print(" Falling back to HTTP (INSECURE)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to generate SSL certificates: {e}")
|
||||||
|
print(" Falling back to HTTP (INSECURE)")
|
||||||
|
print(" To fix: mkdir -p certs && openssl req -x509 -newkey rsa:2048 \\")
|
||||||
|
print(" -keyout certs/server.key -out certs/server.crt -days 365 -nodes \\")
|
||||||
|
print(" -subj '/CN=localhost'")
|
||||||
|
|
||||||
# Auth status
|
# Auth status
|
||||||
if app.config.get("AUTH_ENABLED", True):
|
if app.config.get("AUTH_ENABLED", True):
|
||||||
|
|||||||
214
frontends/web/temp_storage.py
Normal file
214
frontends/web/temp_storage.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
"""
|
||||||
|
File-based Temporary Storage
|
||||||
|
|
||||||
|
Stores temp files on disk instead of in-memory dict.
|
||||||
|
This allows multiple Gunicorn workers to share temp files
|
||||||
|
and survives service restarts within the expiry window.
|
||||||
|
|
||||||
|
Files are stored in a temp directory with:
|
||||||
|
- {file_id}.data - The actual file data
|
||||||
|
- {file_id}.json - Metadata (filename, timestamp, mime_type, etc.)
|
||||||
|
|
||||||
|
IMPORTANT: This module ONLY manages files in the temp_files/ directory.
|
||||||
|
It does NOT touch instance/ (auth database) or any other directories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
# Default temp directory (can be overridden)
|
||||||
|
DEFAULT_TEMP_DIR = Path(__file__).parent / "temp_files"
|
||||||
|
|
||||||
|
# Lock for thread-safe operations
|
||||||
|
_lock = Lock()
|
||||||
|
|
||||||
|
# Module-level temp directory (set on init)
|
||||||
|
_temp_dir: Path = DEFAULT_TEMP_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def init(temp_dir: Path | str | None = None):
|
||||||
|
"""Initialize temp storage with optional custom directory."""
|
||||||
|
global _temp_dir
|
||||||
|
_temp_dir = Path(temp_dir) if temp_dir else DEFAULT_TEMP_DIR
|
||||||
|
_temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _data_path(file_id: str) -> Path:
|
||||||
|
"""Get path for file data."""
|
||||||
|
return _temp_dir / f"{file_id}.data"
|
||||||
|
|
||||||
|
|
||||||
|
def _meta_path(file_id: str) -> Path:
|
||||||
|
"""Get path for file metadata."""
|
||||||
|
return _temp_dir / f"{file_id}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _thumb_path(thumb_id: str) -> Path:
|
||||||
|
"""Get path for thumbnail data."""
|
||||||
|
return _temp_dir / f"{thumb_id}.thumb"
|
||||||
|
|
||||||
|
|
||||||
|
def save_temp_file(file_id: str, data: bytes, metadata: dict) -> None:
|
||||||
|
"""
|
||||||
|
Save a temp file with its metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_id: Unique identifier for the file
|
||||||
|
data: File contents as bytes
|
||||||
|
metadata: Dict with filename, mime_type, timestamp, etc.
|
||||||
|
"""
|
||||||
|
init() # Ensure directory exists
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
# Add timestamp if not present
|
||||||
|
if "timestamp" not in metadata:
|
||||||
|
metadata["timestamp"] = time.time()
|
||||||
|
|
||||||
|
# Write data file
|
||||||
|
_data_path(file_id).write_bytes(data)
|
||||||
|
|
||||||
|
# Write metadata
|
||||||
|
_meta_path(file_id).write_text(json.dumps(metadata))
|
||||||
|
|
||||||
|
|
||||||
|
def get_temp_file(file_id: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Get a temp file and its metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with 'data' (bytes) and all metadata fields, or None if not found.
|
||||||
|
"""
|
||||||
|
init()
|
||||||
|
|
||||||
|
data_file = _data_path(file_id)
|
||||||
|
meta_file = _meta_path(file_id)
|
||||||
|
|
||||||
|
if not data_file.exists() or not meta_file.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = data_file.read_bytes()
|
||||||
|
metadata = json.loads(meta_file.read_text())
|
||||||
|
return {"data": data, **metadata}
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_temp_file(file_id: str) -> bool:
|
||||||
|
"""Check if a temp file exists."""
|
||||||
|
init()
|
||||||
|
return _data_path(file_id).exists() and _meta_path(file_id).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_temp_file(file_id: str) -> None:
|
||||||
|
"""Delete a temp file and its metadata."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_data_path(file_id).unlink(missing_ok=True)
|
||||||
|
_meta_path(file_id).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def save_thumbnail(thumb_id: str, data: bytes) -> None:
|
||||||
|
"""Save a thumbnail."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_thumb_path(thumb_id).write_bytes(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_thumbnail(thumb_id: str) -> bytes | None:
|
||||||
|
"""Get thumbnail data."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
thumb_file = _thumb_path(thumb_id)
|
||||||
|
if not thumb_file.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return thumb_file.read_bytes()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_thumbnail(thumb_id: str) -> None:
|
||||||
|
"""Delete a thumbnail."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_thumb_path(thumb_id).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_expired(max_age_seconds: float) -> int:
|
||||||
|
"""
|
||||||
|
Delete expired temp files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_age_seconds: Maximum age in seconds before expiry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of files deleted
|
||||||
|
"""
|
||||||
|
init()
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
deleted = 0
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
# Find all metadata files
|
||||||
|
for meta_file in _temp_dir.glob("*.json"):
|
||||||
|
try:
|
||||||
|
metadata = json.loads(meta_file.read_text())
|
||||||
|
timestamp = metadata.get("timestamp", 0)
|
||||||
|
|
||||||
|
if now - timestamp > max_age_seconds:
|
||||||
|
file_id = meta_file.stem
|
||||||
|
_data_path(file_id).unlink(missing_ok=True)
|
||||||
|
meta_file.unlink(missing_ok=True)
|
||||||
|
# Also delete thumbnail if exists
|
||||||
|
_thumb_path(f"{file_id}_thumb").unlink(missing_ok=True)
|
||||||
|
deleted += 1
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
# Remove corrupted files
|
||||||
|
meta_file.unlink(missing_ok=True)
|
||||||
|
deleted += 1
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_all() -> int:
|
||||||
|
"""
|
||||||
|
Delete all temp files. Call on service start/stop.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of files deleted
|
||||||
|
"""
|
||||||
|
init()
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
for f in _temp_dir.iterdir():
|
||||||
|
if f.is_file():
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
deleted += 1
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats() -> dict:
|
||||||
|
"""Get temp storage statistics."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
files = list(_temp_dir.glob("*.data"))
|
||||||
|
total_size = sum(f.stat().st_size for f in files if f.exists())
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file_count": len(files),
|
||||||
|
"total_size_bytes": total_size,
|
||||||
|
"temp_dir": str(_temp_dir),
|
||||||
|
}
|
||||||
@@ -518,5 +518,8 @@ advancedOptionsDec?.addEventListener('show.bs.collapse', () => {
|
|||||||
advancedOptionsDec?.addEventListener('hide.bs.collapse', () => {
|
advancedOptionsDec?.addEventListener('hide.bs.collapse', () => {
|
||||||
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-up', 'bi-chevron-down');
|
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-up', 'bi-chevron-down');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Loading state for decode button
|
||||||
|
Stegasoo.initFormLoading('decodeForm', 'decodeBtn', 'Decoding...');
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -279,6 +279,32 @@ EOF
|
|||||||
"
|
"
|
||||||
gum style --foreground 82 "✓ Service configured"
|
gum style --foreground 82 "✓ Service configured"
|
||||||
|
|
||||||
|
# Generate SSL certificates if HTTPS enabled
|
||||||
|
if [ "$ENABLE_HTTPS" = "true" ]; then
|
||||||
|
gum spin --spinner dot --title "Generating SSL certificates..." -- bash -c "
|
||||||
|
CERT_DIR='$INSTALL_DIR/frontends/web/certs'
|
||||||
|
mkdir -p \"\$CERT_DIR\"
|
||||||
|
|
||||||
|
# Get local IP for SAN
|
||||||
|
LOCAL_IP=\$(hostname -I | awk '{print \$1}')
|
||||||
|
HOSTNAME=\$(hostname)
|
||||||
|
|
||||||
|
# Generate cert with SANs for IP, hostname, and localhost
|
||||||
|
openssl req -x509 -newkey rsa:2048 \
|
||||||
|
-keyout \"\$CERT_DIR/server.key\" \
|
||||||
|
-out \"\$CERT_DIR/server.crt\" \
|
||||||
|
-days 365 -nodes \
|
||||||
|
-subj \"/O=Stegasoo/CN=\$HOSTNAME\" \
|
||||||
|
-addext \"subjectAltName=DNS:\$HOSTNAME,DNS:\$HOSTNAME.local,DNS:localhost,IP:\$LOCAL_IP,IP:127.0.0.1\" \
|
||||||
|
2>/dev/null
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
chmod 600 \"\$CERT_DIR/server.key\"
|
||||||
|
chown -R $STEGASOO_USER:\$(id -gn $STEGASOO_USER) \"\$CERT_DIR\"
|
||||||
|
"
|
||||||
|
gum style --foreground 82 "✓ SSL certificates generated"
|
||||||
|
fi
|
||||||
|
|
||||||
# Setup port 443 if requested
|
# Setup port 443 if requested
|
||||||
if [ "$USE_PORT_443" = "true" ]; then
|
if [ "$USE_PORT_443" = "true" ]; then
|
||||||
gum spin --spinner dot --title "Setting up port 443 redirect..." -- bash -c "
|
gum spin --spinner dot --title "Setting up port 443 redirect..." -- bash -c "
|
||||||
|
|||||||
232
rpi/flash-pi.sh
Executable file
232
rpi/flash-pi.sh
Executable file
@@ -0,0 +1,232 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Flash Raspberry Pi image with headless config (Trixie/Bookworm compatible)
|
||||||
|
# Usage: ./flash-pi.sh <image.img.xz> <device>
|
||||||
|
# Reads settings from config.json in same directory
|
||||||
|
#
|
||||||
|
# Uses the same firstrun.sh approach as rpi-imager for compatibility
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_FILE="$SCRIPT_DIR/config.json"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Load config
|
||||||
|
# ============================================================================
|
||||||
|
if [ ! -f "$CONFIG_FILE" ]; then
|
||||||
|
echo "Error: config.json not found at $CONFIG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PI_USER=$(jq -r '.username' "$CONFIG_FILE")
|
||||||
|
PI_PASS=$(jq -r '.password' "$CONFIG_FILE")
|
||||||
|
WIFI_SSID=$(jq -r '.wifiSSID' "$CONFIG_FILE")
|
||||||
|
WIFI_PASS=$(jq -r '.wifiPassword' "$CONFIG_FILE")
|
||||||
|
WIFI_COUNTRY=$(jq -r '.wifiCountry // "US"' "$CONFIG_FILE")
|
||||||
|
PI_HOSTNAME=$(jq -r '.hostname' "$CONFIG_FILE")
|
||||||
|
PI_TIMEZONE=$(jq -r '.timezone // "America/New_York"' "$CONFIG_FILE")
|
||||||
|
PI_KEYMAP=$(jq -r '.keyboardLayout // "us"' "$CONFIG_FILE")
|
||||||
|
|
||||||
|
echo "Loaded config from $CONFIG_FILE"
|
||||||
|
echo " Hostname: $PI_HOSTNAME"
|
||||||
|
echo " User: $PI_USER"
|
||||||
|
echo " WiFi: $WIFI_SSID"
|
||||||
|
echo " Timezone: $PI_TIMEZONE"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Validate args
|
||||||
|
# ============================================================================
|
||||||
|
if [ $# -ne 2 ]; then
|
||||||
|
echo "Usage: $0 <image.img.xz> <device>"
|
||||||
|
echo "Example: $0 2025-12-04-raspios-trixie-arm64-lite.img.xz /dev/sdb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGE="$1"
|
||||||
|
DEVICE="$2"
|
||||||
|
|
||||||
|
if [ ! -f "$IMAGE" ]; then
|
||||||
|
echo "Error: Image file not found: $IMAGE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -b "$DEVICE" ]; then
|
||||||
|
echo "Error: Device not found: $DEVICE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Safety check
|
||||||
|
echo "WARNING: This will ERASE all data on $DEVICE"
|
||||||
|
echo "Device info:"
|
||||||
|
lsblk "$DEVICE"
|
||||||
|
echo
|
||||||
|
read -p "Type 'yes' to continue: " confirm
|
||||||
|
if [ "$confirm" != "yes" ]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask about wiping
|
||||||
|
echo
|
||||||
|
read -p "Wipe partition table first? (recommended if having issues) [y/N] " wipe_confirm
|
||||||
|
if [[ "$wipe_confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Wiping partition table..."
|
||||||
|
sudo wipefs -a "$DEVICE"
|
||||||
|
sudo dd if=/dev/zero of="$DEVICE" bs=1M count=10 status=none
|
||||||
|
sync
|
||||||
|
echo " Wiped clean"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Flash image
|
||||||
|
# ============================================================================
|
||||||
|
echo "Flashing $IMAGE to $DEVICE..."
|
||||||
|
if [[ "$IMAGE" == *.xz ]]; then
|
||||||
|
xzcat "$IMAGE" | sudo dd of="$DEVICE" bs=4M status=progress conv=fsync
|
||||||
|
elif [[ "$IMAGE" == *.zst ]]; then
|
||||||
|
zstdcat "$IMAGE" | sudo dd of="$DEVICE" bs=4M status=progress conv=fsync
|
||||||
|
else
|
||||||
|
sudo dd if="$IMAGE" of="$DEVICE" bs=4M status=progress conv=fsync
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Syncing..."
|
||||||
|
sync
|
||||||
|
|
||||||
|
# Wait for partitions
|
||||||
|
sleep 2
|
||||||
|
sudo partprobe "$DEVICE" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Find partitions
|
||||||
|
# ============================================================================
|
||||||
|
if [ -b "${DEVICE}1" ]; then
|
||||||
|
BOOT_PART="${DEVICE}1"
|
||||||
|
elif [ -b "${DEVICE}p1" ]; then
|
||||||
|
BOOT_PART="${DEVICE}p1"
|
||||||
|
else
|
||||||
|
echo "Error: Could not find boot partition"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MOUNT_DIR=$(mktemp -d)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Configure boot partition with firstrun.sh (rpi-imager method)
|
||||||
|
# ============================================================================
|
||||||
|
echo "Mounting boot partition..."
|
||||||
|
sudo mount "$BOOT_PART" "$MOUNT_DIR"
|
||||||
|
|
||||||
|
# Enable SSH
|
||||||
|
echo "Enabling SSH..."
|
||||||
|
sudo touch "$MOUNT_DIR/ssh"
|
||||||
|
|
||||||
|
# Generate password hash
|
||||||
|
PASS_HASH=$(echo "$PI_PASS" | openssl passwd -6 -stdin)
|
||||||
|
|
||||||
|
# Create firstrun.sh - this is exactly what rpi-imager generates
|
||||||
|
echo "Creating firstrun.sh..."
|
||||||
|
sudo tee "$MOUNT_DIR/firstrun.sh" > /dev/null << 'EOFSCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
set +e
|
||||||
|
|
||||||
|
CURRENT_HOSTNAME=$(cat /etc/hostname | tr -d " \t\n\r")
|
||||||
|
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
|
||||||
|
/usr/lib/raspberrypi-sys-mods/imager_custom set_hostname PLACEHOLDER_HOSTNAME
|
||||||
|
else
|
||||||
|
echo PLACEHOLDER_HOSTNAME >/etc/hostname
|
||||||
|
sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\tPLACEHOLDER_HOSTNAME/g" /etc/hosts
|
||||||
|
fi
|
||||||
|
|
||||||
|
FIRSTUSER=$(getent passwd 1000 | cut -d: -f1)
|
||||||
|
FIRSTUSERHOME=$(getent passwd 1000 | cut -d: -f6)
|
||||||
|
|
||||||
|
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
|
||||||
|
/usr/lib/raspberrypi-sys-mods/imager_custom enable_ssh
|
||||||
|
else
|
||||||
|
systemctl enable ssh
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /usr/lib/userconf-pi/userconf ]; then
|
||||||
|
/usr/lib/userconf-pi/userconf 'PLACEHOLDER_USER' 'PLACEHOLDER_HASH'
|
||||||
|
else
|
||||||
|
echo "$FIRSTUSER:"'PLACEHOLDER_HASH' | chpasswd -e
|
||||||
|
if [ "$FIRSTUSER" != "PLACEHOLDER_USER" ]; then
|
||||||
|
usermod -l "PLACEHOLDER_USER" "$FIRSTUSER"
|
||||||
|
usermod -m -d "/home/PLACEHOLDER_USER" "PLACEHOLDER_USER"
|
||||||
|
groupmod -n "PLACEHOLDER_USER" "$FIRSTUSER"
|
||||||
|
if grep -q "^autologin-user=" /etc/lightdm/lightdm.conf 2>/dev/null; then
|
||||||
|
sed -i "s/^autologin-user=.*/autologin-user=PLACEHOLDER_USER/" /etc/lightdm/lightdm.conf
|
||||||
|
fi
|
||||||
|
if [ -f /etc/systemd/system/getty@tty1.service.d/autologin.conf ]; then
|
||||||
|
sed -i "s/$FIRSTUSER/PLACEHOLDER_USER/" /etc/systemd/system/getty@tty1.service.d/autologin.conf
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
|
||||||
|
/usr/lib/raspberrypi-sys-mods/imager_custom set_keymap 'PLACEHOLDER_KEYMAP'
|
||||||
|
/usr/lib/raspberrypi-sys-mods/imager_custom set_timezone 'PLACEHOLDER_TIMEZONE'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
|
||||||
|
/usr/lib/raspberrypi-sys-mods/imager_custom set_wlan 'PLACEHOLDER_SSID' 'PLACEHOLDER_WIFIPASS' 'PLACEHOLDER_COUNTRY'
|
||||||
|
else
|
||||||
|
cat >/etc/wpa_supplicant/wpa_supplicant.conf <<'WPAEOF'
|
||||||
|
country=PLACEHOLDER_COUNTRY
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
ap_scan=1
|
||||||
|
update_config=1
|
||||||
|
network={
|
||||||
|
ssid="PLACEHOLDER_SSID"
|
||||||
|
psk="PLACEHOLDER_WIFIPASS"
|
||||||
|
}
|
||||||
|
WPAEOF
|
||||||
|
chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf
|
||||||
|
rfkill unblock wifi
|
||||||
|
for filename in /var/lib/systemd/rfkill/*:wlan ; do
|
||||||
|
echo 0 > "$filename"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /boot/firstrun.sh
|
||||||
|
rm -f /boot/firmware/firstrun.sh
|
||||||
|
sed -i 's| systemd.run.*||g' /boot/cmdline.txt 2>/dev/null
|
||||||
|
sed -i 's| systemd.run.*||g' /boot/firmware/cmdline.txt 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
EOFSCRIPT
|
||||||
|
|
||||||
|
# Replace placeholders with actual values
|
||||||
|
sudo sed -i "s/PLACEHOLDER_HOSTNAME/$PI_HOSTNAME/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sudo sed -i "s/PLACEHOLDER_USER/$PI_USER/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sudo sed -i "s|PLACEHOLDER_HASH|$PASS_HASH|g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sudo sed -i "s/PLACEHOLDER_KEYMAP/$PI_KEYMAP/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sudo sed -i "s|PLACEHOLDER_TIMEZONE|$PI_TIMEZONE|g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sudo sed -i "s/PLACEHOLDER_SSID/$WIFI_SSID/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sudo sed -i "s/PLACEHOLDER_WIFIPASS/$WIFI_PASS/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sudo sed -i "s/PLACEHOLDER_COUNTRY/$WIFI_COUNTRY/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
|
||||||
|
sudo chmod +x "$MOUNT_DIR/firstrun.sh"
|
||||||
|
|
||||||
|
# Update cmdline.txt to run firstrun.sh on boot
|
||||||
|
echo "Updating cmdline.txt..."
|
||||||
|
CMDLINE="$MOUNT_DIR/cmdline.txt"
|
||||||
|
if [ -f "$CMDLINE" ]; then
|
||||||
|
# Read current cmdline, strip any existing systemd.run, append new one
|
||||||
|
CURRENT=$(cat "$CMDLINE" | tr -d '\n' | sed 's| systemd.run.*||g')
|
||||||
|
echo "$CURRENT systemd.run=/boot/firmware/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target" | sudo tee "$CMDLINE" > /dev/null
|
||||||
|
echo " cmdline.txt updated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo umount "$MOUNT_DIR"
|
||||||
|
rmdir "$MOUNT_DIR"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done! SD card is ready."
|
||||||
|
echo " Hostname: $PI_HOSTNAME"
|
||||||
|
echo " User: $PI_USER"
|
||||||
|
echo " SSH: enabled"
|
||||||
|
echo " WiFi: $WIFI_SSID"
|
||||||
|
echo
|
||||||
|
echo "Insert into Pi and boot. Find it with: ping $PI_HOSTNAME.local"
|
||||||
25
rpi/setup.sh
25
rpi/setup.sh
@@ -465,6 +465,31 @@ RestartSec=5
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Generate SSL certificates if HTTPS enabled
|
||||||
|
if [ "$ENABLE_HTTPS" = "true" ]; then
|
||||||
|
echo " Generating SSL certificates..."
|
||||||
|
CERT_DIR="$INSTALL_DIR/frontends/web/certs"
|
||||||
|
mkdir -p "$CERT_DIR"
|
||||||
|
|
||||||
|
# Get local IP for SAN
|
||||||
|
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
PI_HOSTNAME=$(hostname)
|
||||||
|
|
||||||
|
# Generate cert with SANs for IP, hostname, and localhost
|
||||||
|
openssl req -x509 -newkey rsa:2048 \
|
||||||
|
-keyout "$CERT_DIR/server.key" \
|
||||||
|
-out "$CERT_DIR/server.crt" \
|
||||||
|
-days 365 -nodes \
|
||||||
|
-subj "/O=Stegasoo/CN=$PI_HOSTNAME" \
|
||||||
|
-addext "subjectAltName=DNS:$PI_HOSTNAME,DNS:$PI_HOSTNAME.local,DNS:localhost,IP:$LOCAL_IP,IP:127.0.0.1" \
|
||||||
|
2>/dev/null
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
chmod 600 "$CERT_DIR/server.key"
|
||||||
|
chown -R "$USER:$USER" "$CERT_DIR"
|
||||||
|
echo -e " ${GREEN}✓${NC} SSL certificates generated"
|
||||||
|
fi
|
||||||
|
|
||||||
# Setup port 443 redirect if requested
|
# Setup port 443 redirect if requested
|
||||||
if [ "$USE_PORT_443" = "true" ]; then
|
if [ "$USE_PORT_443" = "true" ]; then
|
||||||
echo " Setting up port 443 redirect..."
|
echo " Setting up port 443 redirect..."
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Changes in v4.0.0:
|
|||||||
- encode() and decode() now accept channel_key parameter
|
- encode() and decode() now accept channel_key parameter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "4.1.2"
|
__version__ = "4.1.3"
|
||||||
|
|
||||||
# Core functionality
|
# Core functionality
|
||||||
# Channel key management (v4.0.0)
|
# Channel key management (v4.0.0)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from pathlib import Path
|
|||||||
# VERSION
|
# VERSION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
__version__ = "4.1.2"
|
__version__ = "4.1.3"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# FILE FORMAT
|
# FILE FORMAT
|
||||||
|
|||||||
Reference in New Issue
Block a user