diff --git a/README.md b/README.md index 1100df2..95d80e2 100644 --- a/README.md +++ b/README.md @@ -1 +1,117 @@ -# uploader \ No newline at end of file +# File Uploader + +A simple file uploader application that allows authenticated users to upload, list, and delete files. +The application uses PHP and a Python authentication service running on Apache2. + +## Prerequisites + +- Apache2, configured, up and running +- PHP 8.1 or higher +- Python 3 +- Required PHP extensions: `php-json`, `php-curl` +- PAM authentication for Python + +## Installation + +For simplicity, I'll use my current Ubuntu instance user name, you should replace by yours. + + ### Clone or download this repository + + ``` + git clone https://github.com/yourusername/uploader.git + cd uploader + ``` + + ### Install Python prerequisites + + ``` + pip install flask pam + ``` + + ### Create Python authentication service + (note: **port 7000** is used; if you need to change port number, make needful changes in the **app.py** and php scripts - search for '7000') + + ``` + sudo nano /etc/systemd/system/flaskapp.service + ``` + + Add the following content to this file (but replace **User**, **WorkingDirectory** and **ExecStart**): + + ``` + [Unit] + Description=Flask Application + After=network.target + + [Service] + User=ubuntu + WorkingDirectory=/home/ubuntu/uploader + ExecStart=/usr/bin/python3 /home/ubuntu/uploader/app.py + Restart=always + + [Install] + WantedBy=multi-user.target + ``` + + ### Enable and start the service: + ``` + sudo systemctl enable flaskapp + sudo systemctl start flaskapp + sudo systemctl status flaskapp.service + ``` + + ### Configure PHP + + Ensure the following PHP settings are in your **php.ini**: + ``` + log_errors = On + error_log = /var/log/php_errors.log + ``` + + Also check for max upload file/post size limits in **php.ini** (adjust to your needs, like 10G): + ``` + upload_max_filesize = 10M + post_max_size = 10M + ``` + + ### Create the upload directory and set the necessary permissions: + + ``` + sudo mkdir -p /var/www/html/upload + sudo chown -R www-data:www-data /var/www/html/upload + sudo chmod -R 755 /var/www/html/upload + ``` + + ### Create a limited user for uploading files + (please note, I don't recommend you to use your actual ssh-enabled user account): + + ``` + sudo useradd -M -d /var/www/html/upload -s /usr/sbin/nologin uploader + sudo passwd uploader + sudo chown -R uploader:www-data /var/www/html/upload + ``` + + ### Create application directory at webroot (or configure app/site): + (note: with my Apache configuration, I just need to create a subdirectory) + ``` + sudo mkdir -p /var/www/html/uploader + ``` + + ### Copy all files to the folder created above: + ``` + sudo cp -r * /var/www/html/uploader + ``` + + ### Restart Apache to apply changes: + + ``` + sudo systemctl restart apache2 + ``` + +## Usage +Open your web browser and navigate to https://yourserveraddress/uploader + +Enter your username and password to authenticate. + +Choose a file to upload and click the "Upload" button. + +The uploaded files will be listed on the page, and you can delete them using the "Delete" button. diff --git a/app.py b/app.py new file mode 100644 index 0000000..51e5322 --- /dev/null +++ b/app.py @@ -0,0 +1,25 @@ +from flask import Flask, request, jsonify +import subprocess + +app = Flask(__name__) + +def authenticate(username, password): + command = f"echo {password} | su -c 'whoami' {username}" + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True, check=True) + return result.stdout.strip() == username + except subprocess.CalledProcessError: + return False + +@app.route('/auth', methods=['POST']) +def auth(): + data = request.json + username = data.get('username') + password = data.get('password') + if authenticate(username, password): + return jsonify({"authenticated": True}), 200 + else: + return jsonify({"authenticated": False}), 401 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=7000) diff --git a/auth.js b/auth.js new file mode 100644 index 0000000..45a6b28 --- /dev/null +++ b/auth.js @@ -0,0 +1,42 @@ +let authCredentials = { username: '', password: '' }; + +async function authenticateUser(username, password) { + const response = await fetch('auth.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }); + const result = await response.json(); + return result.authenticated; +} + +function checkAuthentication() { + if (authCredentials.username && authCredentials.password) { + document.getElementById('loginForm').style.display = 'none'; + document.getElementById('uploadForm').style.display = 'block'; + loadFileList(); + } else { + document.getElementById('loginForm').style.display = 'block'; + document.getElementById('uploadForm').style.display = 'none'; + } +} + +document.getElementById('authForm').addEventListener('submit', async function(event) { + event.preventDefault(); + + const username = document.getElementById('loginUsername').value; + const password = document.getElementById('loginPassword').value; + + const isAuthenticated = await authenticateUser(username, password); + + if (isAuthenticated) { + authCredentials = { username, password }; + checkAuthentication(); + } else { + document.getElementById('statusMessage').innerHTML = 'Incorrect username or password!'; + } +}); + +document.addEventListener('DOMContentLoaded', checkAuthentication); diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..c9df7ec --- /dev/null +++ b/auth.php @@ -0,0 +1,35 @@ + $username, "password" => $password)); + $options = array( + 'http' => array( + 'header' => "Content-Type: application/json\r\n", + 'method' => 'POST', + 'content' => $data, + ), + ); + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + $response = json_decode($result, true); + return $response['authenticated']; +} + +$isAuthenticated = authenticate($username, $password); + +if ($isAuthenticated) { + $_SESSION['authenticated'] = true; + echo json_encode(['authenticated' => true]); +} else { + $_SESSION['authenticated'] = false; + echo json_encode(['authenticated' => false]); +} +?> diff --git a/config.php b/config.php new file mode 100644 index 0000000..69ffb3d --- /dev/null +++ b/config.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/displayFileList.js b/displayFileList.js new file mode 100644 index 0000000..35e1006 --- /dev/null +++ b/displayFileList.js @@ -0,0 +1,70 @@ +let sortDirection = {}; +let sortFunctions = { + 'File Name': (a, b) => a.name.localeCompare(b.name), + 'File Size': (a, b) => a.sizeBytes - b.sizeBytes, + 'File Time': (a, b) => new Date(a.modified) - new Date(b.modified), + 'Upload Time': (a, b) => new Date(a.uploaded) - new Date(b.uploaded) +}; + +function displayFileList(fileList) { + const fileListContainer = document.getElementById('fileList'); + fileListContainer.innerHTML = ''; + + const table = document.createElement('table'); + const headerRow = table.insertRow(); + + ['File Name', 'File Size', 'File Time', 'Upload Time', '', ''].forEach(headerText => { + const th = document.createElement('th'); + if (headerText && headerText !== '') { + const button = document.createElement('button'); + button.textContent = headerText; + button.onclick = () => sortTable(headerText, fileList); + button.style.background = 'none'; + button.style.border = 'none'; + button.style.cursor = 'pointer'; + th.appendChild(button); + } else { + th.textContent = headerText; + } + headerRow.appendChild(th); + }); + + fileList.forEach(file => { + const row = table.insertRow(); + row.insertCell().textContent = file.name; + row.insertCell().textContent = file.size; + row.insertCell().textContent = file.modified; + row.insertCell().textContent = file.uploaded; + + const linkCell = row.insertCell(); + const link = document.createElement('a'); + link.href = file.url; + link.textContent = 'Download'; + linkCell.appendChild(link); + + const deleteCell = row.insertCell(); + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'Delete'; + deleteButton.className = 'btn btn-delete'; + deleteButton.onclick = () => deleteFile(file.name); + deleteCell.appendChild(deleteButton); + }); + + fileListContainer.appendChild(table); +} + +function sortTable(column, fileList) { + if (!sortDirection[column]) { + sortDirection[column] = 'asc'; + } else { + sortDirection[column] = sortDirection[column] === 'asc' ? 'desc' : 'asc'; + } + + fileList.sort(sortFunctions[column]); + + if (sortDirection[column] === 'desc') { + fileList.reverse(); + } + + displayFileList(fileList); +} diff --git a/file_list.php b/file_list.php new file mode 100644 index 0000000..67b145a --- /dev/null +++ b/file_list.php @@ -0,0 +1,69 @@ + $username, "password" => $password)); + $options = array( + 'http' => array( + 'header' => "Content-Type: application/json\r\n", + 'method' => 'POST', + 'content' => $data, + ), + ); + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + $response = json_decode($result, true); + return $response['authenticated']; +} + +$data = json_decode(file_get_contents('php://input'), true); +$username = $data['username']; +$password = $data['password']; +$deleteFile = isset($data['delete']) ? basename($data['delete']) : null; + +if (!authenticate($username, $password)) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + exit; +} + +if ($deleteFile) { + $filePath = UPLOAD_DIR . $deleteFile; + if (file_exists($filePath)) { + unlink($filePath); + echo json_encode(['success' => 'File deleted']); + } else { + http_response_code(404); + echo json_encode(['error' => 'File not found']); + } + exit; +} + +$files = array_diff(scandir(UPLOAD_DIR), array('.', '..')); + +$fileList = []; +foreach ($files as $file) { + $filePath = UPLOAD_DIR . $file; + $fileSizeBytes = filesize($filePath); + $fileDate = date(DATE_TIME_FORMAT, filemtime($filePath)); + $uploadDate = date(DATE_TIME_FORMAT, filectime($filePath)); + $fileSizeFormatted = ($fileSizeBytes >= 1048576) ? sprintf("%.1f MB (%s bytes)", $fileSizeBytes / 1048576, number_format($fileSizeBytes)) : sprintf("%s bytes", number_format($fileSizeBytes)); + $fileUrl = BASE_URL . urlencode($file); + $fileList[] = [ + 'name' => htmlspecialchars($file, ENT_QUOTES, 'UTF-8'), + 'size' => $fileSizeFormatted, + 'sizeBytes' => $fileSizeBytes, + 'modified' => $fileDate, + 'uploaded' => $uploadDate, + 'url' => htmlspecialchars($fileUrl, ENT_QUOTES, 'UTF-8') + ]; +} + +usort($fileList, function($a, $b) { + return strtotime($b['uploaded']) - strtotime($a['uploaded']); +}); + +header('Content-Type: application/json'); +echo json_encode($fileList); +?> diff --git a/index.html b/index.html new file mode 100644 index 0000000..443b755 --- /dev/null +++ b/index.html @@ -0,0 +1,158 @@ + + +
+ + +