Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65d9e6682 | ||
|
|
eeb44eae94 | ||
|
|
26d4b82c91 | ||
|
|
7efeaf02e8 | ||
|
|
925fb05cbd | ||
|
|
29a02265a1 | ||
|
|
d58f3c6fb6 | ||
|
|
cc46993d80 | ||
|
|
893a044eaa | ||
|
|
9f03b69408 | ||
|
|
cce2007c6e | ||
|
|
52f43d3a86 | ||
|
|
85a7092d55 | ||
|
|
4b37a81087 | ||
|
|
31941dc3f5 | ||
|
|
9a7e4ddce7 | ||
|
|
0424dd34d5 | ||
|
|
2127b916f3 | ||
|
|
f8e65890e5 | ||
|
|
5861ab0e1e | ||
|
|
5309a08aaf | ||
|
|
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
|
||||||
|
|||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -80,3 +80,16 @@ tests/
|
|||||||
*.img.xz
|
*.img.xz
|
||||||
*.img.zst
|
*.img.zst
|
||||||
pishrink.sh
|
pishrink.sh
|
||||||
|
*.img.zst.zip
|
||||||
|
|
||||||
|
# Temp file storage
|
||||||
|
frontends/web/temp_files/
|
||||||
|
rpi/config.json
|
||||||
|
|
||||||
|
# Pre-built Pi tarballs and images (release assets, too large for git)
|
||||||
|
rpi/stegasoo-pi-arm64.tar.zst
|
||||||
|
rpi/stegasoo-pi-arm64.tar.zst.zip
|
||||||
|
rpi/stegasoo-venv-pi-arm64.tar.zst
|
||||||
|
rpi/*.img
|
||||||
|
rpi/*.img.zst
|
||||||
|
rpi/*.img.zst.zip
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Thank you for your interest in contributing to Stegasoo! This document provides
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Python 3.10 or higher
|
- Python 3.10 - 3.12
|
||||||
- Git
|
- Git
|
||||||
- Docker (optional, for container testing)
|
- Docker (optional, for container testing)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
165
PLAN-4.1.4.md
Normal file
165
PLAN-4.1.4.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Stegasoo 4.1.4 Plan
|
||||||
|
|
||||||
|
## Build / Deploy
|
||||||
|
- [x] Pre-built Python 3.12 venv tarball for Pi (skip 20+ min compile) - see details below
|
||||||
|
- [x] Fixed partition sizing in flash script (16GB rootfs for faster imaging)
|
||||||
|
- [x] Rename `flash-pi.sh` → `flash-stock-img.sh` for clarity
|
||||||
|
- [x] pip-audit integration in release validation
|
||||||
|
|
||||||
|
### Pi venv Tarball Approach
|
||||||
|
1. Flash fresh Pi image, let it fully build (20+ min compile)
|
||||||
|
2. Once running and working, SSH in and create optimized tarball:
|
||||||
|
```bash
|
||||||
|
cd /opt/stegasoo
|
||||||
|
# Strip caches and tests (295MB → 208MB)
|
||||||
|
find venv/ -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null
|
||||||
|
find venv/ -type d -name 'tests' -exec rm -rf {} + 2>/dev/null
|
||||||
|
find venv/ -type d -name 'test' -exec rm -rf {} + 2>/dev/null
|
||||||
|
# Compress with zstd (208MB → 39MB)
|
||||||
|
tar -cf - venv/ | zstd -19 -T0 > /tmp/stegasoo-venv-pi-arm64.tar.zst
|
||||||
|
```
|
||||||
|
3. Pull tarball to host: `scp admin@pi:/tmp/stegasoo-venv-pi-arm64.tar.zst rpi/`
|
||||||
|
4. setup.sh auto-detects and extracts tarball if present in rpi/
|
||||||
|
5. Re-flash and test fresh build with pre-built venv (should be <2 min vs 20+)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- [x] QR channel key sharing (see detailed plan below)
|
||||||
|
- [ ] Role-based permissions: admin / mod / user
|
||||||
|
- [x] `stegasoo info` fastfetch-style command (version, service status, channel, CPU, temp, etc.)
|
||||||
|
- [ ] Better capacity estimates / pre-flight check before encode fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QR Channel Key Sharing - Implementation Plan
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
- ✅ **CLI**: `stegasoo channel qr` generates ASCII/PNG QR for server channel key
|
||||||
|
- ✅ **Web UI (about.html)**: Client-side QR generator exists - input key, generate/show QR, download PNG
|
||||||
|
- ✅ **Account page**: Shows saved channel keys with fingerprint, rename, delete
|
||||||
|
- ❌ No role restrictions on QR sharing
|
||||||
|
- ❌ No QR button for saved keys on account page
|
||||||
|
- ❌ No QR scanning to import keys
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
|
||||||
|
**UI Placement** (avoiding encode/decode page crowding):
|
||||||
|
- Keep QR generator in **about.html** (already exists, logical place for tools)
|
||||||
|
- Add QR button to **account.html** saved keys (small icon, doesn't crowd)
|
||||||
|
- Both should be admin-only
|
||||||
|
|
||||||
|
**Role Restriction** (per user request):
|
||||||
|
- QR sharing = admin only (hide generator + saved key QR buttons from non-admins)
|
||||||
|
- Prerequisite: Need role-based permissions feature first
|
||||||
|
- Interim option: Just hide from non-admin users using existing `is_admin` flag
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
|
||||||
|
#### Phase 1: Admin-only restriction (quick win)
|
||||||
|
1. **about.html**: Wrap QR generator section in `{% if is_admin %}` block
|
||||||
|
2. **Account route**: Pass `is_admin` to template (if not already)
|
||||||
|
3. **account.html**: Add small QR icon button to saved keys row (admin only)
|
||||||
|
- Opens modal with QR canvas (reuse qrcode.js pattern from about.html)
|
||||||
|
- Download PNG button in modal
|
||||||
|
|
||||||
|
#### Phase 2: QR Import (optional enhancement)
|
||||||
|
1. Add "Import via QR" button to account.html key-add section
|
||||||
|
2. Use device camera or file upload to scan QR
|
||||||
|
3. Decode and populate channel_key input field
|
||||||
|
4. Requires `pyzbar` on server OR client-side JS library like `jsQR`
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
```
|
||||||
|
frontends/web/app.py
|
||||||
|
- about() route: Add missing vars: is_admin, channel_configured,
|
||||||
|
channel_fingerprint, channel_source (BUG: currently not passed!)
|
||||||
|
- account() route: ✅ Already passes is_admin
|
||||||
|
|
||||||
|
frontends/web/templates/about.html
|
||||||
|
- Wrap channel key QR section in {% if is_admin %}
|
||||||
|
|
||||||
|
frontends/web/templates/account.html
|
||||||
|
- Add QR button to saved keys (admin only)
|
||||||
|
- Add QR modal (copy pattern from about.html)
|
||||||
|
- Include qrcode.min.js CDN script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bug Found During Research
|
||||||
|
The about.html template uses `channel_configured`, `channel_fingerprint`,
|
||||||
|
`channel_source` but the route doesn't pass them - always shows "public mode".
|
||||||
|
Fix this while implementing QR admin restriction.
|
||||||
|
|
||||||
|
### Exact Code Changes
|
||||||
|
|
||||||
|
**app.py - Fix about() route (around line 1564):**
|
||||||
|
```python
|
||||||
|
@app.route("/about")
|
||||||
|
def about():
|
||||||
|
from stegasoo.channel import get_channel_status
|
||||||
|
channel_status = get_channel_status()
|
||||||
|
|
||||||
|
# Check if user is admin (for QR sharing)
|
||||||
|
current_user = get_current_user()
|
||||||
|
is_admin = current_user.is_admin if current_user else False
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"about.html",
|
||||||
|
has_argon2=has_argon2(),
|
||||||
|
has_qrcode_read=HAS_QRCODE_READ,
|
||||||
|
# Channel info (bugfix)
|
||||||
|
channel_configured=channel_status["configured"],
|
||||||
|
channel_fingerprint=channel_status.get("fingerprint"),
|
||||||
|
channel_source=channel_status.get("source"),
|
||||||
|
# Admin check for QR sharing
|
||||||
|
is_admin=is_admin,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Changes Preview
|
||||||
|
|
||||||
|
**account.html - Add to saved key row:**
|
||||||
|
```html
|
||||||
|
{% if is_admin %}
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm"
|
||||||
|
onclick="showKeyQr('{{ key.channel_key }}')" title="Show QR">
|
||||||
|
<i class="bi bi-qr-code"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
**about.html - Wrap existing section:**
|
||||||
|
```html
|
||||||
|
{% if is_admin %}
|
||||||
|
<!-- Channel Key QR Generator -->
|
||||||
|
<div class="card bg-dark border-secondary">
|
||||||
|
...existing QR generator...
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Checklist (Phase 1 Implemented)
|
||||||
|
- [ ] Non-admin users cannot see QR generator in about.html
|
||||||
|
- [ ] Non-admin users cannot see QR buttons on account page
|
||||||
|
- [ ] Admin users can generate QR for any saved key
|
||||||
|
- [ ] QR downloads work correctly
|
||||||
|
- [ ] QR scans correctly with phone camera
|
||||||
|
|
||||||
|
### Implementation Status
|
||||||
|
**Phase 1: COMPLETE** - Admin-only QR sharing implemented:
|
||||||
|
- `app.py`: Fixed about() route to pass channel status + is_admin
|
||||||
|
- `about.html`: QR generator wrapped in `{% if is_admin %}` with Admin badge
|
||||||
|
- `account.html`: QR button added to saved keys (admin only), modal + JS for generation/download
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
- [ ] Optional encryption for temp file storage (paranoid mode, config toggle)
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
- [x] Update UNDER_THE_HOOD.md (v4.1 changes, channel keys)
|
||||||
|
- [ ] General docs refresh
|
||||||
|
|
||||||
|
## Ideas (maybe later)
|
||||||
|
- [ ] Stego detection tool
|
||||||
|
- [ ] Browser extension
|
||||||
|
- [ ] Pi snapshot/backup feature
|
||||||
106
PLAN-4.1.5.md
Normal file
106
PLAN-4.1.5.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Stegasoo 4.1.5 Plan
|
||||||
|
|
||||||
|
## Decode Progress Bar (Real Progress)
|
||||||
|
|
||||||
|
Mirror the encode async pattern for decode operations.
|
||||||
|
|
||||||
|
### Backend Changes
|
||||||
|
|
||||||
|
**1. Add async mode to `/decode` route (`app.py`)**
|
||||||
|
- Check for `async=true` form param
|
||||||
|
- Generate job_id, store job, submit to executor
|
||||||
|
- Return `{"job_id": ..., "status": "pending"}` immediately
|
||||||
|
|
||||||
|
**2. Add decode status/progress endpoints (`app.py`)**
|
||||||
|
```python
|
||||||
|
@app.route("/decode/status/<job_id>")
|
||||||
|
def decode_status(job_id):
|
||||||
|
# Return {"status": "pending|running|complete|error", "result": {...}}
|
||||||
|
|
||||||
|
@app.route("/decode/progress/<job_id>")
|
||||||
|
def decode_progress(job_id):
|
||||||
|
# Read from /tmp/stegasoo_progress_{job_id}.json
|
||||||
|
# Return {"percent": 0-100, "phase": "..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Add `_run_decode_job()` background worker (`app.py`)**
|
||||||
|
- Similar to `_run_encode_job()`
|
||||||
|
- Pass `progress_file` param to decode function
|
||||||
|
- Store result/error in job dict
|
||||||
|
|
||||||
|
**4. Update decode functions to write progress (`lsb_steganography.py`, `dct_steganography.py`)**
|
||||||
|
|
||||||
|
Phases for decode:
|
||||||
|
- `"starting"` (0%)
|
||||||
|
- `"reading"` (10%) - reading stego image
|
||||||
|
- `"extracting"` (30%) - extracting hidden data
|
||||||
|
- `"decrypting"` (60%) - Argon2 + AES decryption
|
||||||
|
- `"verifying"` (80%) - HMAC verification
|
||||||
|
- `"finalizing"` (95%) - preparing output
|
||||||
|
- `"complete"` (100%)
|
||||||
|
|
||||||
|
### Frontend Changes
|
||||||
|
|
||||||
|
**5. Update decode form submission (`decode.html`)**
|
||||||
|
- Add async form handler like encode
|
||||||
|
- Call `Stegasoo.submitDecodeAsync(form, btn)`
|
||||||
|
|
||||||
|
**6. Add decode async methods (`stegasoo.js`)**
|
||||||
|
```javascript
|
||||||
|
submitDecodeAsync(form, btn) // POST with async=true, show modal
|
||||||
|
pollDecodeProgress(jobId) // Poll /decode/status, /decode/progress
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuse existing:
|
||||||
|
- `showProgressModal('Decoding')`
|
||||||
|
- `updateProgress(percent, phase)`
|
||||||
|
|
||||||
|
**7. Handle decode result redirect**
|
||||||
|
- On complete: redirect to `/decode/result/{file_id}` or display inline
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
```
|
||||||
|
frontends/web/app.py
|
||||||
|
- Add async handling to /decode route (~line 1300+)
|
||||||
|
- Add /decode/status/<job_id> endpoint
|
||||||
|
- Add /decode/progress/<job_id> endpoint
|
||||||
|
- Add _run_decode_job() function
|
||||||
|
|
||||||
|
frontends/web/static/js/stegasoo.js
|
||||||
|
- Add submitDecodeAsync()
|
||||||
|
- Add pollDecodeProgress()
|
||||||
|
|
||||||
|
frontends/web/templates/decode.html
|
||||||
|
- Update form submit to use async mode
|
||||||
|
|
||||||
|
src/stegasoo/lsb_steganography.py
|
||||||
|
- Add progress_file param to decode()
|
||||||
|
- Write progress at each phase
|
||||||
|
|
||||||
|
src/stegasoo/dct_steganography.py
|
||||||
|
- Add progress_file param to decode()
|
||||||
|
- Write progress at each phase
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Decode shows progress modal on submit
|
||||||
|
- [ ] Progress bar animates through phases
|
||||||
|
- [ ] Successful decode redirects to result
|
||||||
|
- [ ] Failed decode shows error in modal
|
||||||
|
- [ ] Works for both LSB and DCT modes
|
||||||
|
- [ ] Works for message and file payloads
|
||||||
|
- [ ] Progress file cleaned up after completion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other 4.1.5 Ideas (if time)
|
||||||
|
|
||||||
|
- [ ] Role-based permissions: admin / mod / user
|
||||||
|
- [ ] Better capacity estimates / pre-flight check
|
||||||
|
- [ ] Stego detection tool
|
||||||
|
|
||||||
|
## Bugs / Nice to Have
|
||||||
|
|
||||||
|
- [ ] **flash-stock-img.sh 16GB resize not working** - partition still full SD size after flash, makes dd pull slow. Investigate resize2fs/parted logic and test fix.
|
||||||
34
RELEASE_NOTES.md
Normal file
34
RELEASE_NOTES.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## Stegasoo v4.1.3
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **SSL Certificate Generation**: First-boot wizard now properly generates self-signed certs when HTTPS is enabled
|
||||||
|
- **Download Bug Fixed**: No more "File expired or not found" errors - fixed multi-worker temp file sharing
|
||||||
|
- **Docker Build**: Reduced build context from 2.3GB to ~900KB
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- Docker memory limits increased to 2GB (prevents OOM on large DCT operations)
|
||||||
|
- Decode button now shows loading spinner during processing
|
||||||
|
- Headless Pi flash script with Trixie/NetworkManager support
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
```bash
|
||||||
|
docker-compose up -d web # Web UI on :5000
|
||||||
|
docker-compose up -d api # REST API on :8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raspberry Pi Image
|
||||||
|
Download `stegasoo-rpi-4.1.3.img.zst`, flash to SD card, and boot. The first-boot wizard will guide you through WiFi, HTTPS, and channel key setup.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Flash with included script
|
||||||
|
./rpi/flash-image.sh stegasoo-rpi-4.1.3.img.zst /dev/sdX
|
||||||
|
|
||||||
|
# First time: save your WiFi credentials
|
||||||
|
./rpi/inject-wifi.sh --setup
|
||||||
|
|
||||||
|
# Then inject WiFi after flashing
|
||||||
|
sudo ./rpi/inject-wifi.sh /dev/sdX
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Changelog
|
||||||
|
See [CHANGELOG.md](CHANGELOG.md) for complete details.
|
||||||
10
SECURITY.md
10
SECURITY.md
@@ -4,16 +4,16 @@
|
|||||||
|
|
||||||
| Version | Supported | Notes |
|
| Version | Supported | Notes |
|
||||||
| ------- | ------------------ | ----- |
|
| ------- | ------------------ | ----- |
|
||||||
| 4.x.x | ✅ Active | Current release |
|
| 4.1.x | Current Version | What you SHOULD be using. |
|
||||||
| 3.x.x | ⚠️ Security fixes only | Upgrade recommended |
|
| 4.x.x | ⚠️ Security fixes only | Upgrade (EOL soon) |
|
||||||
| 2.x.x | ❌ End of life | |
|
| <= 3.x.x | ❌ End of life | |
|
||||||
| 1.x.x | ❌ End of life | |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||||
|
|
||||||
Instead, please email: **security@example.com** (replace with your email)
|
Instead, please email: **adlee-was-taken@proton.me**
|
||||||
|
|
||||||
Include:
|
Include:
|
||||||
- Description of the vulnerability
|
- Description of the vulnerability
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work under the hood.
|
A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work under the hood.
|
||||||
|
|
||||||
**Version 4.0** - Updated for simplified authentication (no date dependency)
|
**Version 4.1** - Updated for channel keys and deployment isolation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -22,20 +22,20 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ STEGASOO ARCHITECTURE (v4.0) │
|
│ STEGASOO ARCHITECTURE (v4.1) │
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ │
|
│ │
|
||||||
│ INPUTS PROCESSING OUTPUT │
|
│ INPUTS PROCESSING OUTPUT │
|
||||||
│ ─────── ────────── ────── │
|
│ ─────── ────────── ────── │
|
||||||
│ │
|
│ │
|
||||||
│ Reference Photo ─┐ │
|
│ Reference Photo ─┐ │
|
||||||
│ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │
|
│ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │
|
||||||
│ PIN/RSA Key ─────┘ │ │
|
│ PIN/RSA Key ─────┤ │ │
|
||||||
│ ▼ │
|
│ Channel Key ─────┘ (v4.1) ▼ │
|
||||||
│ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │
|
│ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │
|
||||||
│ Encryption │ │
|
│ Encryption │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ Carrier Image ───────────────────────────────────────► Embedding ──► Stego│
|
│ Carrier Image ───────────────────────────────────────► Embedding ─► Stego │
|
||||||
│ (LSB/DCT) Image │
|
│ (LSB/DCT) Image │
|
||||||
│ │
|
│ │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -50,11 +50,24 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde
|
|||||||
| Header size | 75 bytes | 65 bytes (no date field) |
|
| Header size | 75 bytes | 65 bytes (no date field) |
|
||||||
| Python support | 3.10+ | 3.10-3.12 only |
|
| Python support | 3.10+ | 3.10-3.12 only |
|
||||||
|
|
||||||
|
### v4.1 Changes
|
||||||
|
|
||||||
|
| Change | v4.0 | v4.1 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Channel keys | None | 32-byte deployment isolation |
|
||||||
|
| Key derivation | passphrase + ref + pin | passphrase + ref + pin + channel |
|
||||||
|
| Web auth | Session-based | Session + admin/user roles |
|
||||||
|
| Raspberry Pi | Manual setup | First-boot wizard with gum |
|
||||||
|
| Docker | Basic | Production-ready compose |
|
||||||
|
|
||||||
|
**Channel Keys** provide deployment isolation - messages encoded on one Stegasoo instance cannot be decoded by another instance with a different channel key, even with the same passphrase/PIN/reference photo.
|
||||||
|
|
||||||
### Module Responsibilities
|
### Module Responsibilities
|
||||||
|
|
||||||
| Module | File | Purpose |
|
| Module | File | Purpose |
|
||||||
|--------|------|---------|
|
|--------|------|---------|
|
||||||
| **Crypto** | `crypto.py` | Key derivation (Argon2id), AES-256-GCM encryption/decryption |
|
| **Crypto** | `crypto.py` | Key derivation (Argon2id), AES-256-GCM encryption/decryption |
|
||||||
|
| **Channel** | `channel.py` | Channel key management, deployment isolation (v4.1) |
|
||||||
| **Steganography** | `steganography.py` | LSB pixel manipulation, capacity calculation |
|
| **Steganography** | `steganography.py` | LSB pixel manipulation, capacity calculation |
|
||||||
| **DCT Steganography** | `dct_steganography.py` | Frequency-domain embedding, jpegio integration |
|
| **DCT Steganography** | `dct_steganography.py` | Frequency-domain embedding, jpegio integration |
|
||||||
| **Compression** | `compression.py` | Optional LZ4 compression of payload |
|
| **Compression** | `compression.py` | Optional LZ4 compression of payload |
|
||||||
@@ -626,7 +639,7 @@ Factor 1: Reference Photo ─┐
|
|||||||
• 80-256 bits entropy │
|
• 80-256 bits entropy │
|
||||||
• "Something you have" │
|
• "Something you have" │
|
||||||
├──► Combined entropy: 133-400+ bits
|
├──► Combined entropy: 133-400+ bits
|
||||||
Factor 2: Passphrase │ (Beyond brute force)
|
Factor 2: Passphrase │ (Beyond brute force)
|
||||||
• 43-132 bits entropy │
|
• 43-132 bits entropy │
|
||||||
• "Something you know" │
|
• "Something you know" │
|
||||||
• 4 words default (v4.0) │
|
• 4 words default (v4.0) │
|
||||||
@@ -688,7 +701,7 @@ AUTHENTICATED ENCRYPTION (AES-256-GCM)
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ ENCODE FLOW (v4.0) │
|
│ ENCODE FLOW (v4.0) │
|
||||||
└──────────────────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
User Inputs Processing Output
|
User Inputs Processing Output
|
||||||
@@ -714,14 +727,14 @@ Carrier Image ──────────────────────
|
|||||||
│ │
|
│ │
|
||||||
┌───────────┴─────┴────────────┐
|
┌───────────┴─────┴────────────┐
|
||||||
│ │
|
│ │
|
||||||
LSB Mode DCT Mode
|
LSB Mode DCT Mode
|
||||||
│ │
|
│ │
|
||||||
▼ ▼
|
▼ ▼
|
||||||
embed_lsb() embed_in_dct()
|
embed_lsb() embed_in_dct()
|
||||||
(pixel LSBs) (DCT coefficients)
|
(pixel LSBs) (DCT coefficients)
|
||||||
│ │
|
│ │
|
||||||
▼ ▼
|
▼ ▼
|
||||||
PNG Output PNG or JPEG
|
PNG Output PNG or JPEG
|
||||||
│ │
|
│ │
|
||||||
└──────────┬───────────────────┘
|
└──────────┬───────────────────┘
|
||||||
│
|
│
|
||||||
@@ -793,8 +806,8 @@ Stego Image ──────────► detect_mode() ──────
|
|||||||
Both modes share the same cryptographic foundation (Argon2id + AES-256-GCM) and multi-factor authentication, ensuring security regardless of embedding method.
|
Both modes share the same cryptographic foundation (Argon2id + AES-256-GCM) and multi-factor authentication, ensuring security regardless of embedding method.
|
||||||
|
|
||||||
The choice comes down to your use case:
|
The choice comes down to your use case:
|
||||||
- **Private channel?** → LSB (maximum capacity)
|
|
||||||
- **Public platform?** → DCT (maximum compatibility)
|
- **Public platform?** → DCT (maximum compatibility)
|
||||||
|
- **Private channel?** → LSB (maximum capacity)
|
||||||
|
|
||||||
### v4.0 Simplifications
|
### v4.0 Simplifications
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -1576,7 +1561,25 @@ def decode_download(file_id):
|
|||||||
|
|
||||||
@app.route("/about")
|
@app.route("/about")
|
||||||
def about():
|
def about():
|
||||||
return render_template("about.html", has_argon2=has_argon2(), has_qrcode_read=HAS_QRCODE_READ)
|
from stegasoo.channel import get_channel_status
|
||||||
|
|
||||||
|
channel_status = get_channel_status()
|
||||||
|
|
||||||
|
# Check if user is admin (for QR sharing)
|
||||||
|
current_user = get_current_user()
|
||||||
|
is_admin = current_user.is_admin if current_user else False
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"about.html",
|
||||||
|
has_argon2=has_argon2(),
|
||||||
|
has_qrcode_read=HAS_QRCODE_READ,
|
||||||
|
# Channel info (bugfix - was not being passed)
|
||||||
|
channel_configured=channel_status["configured"],
|
||||||
|
channel_fingerprint=channel_status.get("fingerprint"),
|
||||||
|
channel_source=channel_status.get("source"),
|
||||||
|
# Admin check for QR sharing
|
||||||
|
is_admin=is_admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -2320,13 +2323,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),
|
||||||
|
}
|
||||||
@@ -331,10 +331,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Channel Key QR Generator -->
|
<!-- Channel Key QR Generator (Admin only) -->
|
||||||
|
{% if is_admin %}
|
||||||
<div class="card bg-dark border-secondary">
|
<div class="card bg-dark border-secondary">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-qr-code me-2"></i>Share Channel Key via QR
|
<i class="bi bi-qr-code me-2"></i>Share Channel Key via QR
|
||||||
|
<span class="badge bg-warning text-dark ms-2"><i class="bi bi-shield-check me-1"></i>Admin</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="small text-muted mb-3">Generate a QR code to share a channel key with others.</p>
|
<p class="small text-muted mb-3">Generate a QR code to share a channel key with others.</p>
|
||||||
@@ -366,6 +368,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
|
{% if is_admin %}
|
||||||
|
<button type="button" class="btn btn-outline-info"
|
||||||
|
onclick="showKeyQr('{{ key.channel_key }}', '{{ key.name }}')"
|
||||||
|
title="Show QR Code">
|
||||||
|
<i class="bi bi-qr-code"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
onclick="renameKey({{ key.id }}, '{{ key.name }}')"
|
onclick="renameKey({{ key.id }}, '{{ key.name }}')"
|
||||||
title="Rename">
|
title="Rename">
|
||||||
@@ -218,10 +225,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if is_admin %}
|
||||||
|
<!-- QR Code Modal (Admin only) -->
|
||||||
|
<div class="modal fade" id="qrModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title"><i class="bi bi-qr-code me-2"></i><span id="qrKeyName">Channel Key</span></h6>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<canvas id="qrCanvas" class="bg-white p-2 rounded"></canvas>
|
||||||
|
<div class="mt-2">
|
||||||
|
<code class="small" id="qrKeyDisplay"></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrDownload">
|
||||||
|
<i class="bi bi-download me-1"></i>Download PNG
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
||||||
|
{% if is_admin %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
|
||||||
|
{% endif %}
|
||||||
<script>
|
<script>
|
||||||
StegasooAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
|
StegasooAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
|
||||||
|
|
||||||
@@ -230,5 +265,45 @@ function renameKey(keyId, currentName) {
|
|||||||
document.getElementById('renameForm').action = '/account/keys/' + keyId + '/rename';
|
document.getElementById('renameForm').action = '/account/keys/' + keyId + '/rename';
|
||||||
new bootstrap.Modal(document.getElementById('renameModal')).show();
|
new bootstrap.Modal(document.getElementById('renameModal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% if is_admin %}
|
||||||
|
function showKeyQr(channelKey, keyName) {
|
||||||
|
// Format key with dashes if not already
|
||||||
|
const clean = channelKey.replace(/-/g, '').toUpperCase();
|
||||||
|
const formatted = clean.match(/.{4}/g)?.join('-') || clean;
|
||||||
|
|
||||||
|
// Update modal content
|
||||||
|
document.getElementById('qrKeyName').textContent = keyName;
|
||||||
|
document.getElementById('qrKeyDisplay').textContent = formatted;
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
const canvas = document.getElementById('qrCanvas');
|
||||||
|
if (typeof QRCode !== 'undefined' && canvas) {
|
||||||
|
QRCode.toCanvas(canvas, formatted, {
|
||||||
|
width: 200,
|
||||||
|
margin: 2,
|
||||||
|
color: { dark: '#000', light: '#fff' }
|
||||||
|
}, function(error) {
|
||||||
|
if (error) {
|
||||||
|
console.error('QR generation error:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new bootstrap.Modal(document.getElementById('qrModal')).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download QR as PNG
|
||||||
|
document.getElementById('qrDownload')?.addEventListener('click', function() {
|
||||||
|
const canvas = document.getElementById('qrCanvas');
|
||||||
|
const keyName = document.getElementById('qrKeyName').textContent;
|
||||||
|
if (canvas) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = 'stegasoo-channel-key-' + keyName.toLowerCase().replace(/\s+/g, '-') + '.png';
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Stegasoo Pi Image Build Workflow
|
# Stegasoo Pi Image Build Workflow
|
||||||
|
|
||||||
|
> **Note:** This guide is for developers building custom Pi images.
|
||||||
|
> **End users:** Just download the pre-built `.img.zst` from [Releases](https://github.com/adlee-was-taken/stegasoo/releases), flash it, and boot. No build process needed.
|
||||||
|
|
||||||
Quick reference for building a distributable SD card image.
|
Quick reference for building a distributable SD card image.
|
||||||
|
|
||||||
## Step 1: Flash Fresh Raspbian
|
## Step 1: Flash Fresh Raspbian
|
||||||
@@ -26,26 +29,48 @@ ssh admin@stegasoo.local
|
|||||||
# Take ownership of /opt (for pyenv, jpegio builds)
|
# Take ownership of /opt (for pyenv, jpegio builds)
|
||||||
sudo chown admin:admin /opt
|
sudo chown admin:admin /opt
|
||||||
|
|
||||||
# Install git (not included in Lite image)
|
# Install git and zstd (not included in Lite image)
|
||||||
sudo apt-get update && sudo apt-get install -y git
|
sudo apt-get update && sudo apt-get install -y git zstd jq
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 4: Clone & Run Setup
|
## Step 4: Clone Repo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt
|
cd /opt
|
||||||
git clone -b 4.1 https://github.com/adlee-was-taken/stegasoo.git stegasoo
|
git clone -b 4.1 https://github.com/adlee-was-taken/stegasoo.git stegasoo
|
||||||
cd stegasoo
|
|
||||||
./rpi/setup.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This takes ~15-20 minutes and installs:
|
## Step 5: Copy Pre-built Tarball (from host)
|
||||||
- Python 3.12 via pyenv
|
|
||||||
- jpegio (patched for ARM)
|
|
||||||
- Stegasoo with web UI
|
|
||||||
- Systemd service
|
|
||||||
|
|
||||||
## Step 5: Test It Works
|
> **Dev-only asset:** This tarball is for building Pi images, not for end users.
|
||||||
|
> It's available on [Releases](https://github.com/adlee-was-taken/stegasoo/releases) for image builders.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On your host machine:
|
||||||
|
scp rpi/stegasoo-pi-arm64.tar.zst admin@stegasoo.local:/opt/stegasoo/rpi/
|
||||||
|
```
|
||||||
|
|
||||||
|
This tarball contains:
|
||||||
|
- pyenv with Python 3.12 (pre-compiled for ARM64)
|
||||||
|
- venv with all dependencies (jpegio, scipy, etc.)
|
||||||
|
|
||||||
|
Install time: **~2 minutes** (vs 20+ min from source)
|
||||||
|
|
||||||
|
## Step 6: Run Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/stegasoo
|
||||||
|
./rpi/setup.sh # Detects local tarball, skips download
|
||||||
|
```
|
||||||
|
|
||||||
|
### From-Source Build (optional)
|
||||||
|
|
||||||
|
To build without the pre-built tarball:
|
||||||
|
```bash
|
||||||
|
./rpi/setup.sh --no-prebuilt # Takes 15-20 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Test It Works
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl start stegasoo
|
sudo systemctl start stegasoo
|
||||||
@@ -53,7 +78,7 @@ curl -k https://localhost:5000
|
|||||||
# Should return HTML
|
# Should return HTML
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 6: Sanitize for Distribution
|
## Step 8: Sanitize for Distribution
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Full sanitize (for final image - removes WiFi, shuts down)
|
# Full sanitize (for final image - removes WiFi, shuts down)
|
||||||
@@ -73,7 +98,7 @@ This removes:
|
|||||||
|
|
||||||
The script validates all cleanup steps before finishing.
|
The script validates all cleanup steps before finishing.
|
||||||
|
|
||||||
## Step 7: Copy the Image
|
## Step 9: Copy the Image
|
||||||
|
|
||||||
Remove SD card, insert into your Linux machine:
|
Remove SD card, insert into your Linux machine:
|
||||||
|
|
||||||
@@ -85,7 +110,7 @@ lsblk
|
|||||||
sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress
|
sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 8: Shrink & Compress
|
## Step 10: Shrink & Compress
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Optional: Shrink image (saves space)
|
# Optional: Shrink image (saves space)
|
||||||
@@ -97,7 +122,7 @@ sudo ./pishrink.sh stegasoo-rpi-*.img
|
|||||||
zstd -19 -T0 stegasoo-rpi-*.img
|
zstd -19 -T0 stegasoo-rpi-*.img
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 9: Distribute
|
## Step 11: Distribute
|
||||||
|
|
||||||
Upload `.img.zst` to GitHub Releases.
|
Upload `.img.zst` to GitHub Releases.
|
||||||
|
|
||||||
@@ -115,19 +140,58 @@ zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Creating the Pre-built Tarball
|
||||||
|
|
||||||
|
After a successful from-source build, create the pre-built tarball for future installs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the Pi after successful setup:
|
||||||
|
cd ~
|
||||||
|
|
||||||
|
# Strip caches and tests from venv (295MB → 208MB)
|
||||||
|
find /opt/stegasoo/venv/ -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null
|
||||||
|
find /opt/stegasoo/venv/ -type d -name 'tests' -exec rm -rf {} + 2>/dev/null
|
||||||
|
find /opt/stegasoo/venv/ -type d -name 'test' -exec rm -rf {} + 2>/dev/null
|
||||||
|
|
||||||
|
# Create venv tarball
|
||||||
|
cd /opt/stegasoo
|
||||||
|
tar -cf - venv/ | zstd -19 -T0 > ~/stegasoo-venv.tar.zst
|
||||||
|
|
||||||
|
# Create combined tarball (pyenv + venv pointer)
|
||||||
|
cd ~
|
||||||
|
tar -cf - .pyenv stegasoo-venv.tar.zst | zstd -19 -T0 > /tmp/stegasoo-pi-arm64.tar.zst
|
||||||
|
|
||||||
|
# Check size (should be ~50-60MB)
|
||||||
|
ls -lh /tmp/stegasoo-pi-arm64.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
Pull to host and upload to GitHub releases:
|
||||||
|
```bash
|
||||||
|
# On host:
|
||||||
|
scp admin@stegasoo.local:/tmp/stegasoo-pi-arm64.tar.zst ./
|
||||||
|
# Upload to GitHub releases as stegasoo-pi-arm64.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Quick Command Summary
|
## Quick Command Summary
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On Pi (after SSH):
|
# On Pi (after SSH):
|
||||||
sudo chown admin:admin /opt
|
sudo chown admin:admin /opt
|
||||||
sudo apt-get update && sudo apt-get install -y git
|
sudo apt-get update && sudo apt-get install -y git zstd jq
|
||||||
cd /opt && git clone -b 4.1 https://github.com/adlee-was-taken/stegasoo.git stegasoo
|
cd /opt && git clone -b 4.1 https://github.com/adlee-was-taken/stegasoo.git stegasoo
|
||||||
cd stegasoo && ./rpi/setup.sh
|
|
||||||
|
# On host (copy tarball):
|
||||||
|
scp rpi/stegasoo-pi-arm64.tar.zst admin@stegasoo.local:/opt/stegasoo/rpi/
|
||||||
|
|
||||||
|
# On Pi (run setup):
|
||||||
|
cd /opt/stegasoo && ./rpi/setup.sh
|
||||||
sudo systemctl start stegasoo
|
sudo systemctl start stegasoo
|
||||||
curl -k https://localhost:5000
|
curl -k https://localhost:5000
|
||||||
sudo /opt/stegasoo/rpi/sanitize-for-image.sh
|
sudo /opt/stegasoo/rpi/sanitize-for-image.sh
|
||||||
|
|
||||||
# On your machine:
|
# On host (pull image):
|
||||||
sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress
|
sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress
|
||||||
zstd -19 -T0 stegasoo-rpi-*.img
|
zstd -19 -T0 stegasoo-rpi-*.img
|
||||||
```
|
```
|
||||||
|
|||||||
12
rpi/config.json.example
Normal file
12
rpi/config.json.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"hostname": "stegasoo",
|
||||||
|
"username": "admin",
|
||||||
|
"password": "stegasoo",
|
||||||
|
"wifiSSID": "YourNetworkName",
|
||||||
|
"wifiPassword": "YourWiFiPassword",
|
||||||
|
"wifiCountry": "US",
|
||||||
|
"locale": "en_US.UTF-8",
|
||||||
|
"keyboardLayout": "us",
|
||||||
|
"timezone": "America/New_York",
|
||||||
|
"enableSSH": true
|
||||||
|
}
|
||||||
@@ -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 "
|
||||||
|
|||||||
@@ -8,9 +8,14 @@
|
|||||||
# Supports: .img, .img.zst, .img.xz, .img.gz, .img.zst.zip (GitHub release format)
|
# Supports: .img, .img.zst, .img.xz, .img.gz, .img.zst.zip (GitHub release format)
|
||||||
# If device is specified, skips auto-detection (useful for NVMe/large drives)
|
# If device is specified, skips auto-detection (useful for NVMe/large drives)
|
||||||
#
|
#
|
||||||
|
# Optional: Place config.json in same directory for headless WiFi setup
|
||||||
|
#
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_FILE="$SCRIPT_DIR/config.json"
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
@@ -18,6 +23,28 @@ BLUE='\033[0;34m'
|
|||||||
BOLD='\033[1m'
|
BOLD='\033[1m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Load config if present (optional - for headless WiFi setup)
|
||||||
|
HAS_CONFIG=false
|
||||||
|
if [ -f "$CONFIG_FILE" ] && command -v jq &> /dev/null; then
|
||||||
|
WIFI_SSID=$(jq -r '.wifiSSID // empty' "$CONFIG_FILE")
|
||||||
|
WIFI_PASS=$(jq -r '.wifiPassword // empty' "$CONFIG_FILE")
|
||||||
|
WIFI_COUNTRY=$(jq -r '.wifiCountry // "US"' "$CONFIG_FILE")
|
||||||
|
PI_HOSTNAME=$(jq -r '.hostname // empty' "$CONFIG_FILE")
|
||||||
|
if [ -n "$WIFI_SSID" ] && [ -n "$WIFI_PASS" ]; then
|
||||||
|
HAS_CONFIG=true
|
||||||
|
echo -e "${GREEN}Found config.json - will configure WiFi after flash${NC}"
|
||||||
|
echo -e " WiFi: ${YELLOW}$WIFI_SSID${NC}"
|
||||||
|
if [ -n "$PI_HOSTNAME" ]; then
|
||||||
|
echo -e " Hostname: ${YELLOW}$PI_HOSTNAME${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
elif [ -f "$CONFIG_FILE" ]; then
|
||||||
|
echo -e "${YELLOW}Note: config.json found but jq not installed (apt install jq)${NC}"
|
||||||
|
echo -e "${YELLOW} WiFi will need to be configured manually after boot${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
# Check for required tools
|
# Check for required tools
|
||||||
for cmd in dd lsblk; do
|
for cmd in dd lsblk; do
|
||||||
if ! command -v $cmd &> /dev/null; then
|
if ! command -v $cmd &> /dev/null; then
|
||||||
@@ -222,6 +249,17 @@ if [ -n "$MOUNTED" ]; then
|
|||||||
done
|
done
|
||||||
fi
|
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 "$SELECTED"
|
||||||
|
sudo dd if=/dev/zero of="$SELECTED" bs=1M count=10 status=none
|
||||||
|
sync
|
||||||
|
echo " Wiped clean"
|
||||||
|
fi
|
||||||
|
|
||||||
# Final confirmation
|
# Final confirmation
|
||||||
echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e "${RED}║ WARNING: ALL DATA ON THIS DEVICE WILL BE DESTROYED! ║${NC}"
|
echo -e "${RED}║ WARNING: ALL DATA ON THIS DEVICE WILL BE DESTROYED! ║${NC}"
|
||||||
@@ -284,6 +322,109 @@ echo ""
|
|||||||
echo -e "${GREEN}Syncing...${NC}"
|
echo -e "${GREEN}Syncing...${NC}"
|
||||||
sync
|
sync
|
||||||
|
|
||||||
|
# Inject WiFi config if config.json was loaded
|
||||||
|
if [ "$HAS_CONFIG" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}Configuring WiFi from config.json...${NC}"
|
||||||
|
|
||||||
|
# Wait for partitions to appear
|
||||||
|
sleep 2
|
||||||
|
partprobe "$SELECTED" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Determine boot partition
|
||||||
|
if [[ "$SELECTED" == *"nvme"* ]] || [[ "$SELECTED" == *"mmcblk"* ]]; then
|
||||||
|
BOOT_PART="${SELECTED}p1"
|
||||||
|
else
|
||||||
|
BOOT_PART="${SELECTED}1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -b "$BOOT_PART" ]; then
|
||||||
|
MOUNT_DIR=$(mktemp -d)
|
||||||
|
if mount "$BOOT_PART" "$MOUNT_DIR" 2>/dev/null; then
|
||||||
|
# Create firstrun.sh for WiFi setup
|
||||||
|
cat > "$MOUNT_DIR/firstrun.sh" << 'EOFSCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
set +e
|
||||||
|
|
||||||
|
# Set hostname if provided
|
||||||
|
if [ -n "PLACEHOLDER_HOSTNAME" ] && [ "PLACEHOLDER_HOSTNAME" != "" ]; then
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure WiFi
|
||||||
|
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
|
||||||
|
# NetworkManager method (Trixie)
|
||||||
|
cat >/etc/NetworkManager/system-connections/preconfigured.nmconnection <<'NMEOF'
|
||||||
|
[connection]
|
||||||
|
id=preconfigured
|
||||||
|
type=wifi
|
||||||
|
autoconnect=true
|
||||||
|
|
||||||
|
[wifi]
|
||||||
|
mode=infrastructure
|
||||||
|
ssid=PLACEHOLDER_SSID
|
||||||
|
|
||||||
|
[wifi-security]
|
||||||
|
auth-alg=open
|
||||||
|
key-mgmt=wpa-psk
|
||||||
|
psk=PLACEHOLDER_WIFIPASS
|
||||||
|
|
||||||
|
[ipv4]
|
||||||
|
method=auto
|
||||||
|
|
||||||
|
[ipv6]
|
||||||
|
method=auto
|
||||||
|
NMEOF
|
||||||
|
chmod 600 /etc/NetworkManager/system-connections/preconfigured.nmconnection
|
||||||
|
rfkill unblock wifi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
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
|
||||||
|
sed -i "s/PLACEHOLDER_SSID/$WIFI_SSID/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sed -i "s/PLACEHOLDER_WIFIPASS/$WIFI_PASS/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
sed -i "s/PLACEHOLDER_COUNTRY/$WIFI_COUNTRY/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
if [ -n "$PI_HOSTNAME" ]; then
|
||||||
|
sed -i "s/PLACEHOLDER_HOSTNAME/$PI_HOSTNAME/g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
else
|
||||||
|
sed -i "s/PLACEHOLDER_HOSTNAME//g" "$MOUNT_DIR/firstrun.sh"
|
||||||
|
fi
|
||||||
|
chmod +x "$MOUNT_DIR/firstrun.sh"
|
||||||
|
|
||||||
|
# Update cmdline.txt to run firstrun.sh
|
||||||
|
CMDLINE="$MOUNT_DIR/cmdline.txt"
|
||||||
|
if [ -f "$CMDLINE" ]; then
|
||||||
|
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" > "$CMDLINE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
umount "$MOUNT_DIR"
|
||||||
|
echo -e " ${GREEN}✓${NC} WiFi configured for: $WIFI_SSID"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} Could not mount boot partition"
|
||||||
|
fi
|
||||||
|
rmdir "$MOUNT_DIR" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} Boot partition not found"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e "${GREEN}║ Flash Complete! ║${NC}"
|
echo -e "${GREEN}║ Flash Complete! ║${NC}"
|
||||||
@@ -291,5 +432,11 @@ echo -e "${GREEN}╚════════════════════
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "You can now remove the SD card and boot your Raspberry Pi."
|
echo -e "You can now remove the SD card and boot your Raspberry Pi."
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Tip:${NC} On first boot, SSH in and the setup wizard will run automatically."
|
if [ "$HAS_CONFIG" = true ]; then
|
||||||
|
echo -e "${GREEN}WiFi pre-configured${NC} - Pi will connect to $WIFI_SSID on boot"
|
||||||
|
echo -e "SSH: ${YELLOW}ssh admin@${PI_HOSTNAME:-stegasoo}.local${NC} (password: stegasoo)"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Tip:${NC} On first boot, the setup wizard will help configure WiFi."
|
||||||
|
echo -e "${YELLOW}Tip:${NC} Or place config.json in rpi/ for headless setup next time."
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
285
rpi/flash-stock-img.sh
Executable file
285
rpi/flash-stock-img.sh
Executable file
@@ -0,0 +1,285 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Flash Raspberry Pi image with headless config (Trixie/Bookworm compatible)
|
||||||
|
# Usage: ./flash-stock-img.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"
|
||||||
|
ROOT_PART="${DEVICE}2"
|
||||||
|
elif [ -b "${DEVICE}p1" ]; then
|
||||||
|
BOOT_PART="${DEVICE}p1"
|
||||||
|
ROOT_PART="${DEVICE}p2"
|
||||||
|
else
|
||||||
|
echo "Error: Could not find boot partition"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Resize rootfs to 16GB (faster imaging)
|
||||||
|
# ============================================================================
|
||||||
|
echo
|
||||||
|
read -p "Resize rootfs to 16GB for faster imaging? [Y/n] " resize_confirm
|
||||||
|
if [[ ! "$resize_confirm" =~ ^[Nn]$ ]]; then
|
||||||
|
echo "Resizing rootfs partition to 16GB..."
|
||||||
|
|
||||||
|
# Get boot partition end
|
||||||
|
BOOT_END=$(sudo parted -s "$DEVICE" unit s print | grep "^ 1" | awk '{print $3}' | tr -d 's')
|
||||||
|
|
||||||
|
# Calculate 16GB in sectors (512 byte sectors)
|
||||||
|
# 16GB = 16 * 1024 * 1024 * 1024 / 512 = 33554432 sectors
|
||||||
|
ROOT_SIZE_SECTORS=33554432
|
||||||
|
ROOT_END=$((BOOT_END + ROOT_SIZE_SECTORS))
|
||||||
|
|
||||||
|
# Delete and recreate partition 2 with fixed size
|
||||||
|
sudo parted -s "$DEVICE" rm 2
|
||||||
|
sudo parted -s "$DEVICE" mkpart primary ext4 $((BOOT_END + 1))s ${ROOT_END}s
|
||||||
|
|
||||||
|
# Refresh partition table
|
||||||
|
sudo partprobe "$DEVICE"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Check and resize filesystem
|
||||||
|
echo "Checking filesystem..."
|
||||||
|
sudo e2fsck -f -y "$ROOT_PART" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Resizing filesystem to fit partition..."
|
||||||
|
sudo resize2fs "$ROOT_PART"
|
||||||
|
|
||||||
|
# Disable Pi OS auto-expand on first boot
|
||||||
|
echo "Disabling auto-expand..."
|
||||||
|
TEMP_ROOT=$(mktemp -d)
|
||||||
|
sudo mount "$ROOT_PART" "$TEMP_ROOT"
|
||||||
|
|
||||||
|
# Remove resize2fs_once service if it exists
|
||||||
|
sudo rm -f "$TEMP_ROOT/etc/init.d/resize2fs_once"
|
||||||
|
sudo rm -f "$TEMP_ROOT/etc/rc3.d/S01resize2fs_once"
|
||||||
|
|
||||||
|
# Disable the systemd resize service
|
||||||
|
sudo rm -f "$TEMP_ROOT/etc/systemd/system/multi-user.target.wants/rpi-resizerootfs.service"
|
||||||
|
|
||||||
|
# Remove init= parameter from cmdline.txt on boot partition (handled later)
|
||||||
|
|
||||||
|
sudo umount "$TEMP_ROOT"
|
||||||
|
rmdir "$TEMP_ROOT"
|
||||||
|
|
||||||
|
echo " Rootfs resized to 16GB (auto-expand disabled)"
|
||||||
|
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 existing systemd.run and init= (auto-expand)
|
||||||
|
CURRENT=$(cat "$CMDLINE" | tr -d '\n' | sed 's| systemd.run.*||g' | sed 's| init=[^ ]*||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"
|
||||||
@@ -168,12 +168,41 @@ fi
|
|||||||
DEV_SIZE=$(blockdev --getsize64 "$SELECTED")
|
DEV_SIZE=$(blockdev --getsize64 "$SELECTED")
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}[1/3]${NC} Copying image from $SELECTED..."
|
echo -e "${GREEN}[1/4]${NC} Copying image from $SELECTED..."
|
||||||
dd if="$SELECTED" bs=4M status=none | pv -s "$DEV_SIZE" > "$IMG_FILE"
|
dd if="$SELECTED" bs=4M status=none | pv -s "$DEV_SIZE" > "$IMG_FILE"
|
||||||
sync
|
sync
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}[2/3]${NC} Shrinking image..."
|
echo -e "${GREEN}[2/4]${NC} Re-enabling auto-expand for distribution..."
|
||||||
|
# Mount the image and restore auto-expand service (may have been disabled during build)
|
||||||
|
LOOP_DEV=$(losetup -f --show -P "$IMG_FILE")
|
||||||
|
if [ -n "$LOOP_DEV" ]; then
|
||||||
|
TEMP_MOUNT=$(mktemp -d)
|
||||||
|
if mount "${LOOP_DEV}p2" "$TEMP_MOUNT" 2>/dev/null; then
|
||||||
|
# Re-enable the resize service if the service file exists
|
||||||
|
SERVICE_FILE="$TEMP_MOUNT/lib/systemd/system/rpi-resizerootfs.service"
|
||||||
|
SERVICE_LINK="$TEMP_MOUNT/etc/systemd/system/multi-user.target.wants/rpi-resizerootfs.service"
|
||||||
|
if [ -f "$SERVICE_FILE" ] && [ ! -L "$SERVICE_LINK" ]; then
|
||||||
|
mkdir -p "$(dirname "$SERVICE_LINK")"
|
||||||
|
ln -sf /lib/systemd/system/rpi-resizerootfs.service "$SERVICE_LINK"
|
||||||
|
echo -e " ${GREEN}✓${NC} Auto-expand service re-enabled"
|
||||||
|
elif [ -L "$SERVICE_LINK" ]; then
|
||||||
|
echo -e " ${GREEN}✓${NC} Auto-expand already enabled"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} Could not find resize service file"
|
||||||
|
fi
|
||||||
|
umount "$TEMP_MOUNT"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} Could not mount rootfs, skipping auto-expand fix"
|
||||||
|
fi
|
||||||
|
rmdir "$TEMP_MOUNT" 2>/dev/null || true
|
||||||
|
losetup -d "$LOOP_DEV"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} Could not create loop device, skipping auto-expand fix"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}[3/4]${NC} Shrinking image..."
|
||||||
if command -v pishrink.sh &> /dev/null; then
|
if command -v pishrink.sh &> /dev/null; then
|
||||||
pishrink.sh "$IMG_FILE"
|
pishrink.sh "$IMG_FILE"
|
||||||
elif [ -f "./pishrink.sh" ]; then
|
elif [ -f "./pishrink.sh" ]; then
|
||||||
@@ -187,11 +216,11 @@ fi
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$SKIP_COMPRESS" = true ]; then
|
if [ "$SKIP_COMPRESS" = true ]; then
|
||||||
echo -e "${GREEN}[3/3]${NC} Skipping compression (.img output)"
|
echo -e "${GREEN}[4/4]${NC} Skipping compression (.img output)"
|
||||||
FINAL_SIZE=$(du -h "$IMG_FILE" | awk '{print $1}')
|
FINAL_SIZE=$(du -h "$IMG_FILE" | awk '{print $1}')
|
||||||
OUTPUT="$IMG_FILE"
|
OUTPUT="$IMG_FILE"
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}[3/3]${NC} Compressing with zstd..."
|
echo -e "${GREEN}[4/4]${NC} Compressing with zstd..."
|
||||||
pv "$IMG_FILE" | zstd -19 -T0 -q > "$OUTPUT"
|
pv "$IMG_FILE" | zstd -19 -T0 -q > "$OUTPUT"
|
||||||
rm -f "$IMG_FILE"
|
rm -f "$IMG_FILE"
|
||||||
FINAL_SIZE=$(du -h "$OUTPUT" | awk '{print $1}')
|
FINAL_SIZE=$(du -h "$OUTPUT" | awk '{print $1}')
|
||||||
|
|||||||
276
rpi/setup.sh
276
rpi/setup.sh
@@ -36,7 +36,9 @@ show_help() {
|
|||||||
echo "Usage: $0 [options]"
|
echo "Usage: $0 [options]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " -h, --help Show this help message"
|
echo " -h, --help Show this help message"
|
||||||
|
echo " --no-prebuilt Build from source instead of using pre-built venv"
|
||||||
|
echo " --from-source Same as --no-prebuilt"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Configuration:"
|
echo "Configuration:"
|
||||||
echo " Config files are loaded in order (later overrides earlier):"
|
echo " Config files are loaded in order (later overrides earlier):"
|
||||||
@@ -93,7 +95,7 @@ echo -e "\033[1;37m Raspberry Pi Setup\033[0m"
|
|||||||
echo -e "\033[38;5;93m══════════════\033[38;5;99m══════════════\033[38;5;105m══════════════\033[38;5;117m══════════════\033[0m"
|
echo -e "\033[38;5;93m══════════════\033[38;5;99m══════════════\033[38;5;105m══════════════\033[38;5;117m══════════════\033[0m"
|
||||||
echo ""
|
echo ""
|
||||||
echo " This will install Stegasoo with full DCT support"
|
echo " This will install Stegasoo with full DCT support"
|
||||||
echo " Estimated time: 15-20 minutes on Pi 5"
|
echo " Estimated time: ~2 minutes (pre-built) or 15-20 min (from source)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if running on ARM
|
# Check if running on ARM
|
||||||
@@ -135,6 +137,7 @@ sudo apt-get install -y \
|
|||||||
build-essential \
|
build-essential \
|
||||||
git \
|
git \
|
||||||
curl \
|
curl \
|
||||||
|
zstd \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
zlib1g-dev \
|
zlib1g-dev \
|
||||||
libbz2-dev \
|
libbz2-dev \
|
||||||
@@ -164,49 +167,9 @@ else
|
|||||||
echo " gum already installed"
|
echo " gum already installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}[4/12]${NC} Installing pyenv and Python $PYTHON_VERSION..."
|
echo -e "${GREEN}[4/12]${NC} Cloning Stegasoo..."
|
||||||
|
|
||||||
# Install pyenv if not present
|
# Clone Stegasoo first (needed to check for pre-built tarball)
|
||||||
if [ ! -d "$HOME/.pyenv" ]; then
|
|
||||||
curl https://pyenv.run | bash
|
|
||||||
|
|
||||||
# Add pyenv to current shell
|
|
||||||
export PYENV_ROOT="$HOME/.pyenv"
|
|
||||||
export PATH="$PYENV_ROOT/bin:$PATH"
|
|
||||||
eval "$(pyenv init -)"
|
|
||||||
|
|
||||||
# Add to .bashrc if not already there
|
|
||||||
if ! grep -q 'PYENV_ROOT' ~/.bashrc; then
|
|
||||||
echo '' >> ~/.bashrc
|
|
||||||
echo '# pyenv' >> ~/.bashrc
|
|
||||||
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
|
|
||||||
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
|
|
||||||
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "pyenv already installed, skipping..."
|
|
||||||
export PYENV_ROOT="$HOME/.pyenv"
|
|
||||||
export PATH="$PYENV_ROOT/bin:$PATH"
|
|
||||||
eval "$(pyenv init -)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install Python 3.12 if not present
|
|
||||||
if ! pyenv versions | grep -q "$PYTHON_VERSION"; then
|
|
||||||
echo "Building Python $PYTHON_VERSION (this takes ~10 minutes)..."
|
|
||||||
pyenv install $PYTHON_VERSION
|
|
||||||
fi
|
|
||||||
pyenv global $PYTHON_VERSION
|
|
||||||
|
|
||||||
# Verify Python version
|
|
||||||
INSTALLED_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
|
||||||
if [ "$INSTALLED_PY" != "$PYTHON_VERSION" ]; then
|
|
||||||
echo -e "${RED}Error: Python $PYTHON_VERSION not active. Got: $INSTALLED_PY${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}[5/12]${NC} Cloning Stegasoo..."
|
|
||||||
|
|
||||||
# Clone Stegasoo first (needed for jpegio patch script)
|
|
||||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||||
echo " Stegasoo directory exists, updating..."
|
echo " Stegasoo directory exists, updating..."
|
||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
@@ -218,49 +181,166 @@ else
|
|||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}[6/12]${NC} Creating Python virtual environment..."
|
# Pre-built environment tarball (skips 20+ min compile time)
|
||||||
|
# Includes both pyenv Python 3.12 AND venv with all dependencies
|
||||||
|
PREBUILT_TARBALL="$INSTALL_DIR/rpi/stegasoo-pi-arm64.tar.zst"
|
||||||
|
PREBUILT_URL="${PREBUILT_URL:-https://github.com/adlee-was-taken/stegasoo/releases/download/v4.1.3/stegasoo-pi-arm64.tar.zst}"
|
||||||
|
USE_PREBUILT=true
|
||||||
|
|
||||||
# Create venv with pyenv Python (not system Python)
|
# Use local tarball if present, otherwise will download
|
||||||
# Use pyenv which to get actual path (handles 3.12 -> 3.12.12 mapping)
|
if [ -f "$PREBUILT_TARBALL" ]; then
|
||||||
PYENV_PYTHON=$(pyenv which python)
|
echo -e "${GREEN}Found local pre-built environment - fast install mode${NC}"
|
||||||
echo " Using Python: $PYENV_PYTHON"
|
|
||||||
if [ ! -d "venv" ]; then
|
|
||||||
"$PYENV_PYTHON" -m venv venv
|
|
||||||
fi
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Verify we're using the right Python
|
|
||||||
VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
|
||||||
echo " venv Python: $VENV_PY"
|
|
||||||
|
|
||||||
echo -e "${GREEN}[7/12]${NC} Building jpegio for ARM..."
|
|
||||||
|
|
||||||
# Clone jpegio
|
|
||||||
JPEGIO_DIR="/tmp/jpegio-build"
|
|
||||||
rm -rf "$JPEGIO_DIR"
|
|
||||||
git clone "$JPEGIO_REPO" "$JPEGIO_DIR"
|
|
||||||
|
|
||||||
# Apply ARM64 patch
|
|
||||||
if [ -f "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" ]; then
|
|
||||||
bash "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" "$JPEGIO_DIR"
|
|
||||||
else
|
else
|
||||||
echo " Applying inline ARM64 patch..."
|
echo -e "${GREEN}Will download pre-built environment - fast install mode${NC}"
|
||||||
sed -i "s/cargs.append('-m64')/pass # ARM64 fix/g" "$JPEGIO_DIR/setup.py"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$JPEGIO_DIR"
|
# Allow --no-prebuilt flag to force from-source build
|
||||||
|
if [[ " $* " =~ " --no-prebuilt " ]] || [[ " $* " =~ " --from-source " ]]; then
|
||||||
|
USE_PREBUILT=false
|
||||||
|
echo -e "${YELLOW}Building from source (--no-prebuilt specified)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Build jpegio into venv
|
# Fast path: use pre-built environment if available
|
||||||
pip install --upgrade pip setuptools wheel cython numpy
|
if [ "$USE_PREBUILT" = true ]; then
|
||||||
pip install .
|
echo -e "${GREEN}[5/8]${NC} Installing pre-built Python environment..."
|
||||||
|
|
||||||
cd "$INSTALL_DIR"
|
# Download if local file doesn't exist
|
||||||
rm -rf "$JPEGIO_DIR"
|
if [ ! -f "$PREBUILT_TARBALL" ]; then
|
||||||
|
echo " Downloading pre-built environment (~50MB)..."
|
||||||
|
curl -L -o "$PREBUILT_TARBALL" "$PREBUILT_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}[8/12]${NC} Installing Stegasoo..."
|
# Extract pre-built environment (includes pyenv Python + venv)
|
||||||
|
echo " Extracting pre-built environment..."
|
||||||
|
zstd -d "$PREBUILT_TARBALL" --stdout | tar -xf - -C "$HOME"
|
||||||
|
|
||||||
# Install dependencies (jpegio already in venv, won't re-download)
|
# Setup pyenv in current shell
|
||||||
pip install -e ".[web]"
|
export PYENV_ROOT="$HOME/.pyenv"
|
||||||
|
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||||
|
eval "$(pyenv init -)"
|
||||||
|
pyenv global $PYTHON_VERSION
|
||||||
|
|
||||||
|
# Add to .bashrc if not already there
|
||||||
|
if ! grep -q 'PYENV_ROOT' ~/.bashrc; then
|
||||||
|
echo '' >> ~/.bashrc
|
||||||
|
echo '# pyenv' >> ~/.bashrc
|
||||||
|
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
|
||||||
|
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
|
||||||
|
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify Python
|
||||||
|
INSTALLED_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
||||||
|
echo -e " ${GREEN}✓${NC} Python: $INSTALLED_PY"
|
||||||
|
|
||||||
|
# Extract venv to install dir
|
||||||
|
echo -e "${GREEN}[6/8]${NC} Setting up virtual environment..."
|
||||||
|
if [ -f "$HOME/stegasoo-venv.tar.zst" ]; then
|
||||||
|
zstd -d "$HOME/stegasoo-venv.tar.zst" --stdout | tar -xf - -C "$INSTALL_DIR"
|
||||||
|
rm "$HOME/stegasoo-venv.tar.zst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate and verify
|
||||||
|
source "$INSTALL_DIR/venv/bin/activate"
|
||||||
|
VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
||||||
|
echo -e " ${GREEN}✓${NC} venv Python: $VENV_PY"
|
||||||
|
|
||||||
|
# Install stegasoo package in editable mode (quick, no compile)
|
||||||
|
echo -e "${GREEN}[7/8]${NC} Installing Stegasoo package..."
|
||||||
|
pip install -e "." --quiet
|
||||||
|
|
||||||
|
# Adjust step numbers for rest of script
|
||||||
|
STEP_OFFSET=-4
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}[5/12]${NC} Installing pyenv and Python $PYTHON_VERSION..."
|
||||||
|
|
||||||
|
# Install pyenv if not present
|
||||||
|
if [ ! -d "$HOME/.pyenv" ]; then
|
||||||
|
curl https://pyenv.run | bash
|
||||||
|
|
||||||
|
# Add pyenv to current shell
|
||||||
|
export PYENV_ROOT="$HOME/.pyenv"
|
||||||
|
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||||
|
eval "$(pyenv init -)"
|
||||||
|
|
||||||
|
# Add to .bashrc if not already there
|
||||||
|
if ! grep -q 'PYENV_ROOT' ~/.bashrc; then
|
||||||
|
echo '' >> ~/.bashrc
|
||||||
|
echo '# pyenv' >> ~/.bashrc
|
||||||
|
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
|
||||||
|
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
|
||||||
|
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " pyenv already installed"
|
||||||
|
export PYENV_ROOT="$HOME/.pyenv"
|
||||||
|
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||||
|
eval "$(pyenv init -)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Python 3.12 if not present
|
||||||
|
if ! pyenv versions | grep -q "$PYTHON_VERSION"; then
|
||||||
|
echo " Building Python $PYTHON_VERSION (this takes ~10 minutes)..."
|
||||||
|
pyenv install $PYTHON_VERSION
|
||||||
|
else
|
||||||
|
echo " Python $PYTHON_VERSION already installed"
|
||||||
|
fi
|
||||||
|
pyenv global $PYTHON_VERSION
|
||||||
|
|
||||||
|
# Verify Python version
|
||||||
|
INSTALLED_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
||||||
|
if [ "$INSTALLED_PY" != "$PYTHON_VERSION" ]; then
|
||||||
|
echo -e "${RED}Error: Python $PYTHON_VERSION not active. Got: $INSTALLED_PY${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}[6/12]${NC} Creating Python virtual environment..."
|
||||||
|
echo -e " ${YELLOW}Note: No pre-built venv found. Building from source (20+ min)${NC}"
|
||||||
|
echo -e " ${YELLOW}To speed up future installs, add stegasoo-venv-pi-arm64.tar.gz to rpi/${NC}"
|
||||||
|
|
||||||
|
# Create venv with pyenv Python (not system Python)
|
||||||
|
# Use pyenv which to get actual path (handles 3.12 -> 3.12.12 mapping)
|
||||||
|
PYENV_PYTHON=$(pyenv which python)
|
||||||
|
echo " Using Python: $PYENV_PYTHON"
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
"$PYENV_PYTHON" -m venv venv
|
||||||
|
fi
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Verify we're using the right Python
|
||||||
|
VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
||||||
|
echo " venv Python: $VENV_PY"
|
||||||
|
|
||||||
|
echo -e "${GREEN}[7/12]${NC} Building jpegio for ARM..."
|
||||||
|
|
||||||
|
# Clone jpegio
|
||||||
|
JPEGIO_DIR="/tmp/jpegio-build"
|
||||||
|
rm -rf "$JPEGIO_DIR"
|
||||||
|
git clone "$JPEGIO_REPO" "$JPEGIO_DIR"
|
||||||
|
|
||||||
|
# Apply ARM64 patch
|
||||||
|
if [ -f "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" ]; then
|
||||||
|
bash "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" "$JPEGIO_DIR"
|
||||||
|
else
|
||||||
|
echo " Applying inline ARM64 patch..."
|
||||||
|
sed -i "s/cargs.append('-m64')/pass # ARM64 fix/g" "$JPEGIO_DIR/setup.py"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$JPEGIO_DIR"
|
||||||
|
|
||||||
|
# Build jpegio into venv
|
||||||
|
pip install --upgrade pip setuptools wheel cython numpy
|
||||||
|
pip install .
|
||||||
|
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
rm -rf "$JPEGIO_DIR"
|
||||||
|
|
||||||
|
echo -e "${GREEN}[8/12]${NC} Installing Stegasoo..."
|
||||||
|
|
||||||
|
# Install dependencies (jpegio already in venv, won't re-download)
|
||||||
|
pip install -e ".[web]"
|
||||||
|
|
||||||
|
STEP_OFFSET=0
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}[9/12]${NC} Creating systemd service..."
|
echo -e "${GREEN}[9/12]${NC} Creating systemd service..."
|
||||||
|
|
||||||
@@ -346,13 +426,14 @@ if systemctl is-active --quiet stegasoo 2>/dev/null; then
|
|||||||
echo -e "\033[38;5;93m══════════════\033[38;5;99m══════════════\033[38;5;105m══════════════\033[38;5;117m══════════════\033[0m"
|
echo -e "\033[38;5;93m══════════════\033[38;5;99m══════════════\033[38;5;105m══════════════\033[38;5;117m══════════════\033[0m"
|
||||||
echo -e " \033[0;32m●\033[0m Stegasoo is running"
|
echo -e " \033[0;32m●\033[0m Stegasoo is running"
|
||||||
echo -e " \033[0;33m$STEGASOO_URL\033[0m"
|
echo -e " \033[0;33m$STEGASOO_URL\033[0m"
|
||||||
# Show CPU stats if overclocked
|
# Show CPU stats if overclocked (read configured freq, not current idle freq)
|
||||||
if grep -qE "^(arm_freq|over_voltage)" /boot/firmware/config.txt 2>/dev/null || \
|
CONFIG_FILE=""
|
||||||
grep -qE "^(arm_freq|over_voltage)" /boot/config.txt 2>/dev/null; then
|
if [ -f /boot/firmware/config.txt ]; then CONFIG_FILE="/boot/firmware/config.txt"
|
||||||
CPU_FREQ=$(vcgencmd measure_clock arm 2>/dev/null | cut -d= -f2)
|
elif [ -f /boot/config.txt ]; then CONFIG_FILE="/boot/config.txt"; fi
|
||||||
|
if [ -n "$CONFIG_FILE" ] && grep -qE "^arm_freq=" "$CONFIG_FILE" 2>/dev/null; then
|
||||||
|
CPU_MHZ=$(grep "^arm_freq=" "$CONFIG_FILE" | cut -d= -f2)
|
||||||
CPU_TEMP=$(vcgencmd measure_temp 2>/dev/null | cut -d= -f2)
|
CPU_TEMP=$(vcgencmd measure_temp 2>/dev/null | cut -d= -f2)
|
||||||
if [ -n "$CPU_FREQ" ] && [ -n "$CPU_TEMP" ]; then
|
if [ -n "$CPU_MHZ" ] && [ -n "$CPU_TEMP" ]; then
|
||||||
CPU_MHZ=$((CPU_FREQ / 1000000))
|
|
||||||
echo -e " \033[0;35m⚡\033[0m ${CPU_MHZ} MHz \033[0;35m🌡\033[0m ${CPU_TEMP}"
|
echo -e " \033[0;35m⚡\033[0m ${CPU_MHZ} MHz \033[0;35m🌡\033[0m ${CPU_TEMP}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -465,6 +546,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..."
|
||||||
|
|||||||
@@ -120,7 +120,34 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 2. Unit Tests (if they exist)
|
# 2. Security Audit
|
||||||
|
# =============================================================================
|
||||||
|
section "Security Audit"
|
||||||
|
|
||||||
|
# pip-audit for known vulnerabilities
|
||||||
|
if command -v ./venv/bin/pip-audit &> /dev/null; then
|
||||||
|
echo -n "Running pip-audit... "
|
||||||
|
if ./venv/bin/pip-audit --quiet 2>/dev/null; then
|
||||||
|
pass "No known vulnerabilities"
|
||||||
|
else
|
||||||
|
fail "pip-audit found vulnerabilities (run: ./venv/bin/pip-audit)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -n "Installing pip-audit... "
|
||||||
|
if ./venv/bin/pip install pip-audit --quiet 2>/dev/null; then
|
||||||
|
echo -n "Running pip-audit... "
|
||||||
|
if ./venv/bin/pip-audit --quiet 2>/dev/null; then
|
||||||
|
pass "No known vulnerabilities"
|
||||||
|
else
|
||||||
|
fail "pip-audit found vulnerabilities (run: ./venv/bin/pip-audit)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
skip "Could not install pip-audit"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 3. Unit Tests (if they exist)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
section "Unit Tests"
|
section "Unit Tests"
|
||||||
|
|
||||||
@@ -136,7 +163,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 3. Import Tests
|
# 4. Import Tests
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
section "Import Tests"
|
section "Import Tests"
|
||||||
|
|
||||||
@@ -165,7 +192,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 4. Encode/Decode Sanity Test
|
# 5. Encode/Decode Sanity Test
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
section "Encode/Decode Test"
|
section "Encode/Decode Test"
|
||||||
|
|
||||||
@@ -205,7 +232,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 5. Docker Build & Test (optional)
|
# 6. Docker Build & Test (optional)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
if $INCLUDE_DOCKER; then
|
if $INCLUDE_DOCKER; then
|
||||||
section "Docker"
|
section "Docker"
|
||||||
@@ -248,7 +275,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 6. Pi Smoke Test (optional)
|
# 7. Pi Smoke Test (optional)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
if $INCLUDE_PI; then
|
if $INCLUDE_PI; then
|
||||||
section "Pi Smoke Test"
|
section "Pi Smoke Test"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -586,11 +586,113 @@ def generate(ctx, words, pin_length, channel_key):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@click.option("--full", is_flag=True, help="Show full system information (Pi stats)")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def info(ctx):
|
def info(ctx, full):
|
||||||
"""Show version and feature information."""
|
"""Show version, features, and system information."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Check for DCT support
|
||||||
|
try:
|
||||||
|
from .dct_steganography import HAS_SCIPY, HAS_JPEGIO
|
||||||
|
HAS_DCT = HAS_SCIPY and HAS_JPEGIO
|
||||||
|
except ImportError:
|
||||||
|
HAS_DCT = False
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
service_status = "unknown"
|
||||||
|
service_url = None
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["systemctl", "is-active", "stegasoo"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
service_status = result.stdout.strip()
|
||||||
|
if service_status == "active":
|
||||||
|
# Try to get URL from service environment
|
||||||
|
env_result = subprocess.run(
|
||||||
|
["systemctl", "show", "stegasoo", "--property=Environment"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
https_enabled = "HTTPS_ENABLED=true" in env_result.stdout
|
||||||
|
protocol = "https" if https_enabled else "http"
|
||||||
|
# Get IP
|
||||||
|
ip_result = subprocess.run(
|
||||||
|
["hostname", "-I"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
ip = ip_result.stdout.strip().split()[0] if ip_result.stdout.strip() else "localhost"
|
||||||
|
service_url = f"{protocol}://{ip}"
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check channel key
|
||||||
|
channel_fingerprint = None
|
||||||
|
channel_source = None
|
||||||
|
try:
|
||||||
|
from .channel import get_channel_key, get_channel_fingerprint, get_channel_status
|
||||||
|
key = get_channel_key()
|
||||||
|
if key:
|
||||||
|
channel_fingerprint = get_channel_fingerprint(key)
|
||||||
|
status = get_channel_status()
|
||||||
|
channel_source = status.get("source")
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# System info (Pi-specific)
|
||||||
|
cpu_freq = None
|
||||||
|
cpu_temp = None
|
||||||
|
disk_free = None
|
||||||
|
uptime = None
|
||||||
|
|
||||||
|
if full:
|
||||||
|
try:
|
||||||
|
# CPU frequency
|
||||||
|
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq") as f:
|
||||||
|
cpu_freq = int(f.read().strip()) // 1000 # MHz
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# CPU temp
|
||||||
|
with open("/sys/class/thermal/thermal_zone0/temp") as f:
|
||||||
|
cpu_temp = int(f.read().strip()) / 1000 # Celsius
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Disk free
|
||||||
|
st = os.statvfs("/")
|
||||||
|
disk_free = (st.f_bavail * st.f_frsize) / (1024 ** 3) # GB
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Uptime
|
||||||
|
with open("/proc/uptime") as f:
|
||||||
|
uptime_secs = float(f.read().split()[0])
|
||||||
|
days = int(uptime_secs // 86400)
|
||||||
|
hours = int((uptime_secs % 86400) // 3600)
|
||||||
|
uptime = f"{days}d {hours}h" if days else f"{hours}h"
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
info_data = {
|
info_data = {
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
|
"service": service_status,
|
||||||
|
"url": service_url,
|
||||||
|
"dct_support": HAS_DCT,
|
||||||
|
"channel": {
|
||||||
|
"fingerprint": channel_fingerprint,
|
||||||
|
"source": channel_source,
|
||||||
|
} if channel_fingerprint else None,
|
||||||
"compression": {
|
"compression": {
|
||||||
"available": [algorithm_name(a) for a in get_available_algorithms()],
|
"available": [algorithm_name(a) for a in get_available_algorithms()],
|
||||||
"lz4_installed": HAS_LZ4,
|
"lz4_installed": HAS_LZ4,
|
||||||
@@ -599,20 +701,54 @@ def info(ctx):
|
|||||||
"max_message_bytes": MAX_MESSAGE_SIZE,
|
"max_message_bytes": MAX_MESSAGE_SIZE,
|
||||||
"max_file_payload_bytes": MAX_FILE_PAYLOAD_SIZE,
|
"max_file_payload_bytes": MAX_FILE_PAYLOAD_SIZE,
|
||||||
},
|
},
|
||||||
|
"system": {
|
||||||
|
"cpu_mhz": cpu_freq,
|
||||||
|
"temp_c": cpu_temp,
|
||||||
|
"disk_free_gb": round(disk_free, 1) if disk_free else None,
|
||||||
|
"uptime": uptime,
|
||||||
|
} if full else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.obj.get("json"):
|
if ctx.obj.get("json"):
|
||||||
click.echo(json.dumps(info_data, indent=2))
|
click.echo(json.dumps(info_data, indent=2))
|
||||||
else:
|
else:
|
||||||
click.echo(f"Stegasoo v{__version__}")
|
# Fastfetch-style output
|
||||||
click.echo("\nCompression algorithms:")
|
click.echo(f"\033[1mSTEGASOO\033[0m v{__version__}")
|
||||||
for algo in get_available_algorithms():
|
click.echo("─" * 36)
|
||||||
click.echo(f" • {algorithm_name(algo)}")
|
|
||||||
if not HAS_LZ4:
|
# Service status
|
||||||
click.echo(" (install 'lz4' for LZ4 support)")
|
if service_status == "active":
|
||||||
click.echo("\nLimits:")
|
click.echo(f" Service: \033[32m● running\033[0m")
|
||||||
click.echo(f" • Max message: {MAX_MESSAGE_SIZE:,} bytes")
|
if service_url:
|
||||||
click.echo(f" • Max file payload: {MAX_FILE_PAYLOAD_SIZE:,} bytes")
|
click.echo(f" URL: {service_url}")
|
||||||
|
elif service_status == "inactive":
|
||||||
|
click.echo(f" Service: \033[31m○ stopped\033[0m")
|
||||||
|
else:
|
||||||
|
click.echo(f" Service: \033[33m? {service_status}\033[0m")
|
||||||
|
|
||||||
|
# Channel
|
||||||
|
if channel_fingerprint:
|
||||||
|
masked = f"{channel_fingerprint[:4]}••••••••{channel_fingerprint[-4:]}"
|
||||||
|
click.echo(f" Channel: {masked}")
|
||||||
|
else:
|
||||||
|
click.echo(f" Channel: \033[33mpublic\033[0m")
|
||||||
|
|
||||||
|
# DCT
|
||||||
|
dct_status = "\033[32m✓ enabled\033[0m" if HAS_DCT else "\033[31m✗ disabled\033[0m"
|
||||||
|
click.echo(f" DCT: {dct_status}")
|
||||||
|
|
||||||
|
# System info (if --full)
|
||||||
|
if full and any([cpu_freq, cpu_temp, disk_free, uptime]):
|
||||||
|
click.echo("─" * 36)
|
||||||
|
if cpu_freq:
|
||||||
|
click.echo(f" CPU: {cpu_freq} MHz")
|
||||||
|
if cpu_temp:
|
||||||
|
temp_color = "\033[32m" if cpu_temp < 60 else "\033[33m" if cpu_temp < 75 else "\033[31m"
|
||||||
|
click.echo(f" Temp: {temp_color}{cpu_temp:.1f}°C\033[0m")
|
||||||
|
if uptime:
|
||||||
|
click.echo(f" Uptime: {uptime}")
|
||||||
|
if disk_free:
|
||||||
|
click.echo(f" Disk: {disk_free:.1f} GB free")
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -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