9 Commits
0.0.1 ... 0.0.2

Author SHA1 Message Date
Maxwell Jeffress
d839652916 Clean up code, support JSON 2024-11-23 18:12:07 +11:00
Maxwell Jeffress
b1b1c0accc Update some CSS, support JSON 2024-11-23 18:10:56 +11:00
Maxwell Jeffress
61c60f7906 Update readme 2024-11-05 20:41:44 +11:00
Maxwell Jeffress
a4b6721ebc Add web client, update CLI client 2024-11-05 20:35:10 +11:00
Maxwell Jeffress
41c9e7f53f Update README.md 2024-11-05 20:27:25 +11:00
378cb7c867 Update README.md 2024-11-05 20:18:43 +11:00
Maxwell Jeffress
e06f3891c8 Anti timeout ping, user list 2024-10-28 20:37:20 +11:00
Maxwell Jeffress
97a26f7615 Merge branch 'main' of max/chookpen 2024-10-28 18:08:36 +11:00
Maxwell Jeffress
18be6c6fd4 Lay the groundwork for some new features 2024-10-28 18:07:37 +11:00
15 changed files with 867 additions and 375 deletions

View File

@@ -2,96 +2,26 @@
## What is Chookpen? ## What is Chookpen?
Chookpen is a simple messaging service which allows users to talk to each other through text chat. In it's current state it has very low security and has many bugs, so please don't use this in production yet. Chookpen is a lightweight, secure-ish chat server implementation focused on simplicity and real-time communication. It features user authentication, persistent message history, real-time updates via WebSockets, and supports both traditional HTTP endpoints and WebSocket connections for flexible integration. It uses very little resources on a system, often using a maximum of 20mb. Chookpen is BETA SOFTWARE, and MAY BE BUGGY! Don't expect too much.
## How does Chookpen work? ## A guide to this repository
Chookpen works by having a client and a server. The client sends a request to the server, which includes a username, hashed password and message contents. The server returns that request with previous messages. Chookpen is split up into multiple parts, the server and different reference clients for different platforms. For extensive documentation of all parts of Chookpen, a repository wiki is being worked on which will be released soon. For now, if you want to learn more about Chookpen, visit each folder.
## How do I get started? ## Acknowledgments
First, you'll need to install Gradle and Java version 17 or later. These instructions vary depending on your OS, so look those up. Chookpen relies on a lot of FOSS software, and wouldn't be possible without it! Here's some quick links to the original projects if you're interested:
Next, clone the repository with `git clone https://git.maxwellj.xyz/max/chookpen`. Change to the `server` directory. Create the files `chatHistory` and `userDatabase`. Next, open a terminal and `gradle run`. Once the server starts, you can now send requests to the server! Using a web browser or `curl`, whichever is preferable, make a request to `http://localhost:8000/api/createAccount/username:{(pick a username)}token:{(pick a token)}`. If all works, you should have created an account! ### [Javalin](https://javalin.io)
Once you've created an account through the API, you can send requests using your token and username. You can send a request to see the chat history like this: `http://localhost:8000/api/username:{(your username)}token:{(your password)}`. If you'd like to send a message, it's like this: `http://localhost:8000/api/username:{(your username)}token:{(your password)}message:{(a message)}`. An easy to use web server for Kotlin and Java. Chookpen wouldn't be possible without it!
If you don't want all the hassle of sending requests, you can use the experimental CLI client. Open a new terminal, and cd to `client-cli`. Run `gradle installDist`, and wait for it to build. Cd to build/install. Make a file called `.chookpen.profile` in your home directory and add the following information, styled like a Unix /etc/passwd (colons in between items): ### [Gradle](https://gradle.org)
`username:password:server:port:0` The build tool for compiling Chookpen. It just works!
NOTE: When creating your account with this method, use the MD5 hash of your password. If you're unsure of what it is, just fill in all your details in .chookpen.profile and run the program. The server will tell you the hash the CLI created. ### [Kotlin](https://kotlinlang.org)
Once you're set up, run the CLI program in bin. Use the .bat if you're on Windows (yucky). You should be able to send and recieve messages! If that isn't working, make sure your .chookpen.profile is in your system home directory. The language Chookpen is coded in. How else does it work?
## How can I make my own client? ### [OpenJDK](https://openjdk.org)
Chookpen is very simple in how it works at present. There's only one chat on each server, and messages are sent as plaintext (unless you put your server behind a reverse proxy like Caddy). You can send a request to create an account, get messages or send a message.
### Brief API documentation for the server
#### Create account
**Usage**
`http://(address:port)/api/createAccount/username:{(username)}token:{(password-hash)}`
**Successful response**
`Success`
**Unsuccessful responses**
`Username already exists` - Choose a new username
`No username` - Add a username
`No token` - Add a token
#### Send a message
**Usage**
`http://(address:port)/api/send/username:{(username)}token:{(password-hash)}message:{(message to send)}`
**Successful response**
`Success`
**Unsuccessful responses**
`Unknown account` - Either you don't have an account or your username is wrong
`Invalid token` - Password is wrong
`No data provided` - Add a message
#### Get messages
**Usage**
`http://(address:port)/api/syncmessages/username:{(username)}token:{(password-hash)}`
**Successful response**
A successful response should contain everything in `chatHistory` in the directory you run the server in.
**Unsuccessful responses**
`Unknown account` - Either you don't have an account or your username is wrong
`Invalid token` - Password is wrong
### Websockets
Chookpen supports websockets for live updating of messages. You can establish a websocket connection with the URL `ws://(server):(port)/api/websocket/`. Send a login request to the websocket: `username:{(username)}password:{(password)}`. If your username and password are correct, you should start recieving messages. Send a message: `username:{(username)}password:{(password)}message:{(message)}`
## Some handy tips and tricks
Chookpen Server **does not support HTTPS!** You can put Chookpen Server behind a reverse proxy and that will sort that out for you.
Chookpen Server and CLI client are both in an alpha stage, keep this in mind before doing ANYTHING with it!
Chookpen Server is not ready for production :/
The port for the server is 7070.

View File

@@ -2,6 +2,8 @@ package xyz.maxwellj.chookpen.client
import okhttp3.* import okhttp3.*
import java.util.Scanner import java.util.Scanner
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess import kotlin.system.exitProcess
import java.io.File import java.io.File
@@ -52,7 +54,7 @@ fun main() {
} }
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
//.pingInterval(30, TimeUnit.SECONDS) .pingInterval(30, TimeUnit.SECONDS)
.build() .build()
val request = Request.Builder() val request = Request.Builder()

Binary file not shown.

43
client-web/gradient.css Normal file
View File

@@ -0,0 +1,43 @@
/* Reset default margin and ensure full viewport coverage */
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(
30deg,
#50C878, /* green */
#7FFFD4, /* aqua */
#87CEEB, /* light blue */
#00008B, /* dark blue */
#800080, /* purple */
#50C878, /* green */
#7FFFD4, /* aqua */
#87CEEB, /* light blue */
#00008B, /* dark blue */
#800080, /* purple */
#50C878 /* green */
);
background-size: 1000% 1000%;
animation: gradient 120s ease infinite;
}
/* Gradient animation keyframes */
@keyframes gradient {
0% {
background-position: 0% 50%;
}
100% {
background-position: 200% 50%;
}
}
/* Optional: styling for centered content */
.content {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-family: sans-serif;
font-size: 2rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}

208
client-web/index.css Normal file
View File

@@ -0,0 +1,208 @@
@font-face {
font-family: "inter";
src: url("InterVariable.ttf");
}
a {
color: white;
}
input {
color: white;
background: rgba(0, 0, 0, 0.5);
border: none;
border-radius: 10px;
padding: 5px;
font-family: "inter";
margin: 10px;
}
.tiny {
font-size: 5pt;
}
h1 {
font-size: 35pt;
}
h2 {
font-size: 20pt;
}
h3 {
font-size: 15pt;
}
p {
font-size: 12pt;
}
#messagebox {
overflow-y: auto;
border: 0px;
padding: 20px;
margin: 10px 10px;
flex-grow: 1; /* This makes it take up remaining space */
}
.section h2 {
margin: 20px;
}
.box p {
margin: 10px;
}
.box h3 {
margin: 10px;
}
.box img {
margin: 10px;
}
.box button {
margin: 10px;
}
.bluebutton {
color: white;
font-size: 12pt;
background: rgba(0, 0, 255, 0.3);
font-family: inter;
border-radius: 10px;
border: none;
padding: 5px;
}
.greenbutton {
color: white;
font-size: 12pt;
background: rgba(0, 255, 0, 0.3);
font-family: inter;
border-radius: 10px;
border: none;
padding: 5px;
}
.redbutton {
color: white;
font-size: 12pt;
background: rgba(255, 0, 0, 0.3);
font-family: inter;
border-radius: 10px;
border: none;
padding: 5px;
}
.backbutton {
color: white;
font-size: 12pt;
background: rgba(255, 0, 0, 0.3);
font-family: inter;
border-radius: 10px;
border: none;
padding: 5px;
}
.hidden {
display: none;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
box-sizing: border-box;
}
body {
color: white;
background: rgb(231,255,68);
background: linear-gradient(336deg, rgba(231,255,68,0.67) 0%, rgba(73,255,145,0.67) 43%, rgba(104,79,255,1) 100%);
font-family: inter;
background-attachment: fixed;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
/* Update section styles */
.section {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 10px;
padding: 5px;
width: calc(100vw - 40px);
height: calc(100vh - 40px);
box-sizing: border-box; /* Add this to include padding in width calculation */
}
/* Update box styles */
.box {
color: white;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
margin: 10px auto;
padding: 20px;
max-width: 400px;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
}
/* Special handling for messaging box */
#messaging .box {
max-width: none;
height: 100%;
position: relative; /* For absolute positioning of input container */
}
#messagebox {
overflow-y: auto;
border: 0px;
padding: 20px;
margin: 10px 0; /* Remove horizontal margin */
flex-grow: 1;
width: 100%;
box-sizing: border-box;
}
/* Style for individual messages */
.message {
text-align: left;
margin: 5px 0;
width: 100%;
}
/* Create a container for message input and send button */
.input-container {
display: flex;
width: 100%;
gap: 10px; /* Space between input and button */
/* padding: 10px;*/
box-sizing: border-box;
}
/* Update input styles */
input {
color: white;
background: rgba(0, 0, 0, 0.5);
border: none;
border-radius: 10px;
padding: 5px 5px;
font-family: "inter";
max-width: 300px;
width: 100%;
box-sizing: border-box;
}
/* Special style for message input */
#messageInput {
max-width: none;
flex-grow: 1; /* Take up remaining space */
margin: 0; /* Remove default margins */
}
/* Update button styles */
.bluebutton, .greenbutton, .redbutton, .backbutton {
max-width: 200px;
width: auto; /* Allow button to size to content */
margin: 0; /* Remove default margins */
white-space: nowrap; /* Prevent button text from wrapping */
}

44
client-web/index.html Normal file
View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chookchat</title>
<link type="text/css" rel="stylesheet" href="index.css">
<link type="text/css" rel="stylesheet" href="gradient.css">
<link rel="shortcut icon" type="image/jpg" href="favicon.ico"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body class="gradient">
<div id="login">
<div class="section">
<div class="box">
<h3>Chookchat</h3>
<input type="text" id="username" placeholder="Username"><br>
<input type="password" id="password" placeholder="Password"><br>
<button class="bluebutton" onclick="connect()">Log in</button>
<button class="greenbutton" onclick="register()">Register</button>
<button class="redbutton" onclick="showConfig()">Show Server Config</button>
<div id="serverStatus"></div>
</div>
<div class="box" style="display: none;" id="serverconfig">
<input type="text" id="serverUrl" value="bobcompass.online" placeholder="Server URL"><br>
<input type="text" id="serverPort" value="443" placeholder="Server Port"><br>
<input type="checkbox" id="securityStatus" checked>
<label for="securityStatus">Use HTTPS/WSS</label>
</div>
</div>
</div>
<div class="hidden" id="messaging">
<div class="box">
<div id="messagebox" class="box" style="height: 600px;"><div></div></div>
<div class="input-container">
<input type="text" id="messageInput" placeholder="Send a message...">
<button onclick="sendMessage()" class="bluebutton">Send</button>
</div>
</div>
</div>
<script src="index.js"></script>
</body>
</html>

173
client-web/index.js Normal file
View File

@@ -0,0 +1,173 @@
alert("Chookchat is currently in early development, expect bugs! Please don't try breaking the public server, do that with your own test server (read more in the Git repo). Thanks for trying Chookchat!")
const width = $(document).width();
if (width < 512) {
console.log("enlarging");
const loginDiv = document.getElementById('login');
if (loginDiv) {
console.log(loginDiv.style.width = `${width}px`);
console.log("enlarged?");
}
}
let ws;
let username;
let password;
function resizeMessaging() {
const messagingDiv = document.getElementById('messaging');
if (messagingDiv) {
messagingDiv.style.width = `${window.innerWidth - 40}px`; // -40 for body margins
messagingDiv.style.height = `${window.innerHeight - 40}px`; // -40 for body margins
}
}
// Call it initially
resizeMessaging();
// Add resize listener to handle window resizing
window.addEventListener('resize', resizeMessaging);
function showConfig() {
const serverconfig = document.getElementById('serverconfig')
if (serverconfig) {
serverconfig.style.display = 'block';
}
}
function md5(string) {
return CryptoJS.MD5(string).toString();
}
function getUrl() {
const serverUrl = document.getElementById('serverUrl').value.trim();
const serverPort = document.getElementById('serverPort').value;
const useWss = document.getElementById('securityStatus').checked;
const protocol = useWss ? 'wss' : 'ws';
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
return `${protocol}://${cleanUrl}:${serverPort}/api/websocket`;
}
function getSignupUrl() {
const serverUrl = document.getElementById('serverUrl').value.trim();
const serverPort = document.getElementById('serverPort').value;
const useWss = document.getElementById('securityStatus').checked;
const protocol = useWss ? 'https' : 'http';
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
return `${protocol}://${cleanUrl}:${serverPort}/api/createaccount/`;
}
function connect() {
username = document.getElementById('username').value;
password = document.getElementById('password').value;
if (!username || !password) {
alert('Please enter a username and password');
return;
}
const wsUrl = getUrl();
if (ws) {
ws.close();
}
ws = new WebSocket(wsUrl);
var incorrectDetail = 0;
ws.onopen = () => {
Notification.requestPermission();
console.log('Connected!');
document.getElementById('login').style.display = 'none';
document.getElementById('messaging').style.display = 'block';
const connectMessage = {
"type": "connect",
"username": username,
"token": md5(password),
"content": `${username} joined the room!`
}
ws.send(JSON.stringify(connectMessage));
ws.onmessage = (event) => {
if (event.data === "ping") {
ws.send("pong");
return;
}
const message = JSON.parse(event.data);
if (message.type == "error") {
if (message.username == "system") {
if (message.content == "invalid-token") {
alert("Your password is incorrect! Please try putting in your password right.");
incorrectDetail = 1;
location.reload();
}
if (message.content == "unknown-account") {
alert("That username isn't on the server. Maybe try registering?");
incorrectDetail = 1;
location.reload();
}
}
}
const messagesDiv = document.getElementById('messagebox');
const messageElement = document.createElement('div');
if (messageElement) {
if (messagesDiv) {
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
messageElement.className = 'message';
messageElement.textContent = `${message.username}: ${message.content}` ;
}
}
if (document.hidden) {
const notifiction = new Notification("Chookchat", {body: messageElement.textContent});
}
};
}
ws.onclose = () => {
alert("Chookchat has disconnected :/ Refresh the page to try again");
}
}
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
const processedMessage = {
"type": "message",
"username": username,
"token": md5(password),
"content": message
}
if (processedMessage && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(processedMessage));
messageInput.value = '';
}
}
document.getElementById('messageInput').addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
sendMessage();
}
});
document.getElementById('password').addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
connect();
}
});
function register() {
username = document.getElementById('username').value;
password = document.getElementById('password').value;
if (!username || !password) {
alert('Please enter a username and password');
return;
}
window.open(`${getSignupUrl()}username:{${username}}token:{${md5(password)}}`);
}

View File

@@ -1,5 +1,130 @@
# Chookpen Server # Chookpen Server
This is the official Chookpen server implementation. It provides a backend and exposes an API. Key features:
- Real-time messaging with WebSocket support
- User authentication and account management
- Persistent message history
- Simple HTTP API for basic operations
- Built-in static file serving for web clients
- Support for encryption keys
- Active user tracking and presence notifications
## Requirements
- Java 17 or later
- Caddy (for HTTPS reverse proxy) - [Download from caddyserver.com](https://caddyserver.com/download)
- Some kind of OS, any should do
- 50mb free space, 30mb avaliable ram (yes I know, very bloated and inefficient)
## Running the Server
Prebuilt binaries are available for immediate use. Simply download the latest server release and run:
```bash
./run
```
The server will start on port 7070 by default.
## Building from Source
First, install Gradle. Unless you want to painfully compile everything manually, Gradle is your best friend.
Clone the repository and CD into the server directory. Then run:
```bash
gradle build
```
To create a distribution (you don't need to run gradle build, it will do that for you):
```bash
./gradlew installDist
```
To run the server, create the files `chatHistory` and `userDatabase` in the directory you're running it from, then run the script to start Chookpen.
## API Documentation
### HTTP Endpoints
#### Send Message
- Endpoint: `/api/send/{content}`
- Content Format: `username:{name}token:{token}message:{message}`
- Response: "Success" or error message
#### Create Account
- Endpoint: `/api/createaccount/{content}`
- Content Format: `username:{name}token:{password}`
- Response: "Success" or error message
#### Sync Messages
- Endpoint: `/api/syncmessages/{content}`
- Content Format: `username:{name}token:{token}`
- Response: Chat history or error message
#### Auth Key (being worked on for E2EE)
- Endpoint: `/api/authkey/{content}`
- Content Format: `username:{name}token:{token}authkey:{key}`
- Response: "Success" or error message
## WebSocket Interface
Connect to `/api/websocket` for real-time updates.
### WebSocket Messages
1. Server to Client:
- Ping messages: "ping"
- User updates: "!users:{user1,user2,user3}"
- Chat messages: "username: message"
2. Client to Server:
- Pong response: "pong"
- Message format: Same as HTTP send endpoint
## Setting up HTTPS with Caddy
Caddy provides automatic HTTPS and serves as a reverse proxy for your Chookpen server. [Download from caddyserver.com](https://caddyserver.com/download) or from your Linux/BSD/Illumos/Haiku/TempleOS/whatever distribution's package manager.
1. Create a `Caddyfile` in your server directory:
```
chat.yourdomain.com {
reverse_proxy localhost:7070
}
```
2. Start Caddy:
```bash
caddy run
```
Caddy will automatically obtain and manage SSL certificates for your domain.
## Client Deployment
Place your client files in the `src/main/resources/public` directory. The server will automatically serve these static files, making the client accessible at your server's root URL.
## Maintenance
### Database Files
- `userDatabase`: Contains user credentials in format `username:token:salt`
- `chatHistory`: Stores message history
- Regular backups recommended
### Health Checks
- Server automatically maintains WebSocket connections
- Dead sessions are cleaned up automatically
- Active user count is logged
### Security Recommendations
- Keep database files secure
- Regular system updates
- Monitor for unusual login patterns
- Back up regularly:
```bash
#!/bin/bash
backup_dir="/path/to/backups"
date_stamp=$(date +%Y%m%d)
cp userDatabase "${backup_dir}/userDatabase_${date_stamp}"
cp chatHistory "${backup_dir}/chatHistory_${date_stamp}"
```

View File

@@ -5,11 +5,11 @@ plugins {
} }
application { application {
mainClass.set("xyz.maxwellj.chookpen.MainKt") mainClass.set("xyz.maxwellj.chookchat.MainKt")
layout.buildDirectory.dir("distributions/") layout.buildDirectory.dir("distributions/")
} }
group = "xyz.maxwellj.chookpen" group = "xyz.maxwellj.chookchat"
version = "0.0.1" version = "0.0.1"
repositories { repositories {
@@ -18,14 +18,16 @@ repositories {
tasks.withType<Jar> { tasks.withType<Jar> {
manifest { manifest {
attributes["Main-Class"] = "xyz.maxwellj.chookpen.MainKt" attributes["Main-Class"] = "xyz.maxwellj.chookchat.MainKt"
} }
} }
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("io.javalin:javalin:6.3.0") implementation("io.javalin:javalin:6.3.0")
implementation("org.slf4j:slf4j-simple:2.0.16") implementation("org.slf4j:slf4j-simple:2.0.16")
implementation("org.json:json:20230618")
} }
tasks.test { tasks.test {

View File

@@ -1,106 +1,182 @@
package xyz.maxwellj.chookpen package xyz.maxwellj.chookchat
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.websocket.WsContext import io.javalin.websocket.WsContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.UUID import java.util.UUID
import kotlin.concurrent.fixedRateTimer
import org.json.JSONObject
import org.json.JSONArray
import org.json.JSONException
import java.io.File import java.io.File
import java.io.BufferedReader import java.io.BufferedReader
import java.math.BigInteger
import java.security.MessageDigest
import java.nio.file.Paths
import java.nio.file.Files
fun md5(input:String): String {
val md = MessageDigest.getInstance("MD5")
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
}
/* /*
object WsSessionManager { fun removeLines(fileName: String, lineNumber: String) {
val sessions = ConcurrentHashMap<String, WsContext>() require(!fileName.isEmpty() && startLine >= 1 && numLines >= 1)
fun addSession(sessionID: String, ctx: WsContext) { val f = File(fileName)
sessions[sessionID] = ctx var lines = f.readLines()
} if (startLine > size) {
fun removeSession(sessionID: String) { println("The starting line is beyond the length of the file")
sessions.remove(sessionID) return
}
fun broadcast(message: String) {
sessions.values.forEach { ctx ->
ctx.send(message)
}
} }
lines = lines.take(startLine - 1) + lines.drop(startLine + n - 1)
val text = lines.joinToString(System.lineSeparator())
f.writeText(text)
} }
*/ */
object WsSessionManager { object WsSessionManager {
private val sessions = ConcurrentHashMap<String, WsContext>() val peopleOnline = mutableListOf("")
val sessionsList = mutableListOf("")
val sessions = ConcurrentHashMap<WsContext, String>()
val sessionIds = ConcurrentHashMap<String, WsContext>()
val userSessions = ConcurrentHashMap<String, String>()
init {
fixedRateTimer("websocket-ping", period = 5000) {
sendPing()
}
}
private fun sendPing() {
val deadSessions = mutableListOf<WsContext>()
sessions.keys.forEach { ctx ->
try {
if (ctx.session.isOpen) {
ctx.send("ping")
} else {
deadSessions.add(ctx)
}
} catch (e: Exception) {
println("Error sending ping: ${e.message}")
deadSessions.add(ctx)
}
}
// Clean up any dead sessions
deadSessions.forEach { removeSession(it) }
}
fun broadcastOnlineUsers() {
val processedData = JSONObject().apply {
put("type", "users")
put("username", "system")
put("content", peopleOnline.joinToString(", "))
}
broadcast(processedData.toString())
}
fun handleUserLogin(username: String) {
if (!peopleOnline.contains(username)) {
peopleOnline.add(username)
broadcastOnlineUsers()
}
}
fun addSession(ctx: WsContext) { fun addSession(ctx: WsContext) {
// Generate our own UUID for the session since we can't access Javalin's private sessionId try {
val sessionId = UUID.randomUUID().toString() val sessionId = UUID.randomUUID().toString()
sessions[sessionId] = ctx sessionsList.add(sessionId) // Changed from += to add()
sessions[ctx] = sessionId
sessionIds[sessionId] = ctx
} catch (e: Exception) {
println("Error adding session: ${e.message}")
}
} }
fun removeSession(ctx: WsContext) { fun removeSession(ctx: WsContext) {
// Find and remove the session by context try {
sessions.entries.removeIf { it.value === ctx } val sessionId = sessions[ctx]
if (sessionId != null) {
// Find and remove the username associated with this session
userSessions.entries.find { it.value == sessionId }?.let { entry ->
peopleOnline.remove(entry.key)
userSessions.remove(entry.key)
}
sessionsList.remove(sessionId)
sessions.remove(ctx)
sessionIds.remove(sessionId)
broadcastOnlineUsers()
}
} catch (e: Exception) {
println("Error removing session: ${e.message}")
}
}
fun associateUserWithSession(username: String, ctx: WsContext) {
val sessionId = sessions[ctx]
if (sessionId != null) {
userSessions[username] = sessionId
}
} }
fun broadcast(message: String) { fun broadcast(message: String) {
sessions.values.forEach { ctx -> val deadSessions = mutableListOf<WsContext>()
ctx.send(message)
sessions.keys.forEach { ctx ->
try {
if (ctx.session.isOpen) {
ctx.send(message)
} else {
deadSessions.add(ctx)
}
} catch (e: Exception) {
println("Error broadcasting to session: ${e.message}")
deadSessions.add(ctx)
}
} }
// Clean up any dead sessions
deadSessions.forEach { removeSession(it) }
} }
fun getSessionCount(): Int = sessions.size
} }
fun extractMessageContent(inputData: String): String { fun extractMessageContent(inputData: String, ctx: WsContext): String {
var username = "" val jsonInputData = JSONObject(inputData)
var message = "" if (jsonInputData.getString("type") == "connect") {
var dataType = "" val username = jsonInputData.getString("username")
var isParsingData = 0 WsSessionManager.associateUserWithSession(username, ctx)
WsSessionManager.handleUserLogin(username)
for (char in inputData) { val processedData = JSONObject().apply {
if (char == ':') { put("type", "connect")
isParsingData = 1 put("username", "system")
} else if (isParsingData == 1) { put("content", "${jsonInputData.getString("username")} just joined the room!")
if (char == '}') {
isParsingData = 0
dataType = ""
} else if (char != '{') {
if (dataType == "username") {
username += char
} else if (dataType == "message") {
message += char
}
}
} else {
dataType += char
} }
return(processedData.toString())
} }
val processedData = JSONObject().apply {
return("$username: $message") put("type", jsonInputData.getString("type"))
put("username", jsonInputData.getString("username"))
put("content", jsonInputData.getString("content"))
}
return(processedData.toString())
} }
fun handleSentMessage(inputData: String): String { fun handleSentMessage(inputData: String): String {
println("API request recieved: $inputData") println("API request recieved: $inputData")
// Parse data sent to the server by client var jsonInputData: JSONObject
var username = "" try {jsonInputData = JSONObject(inputData)} catch (error: JSONException){return(error.toString())}
var token = ""
var message = "" val username = jsonInputData.getString("username")
var dataType = "" val token = jsonInputData.getString("token")
var isParsingData = 0 val content = jsonInputData.getString("content")
for (char in inputData) {
val character = char
if (character == ':') {
isParsingData = 1
} else if (isParsingData == 1) {
if (character == '}') {
isParsingData = 0
dataType = ""
} else if (character != '{') {
if (dataType == "username") {
username += character
} else if (dataType == "token") {
token += character
} else if (dataType == "message") {
message += character
}
}
} else {
dataType += character
}
}
val userDatabaseParser = BufferedReader(File("userDatabase").reader()) val userDatabaseParser = BufferedReader(File("userDatabase").reader())
var lineNumber = 1 var lineNumber = 1
var userLine = "" var userLine = ""
@@ -115,11 +191,18 @@ fun handleSentMessage(inputData: String): String {
userDatabaseParser.close() userDatabaseParser.close()
if (userLine == "") { if (userLine == "") {
return("That account does not exist on this server.") val processedData = JSONObject().apply {
put("type", "error")
put("username", "system")
put("content", "unknown-account")
}
return(processedData.toString())
} }
var usernameInDatabase = "" var usernameInDatabase = ""
var tokenInDatabase = "" var tokenInDatabase = ""
var saltInDatabase = ""
var banStatus = ""
var currentStage = 0 var currentStage = 0
for (char in userLine) { for (char in userLine) {
if (char == ':') { if (char == ':') {
@@ -129,90 +212,48 @@ fun handleSentMessage(inputData: String): String {
usernameInDatabase += char usernameInDatabase += char
} else if (currentStage == 1) { } else if (currentStage == 1) {
tokenInDatabase += char tokenInDatabase += char
} else if (currentStage == 2) {
saltInDatabase += char
} else if (currentStage == 3) {
banStatus += char
} }
} }
tokenInDatabase = tokenInDatabase.replace(":", "") tokenInDatabase = tokenInDatabase.replace(":", "")
if (token != tokenInDatabase) { saltInDatabase = saltInDatabase.replace(":", "")
return("Invalid token! Please try putting in your password right") banStatus = banStatus.replace(":", "")
if (banStatus == "1") {
val processedData = JSONObject().apply {
put("type", "error")
put("username", "system")
put("content", "banned")
}
return(processedData.toString())
}
val tokenWithSalt = (md5(token + saltInDatabase))
/*println(saltInDatabase)
println(tokenWithSalt)
if (tokenWithSalt != tokenInDatabase) {*/
if (token != tokenInDatabase) {
val processedData = JSONObject().apply {
put("type", "error")
put("username", "system")
put("content", "invalid-token")
}
return(processedData.toString())
} }
// Make the message to respond to the client // Make the message to respond to the client
val chatHistoryView = File("chatHistory") val chatHistoryView = File("chatHistory")
var fullMessage = "" var fullMessage = ""
if (message != "") { if (content != "") {
fullMessage = "${chatHistoryView.readText()}$username: $message" fullMessage = "${chatHistoryView.readText()}$username: $content"
// Add the client's message to the chat history // Add the client's message to the chat history
val chatHistory = File("chatHistory") val chatHistory = File("chatHistory")
chatHistory.appendText("$username: $message ${System.lineSeparator()}") chatHistory.appendText("$username: $content ${System.lineSeparator()}")
message = ""
return("Success") return("Success")
} else { } else {
return("No data provided") return("No data provided")
} }
} return("Chookchat")
fun syncMessages(inputData: String): String {
println("API request recieved: $inputData")
// Parse data sent to the server by client
var username = ""
var token = ""
var dataType = ""
var isParsingData = 0
for (char in inputData) {
val character = char
if (character == ':') {
isParsingData = 1
} else if (isParsingData == 1) {
if (character == '}') {
isParsingData = 0
dataType = ""
} else if (character != '{') {
if (dataType == "username") {
username += character
} else if (dataType == "token") {
token += character
}
}
} else {
dataType += character
}
}
val userDatabaseParser = BufferedReader(File("userDatabase").reader())
var lineNumber = 1
var userLine = ""
// Search the user database to find required information about the user
userDatabaseParser.forEachLine { line ->
if (line.contains(username)) {
userLine = line
}
lineNumber++
}
userDatabaseParser.close()
if (userLine == "") {
return("Account not found")
}
var usernameInDatabase = ""
var tokenInDatabase = ""
var currentStage = 0
for (char in userLine) {
if (char == ':') {
currentStage ++
}
if (currentStage == 0) {
usernameInDatabase += char
} else if (currentStage == 1) {
tokenInDatabase += char
}
}
tokenInDatabase = tokenInDatabase.replace(":", "")
if (token != tokenInDatabase) {
return("Invalid token")
}
// Send back message history
val chatHistoryView = File("chatHistory")
return(chatHistoryView.readText())
} }
fun createAccount(inputData: String): String { fun createAccount(inputData: String): String {
@@ -252,7 +293,12 @@ fun createAccount(inputData: String): String {
var response = "" var response = ""
userDatabaseParser.forEachLine { line -> userDatabaseParser.forEachLine { line ->
if (line.contains(username)) { if (line.contains(username)) {
response = "Username already exists" val processedData = JSONObject().apply {
put("type", "error")
put("username", "system")
put("content", "username-taken")
}
response = processedData.toString()
} }
lineNumber++ lineNumber++
} }
@@ -261,175 +307,89 @@ fun createAccount(inputData: String): String {
} }
userDatabaseParser.close() userDatabaseParser.close()
if (username == "") { if (username == "") {
return("No username") val processedData = JSONObject().apply {
put("type", "error")
put("username", "system")
put("content", "no-username")
}
return(processedData.toString())
} }
if (token == "") { if (token == "") {
return("No token") val processedData = JSONObject().apply {
put("type", "error")
put("username", "system")
put("content", "no-token")
}
return(processedData.toString())
} }
val userDatabaseFile = File("userDatabase") val userDatabaseFile = File("userDatabase")
userDatabaseFile.appendText("${System.lineSeparator()}$username:$token") userDatabaseFile.appendText("${System.lineSeparator()}$username:$token")
return("Success") val processedData = JSONObject().apply {
put("type", "success")
put("username", "system")
put("content", "success")
}
return(processedData.toString())
} }
fun authKey(inputData: String): String {
println("API request recieved: $inputData")
// Parse data sent to the server by client fun handleServerCommand(command: String): String {
var username = "" val commandArgs = mutableListOf("")
var token = "" commandArgs.drop(1)
var authKey = ""
var dataType = ""
var isParsingData = 0
for (char in inputData) {
val character = char
if (character == ':') {
isParsingData = 1
} else if (isParsingData == 1) {
if (character == '}') {
isParsingData = 0
dataType = ""
} else if (character != '{') {
if (dataType == "username") {
username += character
} else if (dataType == "token") {
token += character
} else if (dataType == "authkey") {
authKey += character
}
}
} else {
dataType += character
}
}
val userDatabaseParser = BufferedReader(File("userDatabase").reader())
var lineNumber = 1
var userLine = ""
// Search the user database to find required information about the user
userDatabaseParser.forEachLine { line ->
if (line.contains(username)) {
userLine = line
}
lineNumber++
}
userDatabaseParser.close()
if (userLine == "") {
return("Account not found")
}
var usernameInDatabase = ""
var tokenInDatabase = ""
var currentStage = 0 var currentStage = 0
for (char in userLine) {
if (char == ':') {
currentStage ++
}
if (currentStage == 0) {
usernameInDatabase += char
} else if (currentStage == 1) {
tokenInDatabase += char
}
}
tokenInDatabase = tokenInDatabase.replace(":", "")
if (token != tokenInDatabase) {
return("Invalid token")
}
if (authKey == "") {
return("No auth key provided")
}
// Make the message to respond to the client
val chatHistoryView = File("chatHistory")
var fullMessage = ""
if (authKey != "") {
fullMessage = "encryptionKey:$username:$authKey"
authKey = ""
} else {
fullMessage = "${chatHistoryView.readText()}"
}
val response = if (inputData.isNotEmpty()) {
fullMessage
} else {
"No data provided"
}
// Send the message to the client for (char in command) {
return("Success") if (char == ' ') {
currentStage ++
commandArgs += ""
} else {
commandArgs[currentStage] += char
}
}
return("I'm not sure how to ${commandArgs.toString()}")
} }
fun main(args: Array<String>) { fun main(args: Array<String>) {
val app = Javalin.create() WsSessionManager.peopleOnline.removeAt(0)
.get("/") { ctx -> ctx.result("dingus") } WsSessionManager.sessionsList.removeAt(0)
.get("/api/send/{content}") { ctx -> val app = Javalin.create { config ->
val result = handleSentMessage(ctx.pathParam("content")) config.staticFiles.add("/public")
if (result == "Success") { }.get("/") { ctx ->
val messageContent = extractMessageContent(ctx.pathParam("content")) ctx.redirect("/index.html")
WsSessionManager.broadcast(messageContent)
}
ctx.result(result)
} }
.get("/api/createaccount/{content}") { ctx -> ctx.result(createAccount(ctx.pathParam("content")))} .get("/api/createaccount/{content}") { ctx -> ctx.result(createAccount(ctx.pathParam("content")))}
.get("/api/syncmessages/{content}") { ctx -> ctx.result(syncMessages(ctx.pathParam("content")))}
.get("/api/authkey/{content}") { ctx -> ctx.result(authKey(ctx.pathParam("content")))}
.ws("/api/websocket") { ws -> .ws("/api/websocket") { ws ->
ws.onConnect { ctx -> ws.onConnect { ctx ->
WsSessionManager.addSession(ctx) WsSessionManager.addSession(ctx)
ctx.send("Websocket success")
} }
ws.onClose { ctx -> ws.onClose { ctx ->
WsSessionManager.removeSession(ctx) WsSessionManager.removeSession(ctx)
} }
ws.onMessage { ctx -> ws.onMessage { ctx ->
println(ctx.message()) when (ctx.message()) {
val successState = handleSentMessage(ctx.message()) "pong" -> {}
if (successState != "Success") { else -> {
ctx.send(successState) println(ctx.message())
} else { val successState = handleSentMessage(ctx.message())
// Broadcast the message to all clients if successful if (successState != "Success") {
val messageContent = extractMessageContent(ctx.message()) try {
WsSessionManager.broadcast(messageContent) ctx.send(successState)
ctx.send("Message sent successfully") } catch (e: Exception) {
println("Error sending error message: ${e.message}")
}
} else {
val messageContent = extractMessageContent(ctx.message(), ctx)
WsSessionManager.broadcast(messageContent)
}
} }
} }
}
.start(7070)
}
/*
fun main(args: Array<String>) {
val app = Javalin.create()
.get("/") { ctx -> ctx.result("dingus") }
.get("/api/send/{content}") { ctx ->
val result = handleSentMessage(ctx.pathParam("content"))
if (result == "Success") {
val messageContent = extractMessageContent(ctx.pathParam("content")
WsSessionManager.broadcast(messageContent)
ctx.result(result)
}
}
.get("/api/createaccount/{content}") { ctx -> ctx.result(createAccount(ctx.pathParam("content")))}
.get("/api/syncmessages/{content}") { ctx -> ctx.result(syncMessages(ctx.pathParam("content")))}
.get("/api/authkey/{content}") { ctx -> ctx.result(authKey(ctx.pathParam("content")))}
.ws("/api/websocket") { ws ->
ws.onConnect { ctx ->
WsSessionManager.addSession(ctx.sessionId, ctx)
ctx.send("Websocket success")
}
ws.onClose { ctx ->
WsSessionManager.removeSession(ctx.sessionId)
}
ws.onMessage { ctx ->
println(ctx.message())
val successState = handleSentMessage(ctx.message())
if (successState != "Success") {
ctx.send(successState)
} else {
ctx.send("Message sent successfully")
}
} }
} }
.start(7070) .start(7070)
}*/ println("Type a command for the server")
while (1 == 1) {
println(handleServerCommand(readln()))
}
}

View File

@@ -0,0 +1 @@
../../../../../client-web/InterVariable.ttf

View File

@@ -0,0 +1 @@
../../../../../client-web/gradient.css

View File

@@ -0,0 +1 @@
../../../../../client-web/index.css

View File

@@ -0,0 +1 @@
../../../../../client-web/index.html

View File

@@ -0,0 +1 @@
../../../../../client-web/index.js