Compare commits
13 Commits
eb3e5eb043
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bed0e7774 | |||
| 533fcaa498 | |||
| cae24c5c0c | |||
| aebe0c7549 | |||
| ced7937583 | |||
| 09523a5509 | |||
| 51a94fea19 | |||
| 14359b9de1 | |||
| 90254a43bb | |||
| 7e04e37bb1 | |||
| d51eedd7bd | |||
| 4b30283697 | |||
| acafa80454 |
@@ -1,4 +1,3 @@
|
|||||||
vendor
|
vendor
|
||||||
composer.json
|
|
||||||
composer.lock
|
composer.lock
|
||||||
storage/database.sqlite
|
storage/database.sqlite
|
||||||
Generated
+4
-2
@@ -1,8 +1,10 @@
|
|||||||
# Default ignored files
|
# Default ignored files
|
||||||
/shelf/
|
/shelf/
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
# Editor-based HTTP Client requests
|
# Ignored default folder with query files
|
||||||
/httpRequests/
|
/queries/
|
||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
|||||||
Generated
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="InertiaPackage">
|
||||||
|
<option name="directoryPaths">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="LaravelIdeaMainSettings">
|
||||||
|
<option name="codeGeneration">
|
||||||
|
<LaravelCodeGeneration>
|
||||||
|
<option name="generationStringSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="createEloquentScope:namespace" value="Models\Scopes" />
|
||||||
|
<entry key="createModel:namespace" value="Models" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</LaravelCodeGeneration>
|
||||||
|
</option>
|
||||||
|
<option name="frameworkFound" value="true" />
|
||||||
|
<option name="userClassName" value="\App\Models\User" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+17
-17
@@ -12,28 +12,28 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="PhpIncludePathManager">
|
<component name="PhpIncludePathManager">
|
||||||
<include_path>
|
<include_path>
|
||||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/cboden/ratchet" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
|
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
|
||||||
<path value="$PROJECT_DIR$/vendor/ratchet/rfc6455" />
|
<path value="$PROJECT_DIR$/vendor/ratchet/rfc6455" />
|
||||||
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/cboden/ratchet" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="7.4">
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.0">
|
||||||
<option name="suggestChangeDefaultLanguageLevel" value="false" />
|
<option name="suggestChangeDefaultLanguageLevel" value="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpStanOptionsConfiguration">
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
|||||||
+63
-12
@@ -14,35 +14,86 @@ use Symfony\Component\Routing\RequestContext;
|
|||||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||||
use ComCen\Http\LoginController;
|
use ComCen\Http\LoginController;
|
||||||
use ComCen\Http\RegisterController;
|
use ComCen\Http\RegisterController;
|
||||||
|
use ComCen\Database\Handler;
|
||||||
|
use \ComCen\Security\TokenHandler;
|
||||||
|
|
||||||
class WebSocketServer implements MessageComponentInterface
|
class WebSocketServer implements MessageComponentInterface
|
||||||
{
|
{
|
||||||
private array $connectedUsers = [];
|
private $connectionsData = [];
|
||||||
|
|
||||||
|
private function isSameConnection(ConnectionInterface $connection1, ConnectionInterface $connection2): bool
|
||||||
|
{
|
||||||
|
return $connection1->resourceId === $connection2->resourceId;
|
||||||
|
}
|
||||||
|
private function getConnectionIndex(ConnectionInterface $connection): int | null
|
||||||
|
{
|
||||||
|
foreach ($this->connectionsData as $i => $connectionData) {
|
||||||
|
if ($this->isSameConnection($connection, $connectionData["connection"])) {
|
||||||
|
return $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private function sendToAllAuthenticated(string $username, string $msg): void
|
||||||
|
{
|
||||||
|
foreach ($this->connectionsData as $connectionData) {
|
||||||
|
if ($connectionData["username"] !== $username) {
|
||||||
|
$connectionData["connection"]->send("{\"sender\": \"{$username}\",\"msg\": \"{$msg}\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
private function deleteGivenId(int $id): void
|
private function deleteGivenId(int $id): void
|
||||||
{
|
{
|
||||||
foreach ($this->connectedUsers as $key => $conn) {
|
foreach ($this->connectionsData as $i => $connectionData) {
|
||||||
if ($conn->resourceId === $id) {
|
if ($connectionData["connection"]->resourceId === $id) {
|
||||||
unset($this->connectedUsers[$key]);
|
array_splice($this->connectionsData, $i, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onOpen(ConnectionInterface $conn): void
|
public function onOpen(ConnectionInterface $conn): void
|
||||||
{
|
{
|
||||||
$this->connectedUsers[] = $conn;
|
$this->connectionsData[] = [
|
||||||
$conn->send("Connected users: " . count($this->connectedUsers) . " (One is you)");
|
"connection" => $conn,
|
||||||
|
"username" => null
|
||||||
|
];
|
||||||
echo "New connection: {$conn->resourceId}\n";
|
echo "New connection: {$conn->resourceId}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onMessage(ConnectionInterface $from, $msg): void
|
public function onMessage(ConnectionInterface $from, $msg): void
|
||||||
{
|
{
|
||||||
foreach ($this->connectedUsers as $conn) {
|
$decodedMsg = json_decode($msg, true);
|
||||||
if ($from->resourceId !== $conn->resourceId) {
|
if (!$decodedMsg) {
|
||||||
$conn->send("From user " . $from->resourceId . ": " . $msg);
|
$from->send("{\"error\": \"not or empty json\"}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
$index = $this->getConnectionIndex($from);
|
||||||
|
if ($index === null) return;
|
||||||
|
|
||||||
|
if ($this->connectionsData[$index]["username"]) {
|
||||||
|
$msgContent = $decodedMsg["message"] ?? null;
|
||||||
|
if ($msgContent) {
|
||||||
|
$this->sendToAllAuthenticated($this->connectionsData[$index]["username"], $msgContent);
|
||||||
|
$from->send("{\"success\": \"message send\"}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
$from->send("Message sent to others");
|
$from->send("{\"error\": \"no message\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $decodedMsg["token"] ?? null;
|
||||||
|
if ($token) {
|
||||||
|
$tokenUser = TokenHandler::getTokenOwnership($token);
|
||||||
|
if ($tokenUser) {
|
||||||
|
$this->connectionsData[$index]["username"] = $tokenUser;
|
||||||
|
$from->send("{\"success\": \"authenticated\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$from->send("{\"error\": \"invalid token\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$from->send("{\"error\": \"you are not authenticated\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onClose(ConnectionInterface $conn): void
|
public function onClose(ConnectionInterface $conn): void
|
||||||
@@ -54,6 +105,7 @@ class WebSocketServer implements MessageComponentInterface
|
|||||||
public function onError(ConnectionInterface $conn, \Exception $e): void
|
public function onError(ConnectionInterface $conn, \Exception $e): void
|
||||||
{
|
{
|
||||||
echo "Error: {$e->getMessage()}\n";
|
echo "Error: {$e->getMessage()}\n";
|
||||||
|
$this->deleteGivenId($conn->resourceId);
|
||||||
$this->onClose($conn);
|
$this->onClose($conn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,8 +117,7 @@ $routes->add("ws", new Route("/ws", ["_controller" => new WsServer(n
|
|||||||
|
|
||||||
$server = IoServer::factory(
|
$server = IoServer::factory(
|
||||||
new HttpServer(new Router(new UrlMatcher($routes, new RequestContext()))),
|
new HttpServer(new Router(new UrlMatcher($routes, new RequestContext()))),
|
||||||
8080,
|
8080
|
||||||
"0.0.0.0"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
echo "Server running on http://localhost:8080\n";
|
echo "Server running on http://localhost:8080\n";
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"cboden/ratchet": "^0.4.4",
|
||||||
|
"ext-pdo": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ComCen\\": "src/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+245
-28
@@ -5,46 +5,263 @@ require __DIR__ . '/../vendor/autoload.php';
|
|||||||
use ComCen\Html\Html;
|
use ComCen\Html\Html;
|
||||||
|
|
||||||
$html = new Html();
|
$html = new Html();
|
||||||
$html->content .= "
|
$html->content = <<<'HTML'
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang=\"en\">
|
<html lang="pl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset=\"UTF-8\">
|
<meta charset="UTF-8">
|
||||||
<title>Chat</title>
|
<title>ComCen</title>
|
||||||
|
<style>
|
||||||
|
html{
|
||||||
|
background: #666;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* Kontenery główne */
|
||||||
|
#view-auth, #view-chat {
|
||||||
|
margin-top: 5%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nagłówki okien */
|
||||||
|
.logotext {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0px;
|
||||||
|
color: #333;
|
||||||
|
background-color: #ccc;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-bottom: none;
|
||||||
|
width: 75%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Główny boks (fioletowy) */
|
||||||
|
.box {
|
||||||
|
border: 1px solid #000;
|
||||||
|
background-color: #88f;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Obszar logów / formularza */
|
||||||
|
#log, .auth-content {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
height: 500px;
|
||||||
|
background-color: #b0ebbb;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stylizacja formularza wewnątrz zielonego pola */
|
||||||
|
.auth-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Elementy wiadomości i logów */
|
||||||
|
#log div, .stat-msg-div {
|
||||||
|
color: #000;
|
||||||
|
background-color: #a9f;
|
||||||
|
margin: 4px 10px;
|
||||||
|
padding: 5px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputy i Przyciski */
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 5px;
|
||||||
|
width: 250px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 1px solid #000;
|
||||||
|
background: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#m-in {
|
||||||
|
margin: 8px 0px 8px 8px;
|
||||||
|
width: 77%;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send, .logout {
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 3px 0px;
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send { margin: 8px -7px; border-left: none; }
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
float: right;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid #000;
|
||||||
|
height: 40px;
|
||||||
|
background-color: #fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout:hover {
|
||||||
|
background: #faa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kolory systemowe */
|
||||||
|
.error { background-color: #f66 !important; font-weight: bold; }
|
||||||
|
.system { background-color: #66f !important; font-weight: bold; }
|
||||||
|
|
||||||
|
#auth-nav {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>WebSocket Test</h2>
|
|
||||||
<div id=\"log\"></div>
|
<div id="view-auth">
|
||||||
<input id=\"msg\" type=\"text\" placeholder=\"Type a message...\" />
|
<h2 class="logotext">ComCen - Terminal Autoryzacji</h2>
|
||||||
<button onclick=\"send()\">Send</button>
|
<div class="box">
|
||||||
<button onclick='closeConnection()'>Disconnect</button>
|
<div class="auth-content">
|
||||||
|
<div id="auth-nav">
|
||||||
|
<button onclick="showAuth('login')">LOGIN</button>
|
||||||
|
<button onclick="showAuth('reg')">REGISTER</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="form-login">
|
||||||
|
<input type="text" id="l-u" placeholder="Użytkownik">
|
||||||
|
<br><br>
|
||||||
|
<input type="password" id="l-p" placeholder="Hasło">
|
||||||
|
<br><br>
|
||||||
|
<button onclick="runAuth('login')" style="width: 100%">ZALOGUJ</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="form-reg" class="hidden">
|
||||||
|
<input type="text" id="r-u" placeholder="Nowy login (a-z, 0-9)">
|
||||||
|
<br><br>
|
||||||
|
<input type="password" id="r-p" placeholder="Hasło">
|
||||||
|
<br><br>
|
||||||
|
<button onclick="runAuth('register')" style="width: 100%">ZAREJESTRUJ</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stat-msg" class="stat-msg-div hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="view-chat" class="hidden">
|
||||||
|
<h2 class="logotext">ComCen - IRC - Zalogowany jako: <span id="current-user"></span></h2>
|
||||||
|
<div class="box">
|
||||||
|
<div id="log"></div>
|
||||||
|
<input type="text" id="m-in" placeholder="Wiadomość..." onkeydown="if(event.key==='Enter') send()">
|
||||||
|
<button class="send" onclick="send()">Wyślij</button>
|
||||||
|
<button class="logout" onclick="location.reload()">Wyloguj</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const log = document.getElementById('log');
|
const API = "http://localhost:8080";
|
||||||
const ws = new WebSocket('ws://localhost:8080/ws');
|
let ws = null;
|
||||||
|
let myToken = "";
|
||||||
|
|
||||||
ws.onopen = () => append('Connected');
|
function showAuth(mode) {
|
||||||
ws.onmessage = e => append('Server: ' + e.data);
|
document.getElementById('form-login').classList.toggle('hidden', mode !== 'login');
|
||||||
ws.onclose = () => append('Disconnected');
|
document.getElementById('form-reg').classList.toggle('hidden', mode !== 'reg');
|
||||||
ws.onerror = e => append('Error: ' + e.message);
|
document.getElementById('stat-msg').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLog(txt, cls = '') {
|
||||||
|
const log = document.getElementById('log');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
if(cls) div.className = cls;
|
||||||
|
div.textContent = txt;
|
||||||
|
log.appendChild(div);
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAuth(type) {
|
||||||
|
const msgDiv = document.getElementById('stat-msg');
|
||||||
|
const u = document.getElementById(type === 'login' ? 'l-u' : 'r-u').value.trim();
|
||||||
|
const p = document.getElementById(type === 'login' ? 'l-p' : 'r-p').value.trim();
|
||||||
|
|
||||||
|
if (type === 'register') {
|
||||||
|
const regex = /^[a-zA-Z0-9_\.]+$/;
|
||||||
|
if (!regex.test(u)) {
|
||||||
|
msgDiv.textContent = "Błąd: Niepoprawne znaki!";
|
||||||
|
msgDiv.className = "stat-msg-div error";
|
||||||
|
msgDiv.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/${type}?username=${encodeURIComponent(u)}&password=${encodeURIComponent(p)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (type === 'login' && data.token) {
|
||||||
|
myToken = data.token;
|
||||||
|
document.getElementById('current-user').textContent = u;
|
||||||
|
document.getElementById('view-auth').classList.add('hidden');
|
||||||
|
document.getElementById('view-chat').classList.remove('hidden');
|
||||||
|
startWS();
|
||||||
|
} else if (type === 'register' && data.error === 'none') {
|
||||||
|
msgDiv.textContent = "Konto utworzone. Zaloguj się.";
|
||||||
|
msgDiv.className = "stat-msg-div system";
|
||||||
|
msgDiv.classList.remove('hidden');
|
||||||
|
showAuth('login');
|
||||||
|
} else {
|
||||||
|
msgDiv.textContent = "Błąd: " + (data.error || "odmowa");
|
||||||
|
msgDiv.className = "stat-msg-div error";
|
||||||
|
msgDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
msgDiv.textContent = "Błąd połączenia z API";
|
||||||
|
msgDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWS() {
|
||||||
|
ws = new WebSocket("ws://localhost:8080/ws");
|
||||||
|
ws.onopen = () => ws.send(JSON.stringify({ token: myToken }));
|
||||||
|
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(e.data);
|
||||||
|
if (d.success) {
|
||||||
|
if (d.success.includes("authenticated")) addLog("Połączono pomyślnie z czatem.", "system");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.sender && d.msg) addLog(`${d.sender}: ${d.msg}`);
|
||||||
|
else if (d.error) addLog("Błąd: " + d.error, "error");
|
||||||
|
} catch (err) { console.error("Błąd parsowania:", e.data); }
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
addLog("Połączenie przerwane.", "error");
|
||||||
|
document.getElementById('m-in').disabled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
const input = document.getElementById('msg');
|
const input = document.getElementById('m-in');
|
||||||
ws.send(input.value);
|
if(input.value.trim() && ws && ws.readyState === 1) {
|
||||||
append('You: ' + input.value);
|
ws.send(JSON.stringify({ message: input.value }));
|
||||||
input.value = '';
|
addLog("Ty: " + input.value);
|
||||||
|
input.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function append(text) {
|
|
||||||
const p = document.createElement('p');
|
|
||||||
p.textContent = text;
|
|
||||||
log.appendChild(p);
|
|
||||||
}
|
|
||||||
function closeConnection() {
|
|
||||||
ws.close();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
";
|
HTML;
|
||||||
$html->renderContent();
|
$html->renderContent();
|
||||||
@@ -9,20 +9,38 @@ class Handler
|
|||||||
private static ?Handler $instance = null;
|
private static ?Handler $instance = null;
|
||||||
private PDO $pdo;
|
private PDO $pdo;
|
||||||
|
|
||||||
|
public static function getInstance(): static
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new static();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
private function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
$this->pdo = new PDO('sqlite:' . __DIR__ . '/../../storage/database.sqlite');
|
$this->pdo = new PDO('sqlite:' . __DIR__ . '/../../storage/database.sqlite');
|
||||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
}
|
|
||||||
|
|
||||||
public function init(): void
|
|
||||||
{
|
|
||||||
$this->pdo->exec("
|
$this->pdo->exec("
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
username VARCHAR(32) NOT NULL UNIQUE,
|
username VARCHAR(32) NOT NULL UNIQUE,
|
||||||
password CHAR(255) NOT NULL
|
password CHAR(255) NOT NULL
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
$this->pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_groups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(64) NOT NULL UNIQUE
|
||||||
|
)
|
||||||
|
");
|
||||||
|
$this->pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS group_members (
|
||||||
|
group_id INTEGER NOT NULL,
|
||||||
|
username VARCHAR(32) NOT NULL,
|
||||||
|
PRIMARY KEY (group_id, username),
|
||||||
|
FOREIGN KEY (username) REFERENCES users(username)
|
||||||
|
)
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addUser(string $username, string $password): void
|
public function addUser(string $username, string $password): void
|
||||||
@@ -42,11 +60,11 @@ class Handler
|
|||||||
}
|
}
|
||||||
public function userExists(string $username): bool
|
public function userExists(string $username): bool
|
||||||
{
|
{
|
||||||
$statement = $this->pdo->prepare("SELECT * FROM users WHERE username = :username");
|
$statement = $this->pdo->prepare("SELECT 1 FROM users WHERE username = :username");
|
||||||
$statement->execute([
|
$statement->execute([
|
||||||
$username
|
"username" => $username
|
||||||
]);
|
]);
|
||||||
return $statement->rowCount() > 0;
|
return (bool) $statement->fetchColumn();
|
||||||
}
|
}
|
||||||
public function getPasswordHash(string $username): string
|
public function getPasswordHash(string $username): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace ComCen\Http;
|
namespace ComCen\Http;
|
||||||
|
|
||||||
use ComCen\Database\Handler;
|
use ComCen\Database\Handler;
|
||||||
|
use ComCen\Security\TokenHandler;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\Http\HttpServerInterface;
|
use Ratchet\Http\HttpServerInterface;
|
||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
@@ -12,10 +13,17 @@ class LoginController implements HttpServerInterface
|
|||||||
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null): void
|
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null): void
|
||||||
{
|
{
|
||||||
$params = [];
|
$params = [];
|
||||||
|
parse_str($request->getUri()->getQuery(), $params);
|
||||||
|
|
||||||
|
if (empty($params["username"]) && empty($params["password"])) {
|
||||||
|
Utils::respondHtml($conn, $this->loginPage());
|
||||||
|
$conn->close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$login = true;
|
$login = true;
|
||||||
$responseHead = "";
|
$responseHead = "";
|
||||||
$json = "";
|
$json = "";
|
||||||
parse_str($request->getUri()->getQuery(), $params);
|
|
||||||
|
|
||||||
$username = $params["username"];
|
$username = $params["username"];
|
||||||
$password = $params["password"];
|
$password = $params["password"];
|
||||||
@@ -26,22 +34,84 @@ class LoginController implements HttpServerInterface
|
|||||||
$responseHead = "400";
|
$responseHead = "400";
|
||||||
$json = json_encode(["error" => "Not enough params"]);
|
$json = json_encode(["error" => "Not enough params"]);
|
||||||
}
|
}
|
||||||
else if (password_verify($password, Handler::class->getPasswordHash($username)))
|
else if (!Handler::getInstance()->userExists($username) || !password_verify($password, Handler::getInstance()->getPasswordHash($username)))
|
||||||
|
|
||||||
if (!$login)
|
|
||||||
{
|
{
|
||||||
Utils::class->responeJson($conn, $responseHead, $json);
|
$login = false;
|
||||||
|
$responseHead = "409";
|
||||||
|
$json = json_encode(["error" => "Bad"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$login) {
|
||||||
|
Utils::responeJson($conn, $responseHead, $json);
|
||||||
$conn->close();
|
$conn->close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Handler::class->addUser($username, $password);
|
if (TokenHandler::doesUserHaveToken($username)) {
|
||||||
|
TokenHandler::deleteTokensForUser($username);
|
||||||
|
}
|
||||||
|
|
||||||
$json = json_encode(["error" => "none"]);
|
$json = json_encode(["token" => TokenHandler::getNewTokenForUser($username)]);
|
||||||
Utils::class->responeJson($conn, "200", $json);
|
Utils::responeJson($conn, "200", $json);
|
||||||
$conn->close();
|
$conn->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loginPage(): string
|
||||||
|
{
|
||||||
|
return <<<'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login — ComCen</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; max-width: 360px; margin: 80px auto; padding: 0 16px; }
|
||||||
|
h2 { margin-bottom: 24px; }
|
||||||
|
input { width: 100%; box-sizing: border-box; padding: 6px; font-family: monospace; font-size: 14px; margin-bottom: 10px; border: 1px solid #ccc; }
|
||||||
|
button { padding: 6px 16px; font-family: monospace; font-size: 14px; cursor: pointer; }
|
||||||
|
#msg { margin-top: 12px; font-size: 13px; }
|
||||||
|
.error { color: #c00; }
|
||||||
|
#token-box { margin-top: 16px; display: none; }
|
||||||
|
#token-box p { margin: 0 0 6px; }
|
||||||
|
#token-val { width: 100%; box-sizing: border-box; padding: 6px; font-family: monospace; font-size: 12px; background: #f4f4f4; border: 1px solid #ccc; }
|
||||||
|
a { color: inherit; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Login</h2>
|
||||||
|
<input id="username" type="text" placeholder="Username" />
|
||||||
|
<input id="password" type="password" placeholder="Password" />
|
||||||
|
<button onclick="login()">Login</button>
|
||||||
|
<div id="msg"></div>
|
||||||
|
<div id="token-box">
|
||||||
|
<p>Your token (paste into chat):</p>
|
||||||
|
<input id="token-val" type="text" readonly onclick="this.select()" />
|
||||||
|
</div>
|
||||||
|
<p><a href="/register">No account? Register</a></p>
|
||||||
|
<script>
|
||||||
|
async function login() {
|
||||||
|
const u = document.getElementById('username').value.trim();
|
||||||
|
const p = document.getElementById('password').value.trim();
|
||||||
|
const msg = document.getElementById('msg');
|
||||||
|
if (!u || !p) { msg.className = 'error'; msg.textContent = 'Fill in all fields.'; return; }
|
||||||
|
msg.textContent = '';
|
||||||
|
const res = await fetch('/login?username=' + encodeURIComponent(u) + '&password=' + encodeURIComponent(p));
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.token) {
|
||||||
|
document.getElementById('token-val').value = data.token;
|
||||||
|
document.getElementById('token-box').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
msg.className = 'error';
|
||||||
|
msg.textContent = data.error || 'Login failed.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Enter') login(); });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
public function onMessage(ConnectionInterface $from, $msg): void {}
|
public function onMessage(ConnectionInterface $from, $msg): void {}
|
||||||
public function onClose(ConnectionInterface $conn): void {}
|
public function onClose(ConnectionInterface $conn): void {}
|
||||||
public function onError(ConnectionInterface $conn, \Exception $e): void { $conn->close(); }
|
public function onError(ConnectionInterface $conn, \Exception $e): void { $conn->close(); }
|
||||||
|
|||||||
@@ -12,10 +12,17 @@ class RegisterController implements HttpServerInterface
|
|||||||
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null): void
|
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null): void
|
||||||
{
|
{
|
||||||
$params = [];
|
$params = [];
|
||||||
|
parse_str($request->getUri()->getQuery(), $params);
|
||||||
|
|
||||||
|
if (empty($params["username"]) && empty($params["password"])) {
|
||||||
|
Utils::respondHtml($conn, $this->registerPage());
|
||||||
|
$conn->close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$createAccount = true;
|
$createAccount = true;
|
||||||
$responseHead = "";
|
$responseHead = "";
|
||||||
$json = "";
|
$json = "";
|
||||||
parse_str($request->getUri()->getQuery(), $params);
|
|
||||||
|
|
||||||
$username = $params["username"];
|
$username = $params["username"];
|
||||||
$password = $params["password"];
|
$password = $params["password"];
|
||||||
@@ -26,13 +33,13 @@ class RegisterController implements HttpServerInterface
|
|||||||
$responseHead = "400";
|
$responseHead = "400";
|
||||||
$json = json_encode(["error" => "Not enough params"]);
|
$json = json_encode(["error" => "Not enough params"]);
|
||||||
}
|
}
|
||||||
else if (count($password) < 5)
|
else if (strlen($password) < 5)
|
||||||
{
|
{
|
||||||
$createAccount = false;
|
$createAccount = false;
|
||||||
$responseHead = "400";
|
$responseHead = "400";
|
||||||
$json = json_encode(["error" => "Short password"]);
|
$json = json_encode(["error" => "Short password"]);
|
||||||
}
|
}
|
||||||
else if (Handler::class->userExists($username))
|
else if (Handler::getInstance()->userExists($username))
|
||||||
{
|
{
|
||||||
$createAccount = false;
|
$createAccount = false;
|
||||||
$responseHead = "409";
|
$responseHead = "409";
|
||||||
@@ -41,18 +48,69 @@ class RegisterController implements HttpServerInterface
|
|||||||
|
|
||||||
if (!$createAccount)
|
if (!$createAccount)
|
||||||
{
|
{
|
||||||
Utils::class->responeJson($conn, $responseHead, $json);
|
Utils::responeJson($conn, $responseHead, $json);
|
||||||
$conn->close();
|
$conn->close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Handler::class->addUser($username, $password);
|
Handler::getInstance()->addUser($username, $password);
|
||||||
|
|
||||||
$json = json_encode(["error" => "none"]);
|
$json = json_encode(["error" => "none"]);
|
||||||
Utils::class->responeJson($conn, "200", $json);
|
Utils::responeJson($conn, "200", $json);
|
||||||
$conn->close();
|
$conn->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function registerPage(): string
|
||||||
|
{
|
||||||
|
return <<<'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Register — ComCen</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; max-width: 360px; margin: 80px auto; padding: 0 16px; }
|
||||||
|
h2 { margin-bottom: 24px; }
|
||||||
|
input { width: 100%; box-sizing: border-box; padding: 6px; font-family: monospace; font-size: 14px; margin-bottom: 10px; border: 1px solid #ccc; }
|
||||||
|
button { padding: 6px 16px; font-family: monospace; font-size: 14px; cursor: pointer; }
|
||||||
|
#msg { margin-top: 12px; font-size: 13px; }
|
||||||
|
.error { color: #c00; }
|
||||||
|
.success { color: #080; }
|
||||||
|
a { color: inherit; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Register</h2>
|
||||||
|
<input id="username" type="text" placeholder="Username" />
|
||||||
|
<input id="password" type="password" placeholder="Password (min 5 chars)" />
|
||||||
|
<button onclick="register()">Register</button>
|
||||||
|
<div id="msg"></div>
|
||||||
|
<p><a href="/login">Already have an account? Login</a></p>
|
||||||
|
<script>
|
||||||
|
async function register() {
|
||||||
|
const u = document.getElementById('username').value.trim();
|
||||||
|
const p = document.getElementById('password').value.trim();
|
||||||
|
const msg = document.getElementById('msg');
|
||||||
|
if (!u || !p) { msg.className = 'error'; msg.textContent = 'Fill in all fields.'; return; }
|
||||||
|
msg.textContent = '';
|
||||||
|
const res = await fetch('/register?username=' + encodeURIComponent(u) + '&password=' + encodeURIComponent(p));
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error === 'none') {
|
||||||
|
msg.className = 'success';
|
||||||
|
msg.textContent = 'Account created! Redirecting to login...';
|
||||||
|
setTimeout(() => window.location.href = '/login', 1200);
|
||||||
|
} else {
|
||||||
|
msg.className = 'error';
|
||||||
|
msg.textContent = data.error || 'Registration failed.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Enter') register(); });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
public function onMessage(ConnectionInterface $from, $msg): void {}
|
public function onMessage(ConnectionInterface $from, $msg): void {}
|
||||||
public function onClose(ConnectionInterface $conn): void {}
|
public function onClose(ConnectionInterface $conn): void {}
|
||||||
public function onError(ConnectionInterface $conn, \Exception $e): void { $conn->close(); }
|
public function onError(ConnectionInterface $conn, \Exception $e): void { $conn->close(); }
|
||||||
|
|||||||
+14
-2
@@ -4,10 +4,22 @@ namespace ComCen\Http;
|
|||||||
|
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Access-Control-Allow-Origin: *
|
||||||
|
zezwala na urzycie portu 8080
|
||||||
|
portalowi chodzącemu na porcie 8000
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
class Utils
|
class Utils
|
||||||
{
|
{
|
||||||
function responeJson(ConnectionInterface $conn, string $head, string $jsonData): void
|
static function responeJson(ConnectionInterface $conn, string $head, string $jsonData): void
|
||||||
{
|
{
|
||||||
$conn->send("HTTP/1.1 {$head}\r\nContent-Type: application/json\r\n\r\n{$jsonData}");
|
$conn->send("HTTP/1.1 {$head}\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n\r\n{$jsonData}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static function respondHtml(ConnectionInterface $conn, string $html): void
|
||||||
|
{
|
||||||
|
$conn->send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nAccess-Control-Allow-Origin: *\r\n\r\n{$html}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,23 +6,54 @@ class TokenHandler
|
|||||||
{
|
{
|
||||||
private static ?self $instance = null;
|
private static ?self $instance = null;
|
||||||
private static $tokens = [];
|
private static $tokens = [];
|
||||||
|
private static int $iterations = 0;
|
||||||
|
|
||||||
private static function uuid_v4(): string {
|
private static function random32Characters(): string {
|
||||||
$data = random_bytes(16);
|
$data = random_bytes(16);
|
||||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
return bin2hex($data);
|
||||||
}
|
}
|
||||||
public static function getNewTokenForUser(string $username)
|
public static function doesUserHaveToken(string $username): bool
|
||||||
{
|
{
|
||||||
$uuid = sprintf(
|
return array_any(self::$tokens, fn($token) => $token[0] === $username);
|
||||||
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
}
|
||||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
public static function getNewTokenForUser(string $username): string
|
||||||
mt_rand(0, 0xffff),
|
{
|
||||||
mt_rand(0, 0x0fff) | 0x4000,
|
$tokenBody = self::random32Characters() . str_pad(self::$iterations++, 5, '0');
|
||||||
mt_rand(0, 0x3fff) | 0x8000,
|
if (self::$iterations >= 99999) {
|
||||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
self::$iterations = 0;
|
||||||
);
|
}
|
||||||
|
$timestamp = microtime(true) * 10000;
|
||||||
|
self::$tokens[] = [$username, $timestamp, $tokenBody];
|
||||||
|
return $timestamp . $tokenBody;
|
||||||
|
}
|
||||||
|
public static function getTokenOwnership(string $controlledToken): string | null
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < count(self::$tokens); ++$i) {
|
||||||
|
$token = self::$tokens[$i];
|
||||||
|
if ($token[1] . $token[2] === $controlledToken) {
|
||||||
|
return $token[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public static function deleteOldTokens(): void
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < count(self::$tokens); ++$i) {
|
||||||
|
$token = self::$tokens[$i];
|
||||||
|
// 1 hour
|
||||||
|
if (time() - ($token[1] / 10000) > 3600) {
|
||||||
|
array_splice(self::$tokens, $i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static function deleteTokensForUser(string $user): void
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < count(self::$tokens); ++$i) {
|
||||||
|
if (self::$tokens[$i][0] === $user) {
|
||||||
|
array_splice(self::$tokens, $i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user