initial commit

This commit is contained in:
Sergey Svinolobov
2024-06-24 16:02:24 -04:00
parent eea090518d
commit 2ccb8f55fe
10 changed files with 681 additions and 1 deletions

118
README.md
View File

@@ -1 +1,117 @@
# uploader
# 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.

25
app.py Normal file
View File

@@ -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)

42
auth.js Normal file
View File

@@ -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);

35
auth.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$data = json_decode(file_get_contents('php://input'), true);
$username = $data['username'];
$password = $data['password'];
function authenticate($username, $password) {
$url = 'http://localhost:7000/auth';
$data = json_encode(array("username" => $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]);
}
?>

8
config.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
// config.php
define('UPLOAD_DIR', '/var/www/html/upload/');
define('BASE_URL', 'https://yoursite.com/upload/');
define('TIMEZONE', 'America/New_York');
define('DATE_TIME_FORMAT', 'm/d/y H:i');
date_default_timezone_set(TIMEZONE);
?>

70
displayFileList.js Normal file
View File

@@ -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);
}

69
file_list.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
require_once 'config.php';
function authenticate($username, $password) {
$url = 'http://localhost:7000/auth';
$data = json_encode(array("username" => $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);
?>

158
index.html Normal file
View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
margin-top: 10px;
}
.btn-upload {
background-color: #007bff;
color: white;
border-radius: 5px;
}
.btn-upload:disabled {
background-color: gray;
}
.btn-choose-file {
background-color: #6c757d;
color: white;
border-radius: 5px;
}
.file-list {
margin-top: 10px;
}
.progress {
margin-top: 10px;
height: 20px; /* Narrow progress bar */
width: 100%;
}
.progress-bar {
height: 100%; /* Fill the entire height */
}
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid black;
}
th, td {
padding: 10px;
text-align: left;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
h2 {
font-size: 1.5em; /* Smaller font size */
}
.form-group {
margin-bottom: 5px; /* Reduce vertical space between form groups */
}
label {
font-size: 0.9em; /* Smaller font size */
}
.btn {
font-size: 0.9em; /* Smaller font size for buttons */
}
.align-items-center {
display: flex;
align-items: center;
}
.table th button {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
}
#loginForm, #uploadForm {
display: none;
}
.form-row {
align-items: flex-start; /* Align items by top */
}
.full-width {
width: 100%;
}
.btn-delete {
background-color: #dc3545;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 5px;
}
.btn-delete:hover {
background-color: #c82333;
}
</style>
</head>
<body>
<div class="container">
<div class="row" id="loginForm">
<div class="col-md-6">
<form id="authForm" method="post">
<div class="form-row align-items-center">
<div class="form-group col-4 col-md-4">
<label for="loginUsername">User:</label>
<input type="text" class="form-control" id="loginUsername" name="username" required>
</div>
<div class="form-group col-4 col-md-4">
<label for="loginPassword">Password:</label>
<input type="password" class="form-control" id="loginPassword" name="password" required>
</div>
<div class="form-group col-4 col-md-4 align-items-center" style="padding-top: 26px;">
<button type="submit" class="btn btn-upload btn-block" style="transform: translateY(2px);">Login</button>
</div>
</div>
</form>
</div>
</div>
<div class="row" id="uploadForm">
<div class="col-md-12">
<form id="uploadFileForm" method="post" enctype="multipart/form-data">
<div class="form-row align-items-center">
<div class="form-group col-4 col-md-4" style="transform: translateY(4px);">
<label class="btn btn-choose-file btn-block">
Choose File <input type="file" id="file" name="file" required hidden>
</label>
</div>
<div class="form-group col-4 col-md-4">
<input type="submit" value="Upload" id="uploadBtn" class="btn btn-upload btn-block" disabled>
</div>
</div>
<div class="form-row">
<div class="form-group col-12 full-width">
<span id="fileName"></span>
</div>
</div>
<div class="form-row">
<div class="form-group col-12 full-width">
<div id="statusMessage" class="ml-0"></div>
</div>
</div>
<div class="form-row" id="progressRow" style="display: none;">
<div class="form-group col-12">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%;" id="progressBar"></div>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="file-list">
<div id="fileList" class="full-width"></div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="auth.js"></script>
<script src="upload.js"></script>
<script src="displayFileList.js"></script>
</body>
</html>

120
upload.js Normal file
View File

@@ -0,0 +1,120 @@
document.getElementById('file').addEventListener('change', function() {
const fileInput = document.getElementById('file');
const uploadBtn = document.getElementById('uploadBtn');
if (fileInput.files.length > 0) {
document.getElementById('fileName').innerHTML = `<b>${fileInput.files[0].name}</b>`;
uploadBtn.disabled = false;
} else {
document.getElementById('fileName').innerHTML = '';
uploadBtn.disabled = true;
}
});
document.getElementById('uploadFileForm').addEventListener('submit', async function(event) {
event.preventDefault();
const { username, password } = authCredentials;
const fileInput = document.getElementById('file');
const statusMessage = document.getElementById('statusMessage');
const progressBar = document.getElementById('progressBar');
const progressRow = document.getElementById('progressRow');
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('username', username);
formData.append('password', password);
const fileDateTime = file.lastModified;
formData.append('fileDateTime', fileDateTime);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'upload.php', true);
const uploadBtn = document.getElementById('uploadBtn');
const fileBtn = document.getElementById('file');
uploadBtn.disabled = true;
fileBtn.disabled = true;
const startTime = Date.now();
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
progressBar.innerText = Math.round(percentComplete) + '%';
progressRow.style.display = 'block'; // Show the progress bar
}
};
xhr.onload = function() {
const endTime = Date.now();
const uploadTime = (endTime - startTime) / 1000;
const fileSize = fileInput.files[0].size;
const uploadRate = (fileSize / 1024 / uploadTime).toFixed(2);
if (xhr.status === 200) {
statusMessage.innerHTML = `File <b>${fileInput.files[0].name}</b> successfully uploaded. Upload time: <b>${uploadTime.toFixed(2)}</b> seconds. Upload rate: <b>${uploadRate}</b> KBps.`;
loadFileList();
} else {
statusMessage.innerHTML = 'Upload failed!';
}
progressBar.style.width = '0%';
progressBar.innerText = '';
progressRow.style.display = 'none'; // Hide the progress bar
document.getElementById('fileName').innerHTML = '';
uploadBtn.disabled = true;
fileBtn.disabled = false;
fileBtn.value = '';
};
statusMessage.innerHTML = `Uploading file ${fileInput.files[0].name}...`;
xhr.send(formData);
}
});
async function loadFileList() {
try {
const { username, password } = authCredentials;
console.log('Loading file list with credentials:', { username, password }); // Debugging
const response = await fetch('file_list.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fileList = await response.json();
console.log('File list loaded:', fileList); // Debugging: Log the file list to the console
displayFileList(fileList);
} catch (error) {
console.error('Error loading file list:', error); // Debugging: Log any errors to the console
}
}
async function deleteFile(fileName) {
const { username, password } = authCredentials;
try {
const response = await fetch('file_list.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password, delete: fileName }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
loadFileList();
} else {
console.error('Error deleting file:', result.error);
}
} catch (error) {
console.error('Error deleting file:', error);
}
}

37
upload.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
require_once 'config.php';
require_once 'auth.php';
// Check if the form was submitted
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Get the username and password
$username = $_POST['username'];
$password = $_POST['password'];
$fileDateTime = isset($_POST['fileDateTime']) ? (int)$_POST['fileDateTime'] / 1000 : time();
// Validate the credentials using the Flask backend
if (authenticate($username, $password)) {
// Check if a file was uploaded
if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) {
$uploadFile = UPLOAD_DIR . basename($_FILES['file']['name']);
$tmpFile = $_FILES['file']['tmp_name'];
// Move the uploaded file to the specified directory
if (move_uploaded_file($tmpFile, $uploadFile)) {
// Preserve the original file modification time
touch($uploadFile, $fileDateTime);
echo "File is valid, and was successfully uploaded.\n";
} else {
echo "File upload failed! ";
print_r(error_get_last());
}
} else {
echo "No file uploaded or file upload error!\n";
echo "Error code: " . $_FILES['file']['error'];
}
} else {
echo "Invalid username or password!\n";
}
} else {
echo "Invalid request method!\n";
}