From 82b3ee21e40222122e2ab543d8910dc233c89d11 Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Sun, 20 Oct 2024 13:37:03 +1100 Subject: [PATCH] Initial commit --- .gitignore | 9 ++ client-cli/README.md | 3 + client-cli/build.gradle.kts | 35 +++++ client-cli/settings.gradle.kts | 5 + client-cli/src/main/kotlin/Main.kt | 105 ++++++++++++++ server/README.md | 5 + server/build.gradle.kts | 34 +++++ server/chatHistory | 7 + server/settings.gradle.kts | 5 + server/src/main/kotlin/Main.kt | 219 +++++++++++++++++++++++++++++ 10 files changed, 427 insertions(+) create mode 100644 .gitignore create mode 100644 client-cli/README.md create mode 100644 client-cli/build.gradle.kts create mode 100644 client-cli/settings.gradle.kts create mode 100644 client-cli/src/main/kotlin/Main.kt create mode 100644 server/README.md create mode 100644 server/build.gradle.kts create mode 100644 server/chatHistory create mode 100644 server/settings.gradle.kts create mode 100644 server/src/main/kotlin/Main.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..831c9d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle +.swp +server/build +server/.gradle +server/userDatabase +server/messageHistory +client-cli/build +client-cli/.gradle +client-cli/.chookpen.profile diff --git a/client-cli/README.md b/client-cli/README.md new file mode 100644 index 0000000..167f3ed --- /dev/null +++ b/client-cli/README.md @@ -0,0 +1,3 @@ +# Chookpen Client (CLI) + +This is the reference client for Chookpen. It is quite a basic client. This is a good starting point if you need an example, or are building a GUI Kotlin client. diff --git a/client-cli/build.gradle.kts b/client-cli/build.gradle.kts new file mode 100644 index 0000000..87b5362 --- /dev/null +++ b/client-cli/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + kotlin("jvm") version "2.0.0" + application + distribution +} + +application { + mainClass.set("xyz.maxwellj.chookpen.client.MainKt") + layout.buildDirectory.dir("distributions/") +} + +group = "xyz.maxwellj.chookpen.client" +version = "0.0.2" + +repositories { + mavenCentral() +} + +tasks.withType { + manifest { + attributes["Main-Class"] = "xyz.maxwellj.chookpen.client.MainKt" + } +} + +dependencies { + testImplementation(kotlin("test")) + implementation("com.github.kittinunf.fuel:fuel:3.0.0-alpha03") +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(17) +} diff --git a/client-cli/settings.gradle.kts b/client-cli/settings.gradle.kts new file mode 100644 index 0000000..1568f91 --- /dev/null +++ b/client-cli/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "chookpen.client" + diff --git a/client-cli/src/main/kotlin/Main.kt b/client-cli/src/main/kotlin/Main.kt new file mode 100644 index 0000000..c582520 --- /dev/null +++ b/client-cli/src/main/kotlin/Main.kt @@ -0,0 +1,105 @@ +package xyz.maxwellj.chookpen.client + +import fuel.Fuel +import fuel.get +import java.io.File + +import kotlin.system.exitProcess + +import java.math.BigInteger +import java.security.MessageDigest + +fun md5(input:String): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') +} + +suspend fun main(args: Array) { + // Variables + var name = "" + var server = "" + var port = "" + var hasHTTPS = "" + var password = "" + var configFile = File(".chookpen.profile") + + if (!configFile.exists()) { + println("You don't have a Chookpen profile set up yet. If you've got a Chookpen profile, type 'l' to login and press enter. Otherwise, just press enter.") + val userInput = readln() + if (userInput == "l") { + println("Username:") + name = readln() + println("Password:") + password = readln() + val request = Fuel.get("http://localhost:8000/api/createAccount/username:{$name}token:{${md5(password)}}").body.string() + if (request == "Invalid token! Please try putting in your password right") { + println("Invalid password! Please rerun the program and put in the right password") + exitProcess(1) + } else { + configFile.createNewFile() + configFile.writeText("$name:$password:localhost:8000:0") + println("Logged in! Run the command again to start talking!") + exitProcess(0) + } + } else { + println("Choose a username:") + val newName = readln() + if (newName == "") { + println("Please choose a username! Rerun the program and try again") + exitProcess(1) + println("Choose a password:") + val newPassword = readln() + if (newPassword == "") { + println("Please choose a password! Rerun the program and try again") + exitProcess(1) + } + val request = Fuel.get("http://localhost:8000/api/createAccount/username:{$newName}token:{${md5(newPassword)}}").body.string() + if (request == "That username already exists on the server! Please choose a different username") { + println("That username already exists on the server! Rerun the program and choose a different username") + exitProcess(1) + } else { + configFile.createNewFile() + configFile.writeText("$newName:$newPassword:localhost:8000:0") + println("Account created!") + exitProcess(0) + } + } + } +} + + var configStage = 0 + for (char in configFile.readText()) { + if (char == ':') {configStage ++} + if (configStage == 0) { + name += char + } else if (configStage == 1) { + password += char + } else if (configStage == 2) { + server += char + } else if (configStage == 3) { + port += char + } else if (configStage == 4) { + hasHTTPS += char + } + } + + server = server.replace(":", "") + port = port.replace(":", "") + hasHTTPS = hasHTTPS.replace(":", "") + password = password.replace(":", "") + + // Actual code + println("Chookpen Client initialised!") + println("Hello $name@$server") + val protocol = "http" + if (hasHTTPS == "1") { + val protocol = "https" + } + if (args.count() == 0) { + println(Fuel.get("$protocol://$server:$port/api/username:{$name}token:{${md5(password)}}").body.string()) + } else { + val message = args[0] + println(Fuel.get("$protocol://$server:$port/api/username:{$name}token:{${md5(password)}}message:{$message}").body.string()) + } + exitProcess(0) +} diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..97acf5d --- /dev/null +++ b/server/README.md @@ -0,0 +1,5 @@ +# Chookpen Server + +This is the official Chookpen server implementation. It provides a backend and exposes an API. + + diff --git a/server/build.gradle.kts b/server/build.gradle.kts new file mode 100644 index 0000000..6b36b3a --- /dev/null +++ b/server/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + kotlin("jvm") version "2.0.0" + application + distribution +} + +application { + mainClass.set("xyz.maxwellj.chookpen.MainKt") + layout.buildDirectory.dir("distributions/") +} + +group = "xyz.maxwellj.chookpen" +version = "0.0.1" + +repositories { + mavenCentral() +} + +tasks.withType { + manifest { + attributes["Main-Class"] = "xyz.maxwellj.chookpen.MainKt" + } +} + +dependencies { + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(17) +} diff --git a/server/chatHistory b/server/chatHistory new file mode 100644 index 0000000..14f2c9f --- /dev/null +++ b/server/chatHistory @@ -0,0 +1,7 @@ +max: dingus +max: dongus +henry: fndskjnfjkdsnjfdsfdsnfdsnds +henry: fnjdsknfjkdnsjnfjdsnjfndsinifewnfewnofnf +max: nfljdsjfdsnfdsnkjds +max: dingus +max: dongus diff --git a/server/settings.gradle.kts b/server/settings.gradle.kts new file mode 100644 index 0000000..f475a9f --- /dev/null +++ b/server/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "chookpen" + diff --git a/server/src/main/kotlin/Main.kt b/server/src/main/kotlin/Main.kt new file mode 100644 index 0000000..9604d96 --- /dev/null +++ b/server/src/main/kotlin/Main.kt @@ -0,0 +1,219 @@ +package xyz.maxwellj.chookpen + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpHandler +import com.sun.net.httpserver.HttpServer +import java.net.InetSocketAddress + +import java.io.File +import java.io.BufferedReader + +fun main(args: Array) { + // Crete a server and start listening for arguments + val server = HttpServer.create(InetSocketAddress(8000), 0) + server.createContext("/test", ServerTester()) + server.createContext("/api", ApiHandler()) + server.createContext("/api/createAccount", CreateAccount()) + server.executor = null + server.start() + println("Server has started on port 8000") +} + +class ServerTester : HttpHandler { + override fun handle(t: HttpExchange) { + val response = "Testing, testing, 1, 2, 3... Somehow this is going to make it to production one day" + t.sendResponseHeaders(200, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + println("Test request recieved!") + os.close() + } +} + +class ApiHandler : HttpHandler { + override fun handle(t: HttpExchange) { + println("Request recieved...") + // Get the data + val path = t.requestURI.path + val inputData = path.substringAfter("/api/") + println("API request recieved: $inputData") + + // Parse data sent to the server by client + var username = "" + var token = "" + var message = "" + 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 == "message") { + message += 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 == "") { + val response = "That account does not exist on this server." + t.sendResponseHeaders(403, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + os.close() + return + } + + 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) { + val response = "Invalid token! Please try putting in your password right" + t.sendResponseHeaders(403, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + os.close() + return + } + // Make the message to respond to the client + val chatHistoryView = File("chatHistory") + var fullMessage = "" + if (message != "") { + fullMessage = "${chatHistoryView.readText()}$username: $message" + // Add the client's message to the chat history + val chatHistory = File("chatHistory") + chatHistory.appendText("$username: $message ${System.lineSeparator()}") + message = "" + } else { + fullMessage = "${chatHistoryView.readText()}" + } + val response = if (inputData.isNotEmpty()) { + fullMessage + } else { + "No data provided" + } + + // Send the message to the client + t.sendResponseHeaders(200, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + println("API request handled: $inputData") + os.close() + + } +} +class CreateAccount : HttpHandler { + override fun handle(t: HttpExchange) { + val path = t.requestURI.path + val inputData = path.substringAfter("/api/createAccount/") + println("Account creation request recieved: $inputData") + + // Parse data sent to the server by client + var username = "" + var token = "" + var message = "" + 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 == "message") { + message += character + } + } + } else { + dataType += character + } + } + val userDatabaseParser = BufferedReader(File("userDatabase").reader()) + var lineNumber = 1 + var userExists = 0 + + // Search the user database to find required information about the user + userDatabaseParser.forEachLine { line -> + if (line.contains(username)) { + val response = "That username already exists on the server! Please choose a different username" + t.sendResponseHeaders(422, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + os.close() + userExists = 1 + } + lineNumber++ + } + userDatabaseParser.close() + if (userExists == 1) {return} + println("$username:$token") + + if (username == "") { + val response = "Please input a username" + t.sendResponseHeaders(422, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + os.close() + return + } + + if (token == "") { + val response = "Please input a password" + t.sendResponseHeaders(422, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + os.close() + return + } + + val userDatabaseFile = File("userDatabase") + userDatabaseFile.appendText("${System.lineSeparator()}$username:$token") + val response = "Creating account was successful!" + t.sendResponseHeaders(200, response.length.toLong()) + val os = t.responseBody + os.write(response.toByteArray()) + println("") + os.close() + } +}