Added auth support and a helper script, targeting frankenphp
This commit is contained in:
parent
8f545c3f67
commit
0783e08fe9
8 changed files with 655 additions and 4 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.env
|
||||
50
auth.php
Normal file
50
auth.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
// auth.php
|
||||
declare(strict_types=1);
|
||||
|
||||
function auth_session_start(): void
|
||||
{
|
||||
//$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a valid login.
|
||||
* IMPORTANT: releases the session lock immediately to avoid deadlocks/timeouts.
|
||||
*/
|
||||
function require_auth(bool $json = true): void
|
||||
{
|
||||
auth_session_start();
|
||||
|
||||
$ok = (isset($_SESSION['authed']) && $_SESSION['authed'] === true);
|
||||
|
||||
// Release the session file lock ASAP.
|
||||
// This prevents other requests from blocking on session_start().
|
||||
session_write_close();
|
||||
|
||||
if (!$ok) {
|
||||
http_response_code(401);
|
||||
|
||||
if ($json) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
} else {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo "Unauthorized";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2,8 +2,11 @@
|
|||
/**
|
||||
* get_files.php - Recursively scans media directories and returns file structure as JSON
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_auth(true);
|
||||
|
||||
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
|
|
|
|||
374
index.php
374
index.php
|
|
@ -342,6 +342,49 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.player-controls-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.audio-track-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
|
||||
.audio-track-select {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
min-width: 220px;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
}
|
||||
|
||||
.audio-track-select:focus {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.audio-track-hint {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.5rem;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
|
|
@ -352,6 +395,119 @@
|
|||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
|
||||
padding: 1.75rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.4rem;
|
||||
color: var(--accent-primary);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.login-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.login-row label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
|
||||
.login-row input {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: 0.75rem 0.85rem;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
}
|
||||
|
||||
.login-row input:focus {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--bg-primary);
|
||||
font-weight: 700;
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: #ff6b6b;
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
|
|
@ -392,12 +548,22 @@
|
|||
</aside>
|
||||
|
||||
<main class="player-section">
|
||||
<div class="topbar">
|
||||
<button class="btn" id="logoutBtn" style="display:none;">Logout</button>
|
||||
</div>
|
||||
|
||||
<div class="player-container" id="playerContainer">
|
||||
<div class="player-placeholder">
|
||||
<div class="player-placeholder-icon">🎵</div>
|
||||
<p>Select a file to start playing</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-controls-row" id="audioControls" style="display:none;">
|
||||
<div class="audio-track-label">Audio Track</div>
|
||||
<select id="audioSelect" class="audio-track-select"></select>
|
||||
</div>
|
||||
<div class="audio-track-hint" id="audioHint" style="display:none;"></div>
|
||||
|
||||
<div class="now-playing" id="nowPlaying" style="display: none;">
|
||||
<div class="now-playing-label">Now Playing</div>
|
||||
<div class="now-playing-title" id="nowPlayingTitle"></div>
|
||||
|
|
@ -406,7 +572,70 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="loginOverlay" style="display:none;">
|
||||
<div class="login-card">
|
||||
<div class="login-title">Login</div>
|
||||
<div class="login-subtitle">Sign in to access your media library.</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="login-row">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" autocomplete="username" required />
|
||||
</div>
|
||||
|
||||
<div class="login-row">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required />
|
||||
</div>
|
||||
|
||||
<div class="login-actions">
|
||||
<button class="btn btn-primary" type="submit" id="loginBtn">Login</button>
|
||||
<span id="loginSpinner" class="loading" style="display:none;"></span>
|
||||
</div>
|
||||
|
||||
<div class="login-error" id="loginError"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
function showLogin(message = '') {
|
||||
document.getElementById('loginOverlay').style.display = 'flex';
|
||||
document.getElementById('logoutBtn').style.display = 'none';
|
||||
|
||||
const err = document.getElementById('loginError');
|
||||
if (message) {
|
||||
err.textContent = message;
|
||||
err.style.display = 'block';
|
||||
} else {
|
||||
err.textContent = '';
|
||||
err.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function hideLogin() {
|
||||
document.getElementById('loginOverlay').style.display = 'none';
|
||||
document.getElementById('loginError').style.display = 'none';
|
||||
document.getElementById('logoutBtn').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
async function authedFetch(url, options = {}) {
|
||||
// Ensure cookies (session) are sent
|
||||
const resp = await fetch(url, {
|
||||
...options,
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
if (resp.status === 401) {
|
||||
// Session missing/expired
|
||||
showLogin('Please log in to continue.');
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
const state = {
|
||||
currentType: 'videos',
|
||||
currentFile: null,
|
||||
|
|
@ -415,10 +644,77 @@
|
|||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadFiles('videos');
|
||||
setupTabListeners();
|
||||
setupLoginHandlers();
|
||||
|
||||
// Try loading files; if 401, overlay will appear.
|
||||
loadFiles('videos');
|
||||
});
|
||||
|
||||
function setupLoginHandlers() {
|
||||
const form = document.getElementById('loginForm');
|
||||
const spinner = document.getElementById('loginSpinner');
|
||||
const err = document.getElementById('loginError');
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
|
||||
// Show overlay until proven authed
|
||||
showLogin('');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
err.style.display = 'none';
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const resp = await fetch('login.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(data.error || `Login failed (${resp.status})`);
|
||||
}
|
||||
|
||||
hideLogin();
|
||||
|
||||
// Reload file browser after login
|
||||
loadFiles(state.currentType);
|
||||
} catch (ex) {
|
||||
err.textContent = ex.message || 'Login failed';
|
||||
err.style.display = 'block';
|
||||
} finally {
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
logoutBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('logout.php', { method: 'POST', credentials: 'same-origin' });
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
showLogin('Logged out.');
|
||||
// Clear UI
|
||||
document.getElementById('fileBrowser').innerHTML = '';
|
||||
document.getElementById('playerContainer').innerHTML = `
|
||||
<div class="player-placeholder">
|
||||
<div class="player-placeholder-icon">🎵</div>
|
||||
<p>Select a file to start playing</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('nowPlaying').style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setupTabListeners() {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
|
|
@ -436,7 +732,7 @@
|
|||
browser.innerHTML = '<div class="loading"></div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`get_files.php?type=${type}`);
|
||||
const response = await authedFetch(`get_files.php?type=${type}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
|
@ -516,18 +812,92 @@
|
|||
}
|
||||
}
|
||||
|
||||
function resetAudioTrackUI() {
|
||||
const audioControls = document.getElementById('audioControls');
|
||||
const audioSelect = document.getElementById('audioSelect');
|
||||
const audioHint = document.getElementById('audioHint');
|
||||
|
||||
audioControls.style.display = 'none';
|
||||
audioHint.style.display = 'none';
|
||||
audioHint.textContent = '';
|
||||
audioSelect.innerHTML = '';
|
||||
}
|
||||
|
||||
function setupAudioTrackSelector(videoEl) {
|
||||
const audioControls = document.getElementById('audioControls');
|
||||
const audioSelect = document.getElementById('audioSelect');
|
||||
const audioHint = document.getElementById('audioHint');
|
||||
|
||||
// Not all browsers expose audioTracks (and MKV often won't)
|
||||
const tracks = videoEl.audioTracks;
|
||||
|
||||
if (!tracks) {
|
||||
audioControls.style.display = 'none';
|
||||
audioHint.style.display = 'block';
|
||||
audioHint.textContent = 'Audio track switching not supported for this file/browser, open in VLC to change audio track if needed.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (tracks.length <= 1) {
|
||||
audioControls.style.display = 'none';
|
||||
audioHint.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build dropdown
|
||||
audioSelect.innerHTML = '';
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const t = tracks[i];
|
||||
const opt = document.createElement('option');
|
||||
opt.value = String(i);
|
||||
|
||||
// label is often empty; language may exist depending on browser/container
|
||||
const label = (t.label && t.label.trim()) ? t.label.trim() : null;
|
||||
const lang = (t.language && t.language.trim()) ? t.language.trim() : null;
|
||||
|
||||
opt.textContent = label || (lang ? `Track ${i + 1} (${lang})` : `Track ${i + 1}`);
|
||||
audioSelect.appendChild(opt);
|
||||
|
||||
// Set currently enabled track as selected
|
||||
if (t.enabled) {
|
||||
audioSelect.value = String(i);
|
||||
}
|
||||
}
|
||||
|
||||
audioSelect.onchange = () => {
|
||||
const idx = parseInt(audioSelect.value, 10);
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
tracks[i].enabled = (i === idx);
|
||||
}
|
||||
};
|
||||
|
||||
audioHint.style.display = 'none';
|
||||
audioControls.style.display = 'flex';
|
||||
}
|
||||
|
||||
|
||||
function playFile(path, name) {
|
||||
const playerContainer = document.getElementById('playerContainer');
|
||||
const nowPlaying = document.getElementById('nowPlaying');
|
||||
const nowPlayingTitle = document.getElementById('nowPlayingTitle');
|
||||
|
||||
state.currentFile = { path, name };
|
||||
resetAudioTrackUI();
|
||||
|
||||
|
||||
const isVideo = state.currentType === 'videos';
|
||||
const mediaElement = document.createElement(isVideo ? 'video' : 'audio');
|
||||
|
||||
mediaElement.controls = true;
|
||||
mediaElement.autoplay = true;
|
||||
|
||||
if (isVideo) {
|
||||
// Some browsers only populate audioTracks after metadata is available
|
||||
mediaElement.addEventListener('loadedmetadata', () => {
|
||||
setupAudioTrackSelector(mediaElement);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Add error handling
|
||||
mediaElement.addEventListener('error', (e) => {
|
||||
|
|
|
|||
60
login.php
Normal file
60
login.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
// login.php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
auth_session_start();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read JSON body
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw ?: '', true);
|
||||
|
||||
$username = is_array($data) ? (string)($data['username'] ?? '') : '';
|
||||
$password = is_array($data) ? (string)($data['password'] ?? '') : '';
|
||||
|
||||
if ($username === '' || $password === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing username or password']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// === Simple credential check ===
|
||||
// Recommended: set these via environment variables
|
||||
$expectedUser = getenv('MEDIA_USER') ?: 'admin';
|
||||
// Store bcrypt hash in env: MEDIA_PASS_HASH="$2y$10$..."
|
||||
$expectedHash = getenv('MEDIA_PASS_HASH');
|
||||
|
||||
// If no hash provided, fall back to a plain password for quick LAN-only testing.
|
||||
// Strongly recommend switching to MEDIA_PASS_HASH once you verify flow works.
|
||||
$plainFallbackPass = getenv('MEDIA_PASS_PLAIN') ?: 'changeme';
|
||||
|
||||
$ok = false;
|
||||
if ($expectedHash) {
|
||||
$ok = hash_equals($expectedUser, $username) && password_verify($password, $expectedHash);
|
||||
} else {
|
||||
$ok = hash_equals($expectedUser, $username) && hash_equals($plainFallbackPass, $password);
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
// Slight delay makes brute forcing annoying
|
||||
usleep(250000);
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Invalid credentials']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Success: mark session authed, rotate session id
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['authed'] = true;
|
||||
$_SESSION['user'] = $username;
|
||||
|
||||
echo json_encode(['ok' => true, 'user' => $username]);
|
||||
|
||||
20
logout.php
Normal file
20
logout.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
// logout.php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
auth_session_start();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$_SESSION = [];
|
||||
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
|
||||
|
|
@ -2,8 +2,11 @@
|
|||
/**
|
||||
* serve_media.php - Serves media files with proper headers for streaming + Range
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_auth(false); // plain text is fine for media endpoint
|
||||
set_time_limit(0);
|
||||
|
||||
|
||||
// ---- Config (prefer env vars in production) ----
|
||||
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user()));
|
||||
|
|
|
|||
144
start-server.sh
Executable file
144
start-server.sh
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# start-media-server.sh
|
||||
#
|
||||
# Starts frankenphp with MEDIA_USER and MEDIA_PASS_HASH set.
|
||||
# Hash is generated via:
|
||||
# - system php (preferred) OR
|
||||
# - frankenphp php-cli (fallback)
|
||||
#
|
||||
# Usage:
|
||||
# ./start-media-server.sh --user admin --prompt [--port 9000] [--dir /path/to/docroot]
|
||||
#
|
||||
# Security:
|
||||
# Avoid --pass on command line; use --prompt or --pass-stdin to keep it out of `ps`/history.
|
||||
|
||||
PORT="9000"
|
||||
USER=""
|
||||
PASS=""
|
||||
DOCROOT=""
|
||||
|
||||
read_pass_stdin=false
|
||||
prompt=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
start-media-server.sh --user <username> [--pass <password> | --pass-stdin | --prompt] [--port <port>] [--dir <docroot>]
|
||||
|
||||
Options:
|
||||
--user <u> Username to set in MEDIA_USER (required)
|
||||
--pass <p> Password to hash (insecure: visible in process list & shell history)
|
||||
--pass-stdin Read password from stdin (safer)
|
||||
--prompt Prompt for password (safer; hidden input)
|
||||
--port <p> Listen port (default: 9000)
|
||||
--dir <path> cd into docroot before starting (optional)
|
||||
-h, --help Show help
|
||||
|
||||
Examples:
|
||||
./start-media-server.sh --user admin --prompt --port 9000 --dir /home/me/samba-serv
|
||||
printf '%s\n' 'test123' | ./start-media-server.sh --user admin --pass-stdin
|
||||
EOF
|
||||
}
|
||||
|
||||
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# Pick a PHP hashing command:
|
||||
# - If `php` exists: use it.
|
||||
# - Else if `frankenphp` exists and supports php-cli: use it.
|
||||
pick_php_runner() {
|
||||
if have_cmd php; then
|
||||
echo "php"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if have_cmd frankenphp; then
|
||||
# Verify php-cli is available (frankenphp subcommand)
|
||||
if frankenphp php-cli -v >/dev/null 2>&1; then
|
||||
echo "frankenphp php-cli"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--user) USER="${2-}"; shift 2 ;;
|
||||
--pass) PASS="${2-}"; shift 2 ;;
|
||||
--pass-stdin) read_pass_stdin=true; shift ;;
|
||||
--prompt) prompt=true; shift ;;
|
||||
--port) PORT="${2-}"; shift 2 ;;
|
||||
--dir) DOCROOT="${2-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$USER" ]]; then
|
||||
echo "Error: --user is required" >&2
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if $read_pass_stdin && $prompt; then
|
||||
echo "Error: choose only one of --pass-stdin or --prompt" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -z "$PASS" ]]; then
|
||||
if $read_pass_stdin; then
|
||||
IFS= read -r PASS
|
||||
elif $prompt; then
|
||||
read -r -s -p "Password: " PASS
|
||||
echo
|
||||
else
|
||||
echo "Error: provide --pass, --pass-stdin, or --prompt" >&2
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! [[ "$PORT" =~ ^[0-9]+$ ]] || ((PORT < 1 || PORT > 65535)); then
|
||||
echo "Error: invalid --port '$PORT'" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! have_cmd frankenphp; then
|
||||
echo "Error: frankenphp not found in PATH" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
PHP_RUNNER="$(pick_php_runner)" || {
|
||||
echo "Error: neither 'php' nor 'frankenphp php-cli' is available for hashing" >&2
|
||||
echo "Install php OR ensure frankenphp supports 'php-cli'." >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
# Generate bcrypt hash without needing to escape $.
|
||||
# We pass the password via an env var P to avoid quoting surprises in -r strings.
|
||||
MEDIA_PASS_HASH="$(
|
||||
P="$PASS" $PHP_RUNNER -r 'echo password_hash(getenv("P"), PASSWORD_BCRYPT), PHP_EOL;'
|
||||
)"
|
||||
|
||||
export MEDIA_USER="$USER"
|
||||
export MEDIA_PASS_HASH
|
||||
|
||||
if [[ -n "$DOCROOT" ]]; then
|
||||
cd "$DOCROOT"
|
||||
fi
|
||||
|
||||
echo "Using PHP runner: $PHP_RUNNER"
|
||||
echo "Starting frankenphp on :$PORT"
|
||||
echo "MEDIA_USER=$MEDIA_USER"
|
||||
echo "MEDIA_PASS_HASH set (bcrypt)"
|
||||
echo
|
||||
|
||||
exec frankenphp php-server --listen ":$PORT"
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue