first commit

This commit is contained in:
Lain Iwakura 2025-07-24 05:15:35 +03:00
commit 27c3f1662d
No known key found for this signature in database
GPG Key ID: C7C18257F2ADC6F8
7 changed files with 554 additions and 0 deletions

22
README Normal file
View File

@ -0,0 +1,22 @@
mkach - анонимный имиджборд
Установка:
1. Создайте базу данных MySQL
2. Импортируйте sql/create.sql
3. Настройте config.php
4. Создайте папку uploads/ с правами 755
Особенности:
- Анонимные посты без регистрации
- Поддержка изображений (jpg, png, gif, webp)
- Rate limiting на основе IP
- Стиль 4chan/2ch
- Ключ доступа для входа
- Автообновление постов
Безопасность:
- Валидация файлов
- Ограничение размера
- Защита от XSS
- Rate limiting
- Безопасная загрузка файлов

39
RateLimiter.php Normal file
View File

@ -0,0 +1,39 @@
<?php
class RateLimiter {
private $db;
private $maxRequests = 3;
private $timeWindow = 30;
public function __construct($db) {
$this->db = $db;
}
public function isAllowed($ip, $action = 'post') {
$stmt = $this->db->prepare('
SELECT COUNT(*) FROM rate_limits
WHERE ip_address = ? AND action_type = ?
AND created_at > DATE_SUB(NOW(), INTERVAL ? SECOND)
');
$stmt->execute([$ip, $action, $this->timeWindow]);
$count = $stmt->fetchColumn();
if ($count >= $this->maxRequests) {
return false;
}
$stmt = $this->db->prepare('
INSERT INTO rate_limits (ip_address, action_type) VALUES (?, ?)
');
$stmt->execute([$ip, $action]);
return true;
}
public function cleanup() {
$stmt = $this->db->prepare('
DELETE FROM rate_limits
WHERE created_at < DATE_SUB(NOW(), INTERVAL ? SECOND)
');
$stmt->execute([$this->timeWindow]);
}
}

187
board.php Normal file
View File

@ -0,0 +1,187 @@
<?php
session_start();
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
if (!isset($_SESSION['authenticated'])) {
header('Location: index.php');
exit;
}
$config = require 'config.php';
try {
$db = new PDO(
"mysql:host={$config['db']['host']};dbname={$config['db']['name']}",
$config['db']['user'],
$config['db']['pass']
);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die('Connection failed');
}
require_once 'RateLimiter.php';
$rateLimiter = new RateLimiter($db);
$rateLimiter->cleanup();
$ip = $_SERVER['REMOTE_ADDR'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$rateLimiter->isAllowed($ip)) {
$error = 'Слишком много запросов';
} else {
$message = trim($_POST['message'] ?? '');
$file = $_FILES['file'] ?? null;
if ($message || ($file && $file['error'] === UPLOAD_ERR_OK)) {
$postId = sprintf('%06d', mt_rand(1, 999999));
$fileName = null;
$fileSize = null;
$fileType = null;
if ($file && $file['error'] === UPLOAD_ERR_OK) {
$fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($fileExt, $config['allowed_types'])) {
$error = 'Неподдерживаемый тип файла';
} elseif ($file['size'] > $config['max_file_size']) {
$error = 'Файл слишком большой';
} else {
$uploadDir = $config['upload_path'];
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$fileName = $postId . '.' . $fileExt;
$filePath = $uploadDir . $fileName;
if (move_uploaded_file($file['tmp_name'], $filePath)) {
$fileSize = $file['size'];
$fileType = $fileExt;
} else {
$error = 'Ошибка загрузки файла';
}
}
}
if (empty($error)) {
$stmt = $db->prepare('
INSERT INTO posts (post_id, message, file_name, file_size, file_type, ip_address)
VALUES (?, ?, ?, ?, ?, ?)
');
$stmt->execute([$postId, $message, $fileName, $fileSize, $fileType, $ip]);
header('Location: board.php');
exit;
}
} else {
$error = 'Введите сообщение или загрузите файл';
}
}
}
if (isset($_GET['logout'])) {
session_destroy();
header('Location: index.php');
exit;
}
try {
$stmt = $db->query('SELECT * FROM posts ORDER BY created_at DESC LIMIT 100');
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die('Database error');
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mkach</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>mkach</h1>
<div class="header-buttons">
<a href="?logout=1" class="logout-btn">Выход</a>
</div>
</div>
<div class="board-container">
<?php if (!empty($error)): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<div class="posts-container" id="posts">
<?php foreach ($posts as $post): ?>
<div class="post">
<div class="post-header">
<span class="post-id"><?= htmlspecialchars($post['post_id']) ?></span>
<span class="post-time"><?= date('d.m.Y H:i:s', strtotime($post['created_at'])) ?></span>
</div>
<?php if ($post['file_name']): ?>
<div class="post-file">
<a href="uploads/<?= htmlspecialchars($post['file_name']) ?>" target="_blank">
<img src="uploads/<?= htmlspecialchars($post['file_name']) ?>" alt="File" class="post-image">
</a>
<div class="file-info">
<?= htmlspecialchars($post['file_name']) ?>
(<?= number_format($post['file_size'] / 1024, 1) ?> KB)
</div>
</div>
<?php endif; ?>
<?php if ($post['message']): ?>
<div class="post-message"><?= nl2br(htmlspecialchars($post['message'])) ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="post-form">
<form method="post" enctype="multipart/form-data">
<div class="form-row">
<textarea name="message" placeholder="Сообщение (необязательно)" class="message-input"></textarea>
</div>
<div class="form-row">
<input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.webp" class="file-input">
<button type="submit" class="send-btn">Отправить</button>
</div>
</form>
</div>
</div>
<script>
let isScrolledToBottom = true;
const postsContainer = document.getElementById('posts');
postsContainer.addEventListener('scroll', () => {
const { scrollTop, scrollHeight, clientHeight } = postsContainer;
isScrolledToBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 1;
});
setInterval(() => {
fetch(window.location.href)
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const oldHeight = postsContainer.scrollHeight;
postsContainer.innerHTML = doc.getElementById('posts').innerHTML;
if (isScrolledToBottom) {
postsContainer.scrollTop = postsContainer.scrollHeight;
}
});
}, 10000);
</script>
</body>
</html>

13
config.php Normal file
View File

@ -0,0 +1,13 @@
<?php
return [
'db' => [
'host' => 'localhost',
'name' => 'mkach',
'user' => 'mkach',
'pass' => 'your_password'
],
'access_key' => 'mkalwaysthebest1337',
'upload_path' => 'uploads/',
'max_file_size' => 5242880,
'allowed_types' => ['jpg', 'jpeg', 'png', 'gif', 'webp']
];

103
index.php Normal file
View File

@ -0,0 +1,103 @@
<?php
session_start();
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
$config = require 'config.php';
if (!isset($_SESSION['authenticated'])) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$key = $_POST['access_key'] ?? '';
if (hash('sha256', $key) === hash('sha256', $config['access_key'])) {
$_SESSION['authenticated'] = true;
header('Location: board.php');
exit;
} else {
$error = 'Неверный ключ';
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mkach</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
color: #0f0;
font-family: 'Courier New', monospace;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-size: 16px;
}
.container {
text-align: center;
background: rgba(0,20,0,0.8);
padding: 40px;
border: 1px solid #0f0;
border-radius: 5px;
box-shadow: 0 0 20px rgba(0,255,0,0.3);
}
h1 { margin-bottom: 30px; font-size: 2.5em; }
.form-group { margin-bottom: 20px; }
input[type="password"] {
background: #000;
border: 1px solid #0f0;
color: #0f0;
padding: 15px;
font-size: 18px;
width: 300px;
text-align: center;
font-family: 'Courier New', monospace;
}
input[type="password"]:focus {
outline: none;
box-shadow: 0 0 10px rgba(0,255,0,0.5);
}
button {
background: #000;
border: 1px solid #0f0;
color: #0f0;
padding: 15px 30px;
font-size: 16px;
cursor: pointer;
font-family: 'Courier New', monospace;
transition: all 0.3s;
}
button:hover {
background: #0f0;
color: #000;
}
.error {
color: #f00;
margin-top: 15px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>mkach</h1>
<form method="post">
<div class="form-group">
<input type="password" name="access_key" placeholder="Введите ключ доступа" required>
</div>
<button type="submit">Войти</button>
</form>
<?php if (isset($error)): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
</div>
</body>
</html>
<?php
exit;
}
header('Location: board.php');

21
sql/create.sql Normal file
View File

@ -0,0 +1,21 @@
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id VARCHAR(6) NOT NULL UNIQUE,
message TEXT,
file_name VARCHAR(255),
file_size INT,
file_type VARCHAR(10),
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_post_id (post_id),
INDEX idx_created_at (created_at)
);
CREATE TABLE rate_limits (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(45) NOT NULL,
action_type VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_action (ip_address, action_type),
INDEX idx_created_at (created_at)
);

169
styles.css Normal file
View File

@ -0,0 +1,169 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f0e0d6;
font-family: 'Arial', sans-serif;
font-size: 14px;
line-height: 1.4;
color: #800000;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 10px;
}
.header {
background: #efefef;
border: 1px solid #b7c5d9;
padding: 10px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 18px;
font-weight: bold;
color: #800000;
}
.logout-btn {
background: #d6daf0;
border: 1px solid #b7c5d9;
padding: 5px 10px;
text-decoration: none;
color: #800000;
font-size: 12px;
}
.logout-btn:hover {
background: #e5e9f0;
}
.board-container {
background: #efefef;
border: 1px solid #b7c5d9;
margin-bottom: 10px;
}
.posts-container {
max-height: 600px;
overflow-y: auto;
padding: 10px;
}
.post {
background: #f0e0d6;
border: 1px solid #d9bfb7;
margin-bottom: 10px;
padding: 10px;
}
.post-header {
margin-bottom: 8px;
font-size: 12px;
}
.post-id {
font-weight: bold;
color: #800000;
}
.post-time {
color: #707070;
margin-left: 10px;
}
.post-file {
margin-bottom: 8px;
}
.post-image {
max-width: 300px;
max-height: 300px;
border: 1px solid #d9bfb7;
}
.file-info {
font-size: 11px;
color: #707070;
margin-top: 5px;
}
.post-message {
white-space: pre-wrap;
word-wrap: break-word;
}
.post-form {
background: #efefef;
border: 1px solid #b7c5d9;
padding: 10px;
}
.form-row {
margin-bottom: 10px;
}
.message-input {
width: 100%;
height: 80px;
border: 1px solid #b7c5d9;
padding: 8px;
font-family: inherit;
font-size: 14px;
resize: vertical;
}
.file-input {
border: 1px solid #b7c5d9;
padding: 5px;
font-size: 12px;
margin-right: 10px;
}
.send-btn {
background: #d6daf0;
border: 1px solid #b7c5d9;
padding: 8px 15px;
color: #800000;
font-size: 14px;
cursor: pointer;
}
.send-btn:hover {
background: #e5e9f0;
}
.error {
background: #ffd6d6;
border: 1px solid #ff9999;
color: #cc0000;
padding: 10px;
margin-bottom: 10px;
font-size: 14px;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f0e0d6;
}
::-webkit-scrollbar-thumb {
background: #d9bfb7;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #c4a6a0;
}