├── out
├── test
│ └── WebServerKt
│ │ ├── file.txt
│ │ ├── testResources
│ │ └── file.txt
│ │ └── META-INF
│ │ └── WebServerKt.kotlin_module
└── production
│ └── WebServerKt
│ ├── static
│ ├── example1.pdf
│ ├── digitalface.jpg
│ ├── 404.html
│ ├── index.html
│ ├── index.css
│ └── 404.css
│ └── META-INF
│ └── WebServerKt.kotlin_module
├── .idea
├── .gitignore
├── codeStyles
│ └── codeStyleConfig.xml
├── vcs.xml
├── kotlinc.xml
├── misc.xml
├── modules.xml
├── libraries
│ ├── jetbrains_kotlinx_serialization_json.xml
│ └── KotlinJavaRuntime.xml
├── WebServerKt.iml
└── uiDesigner.xml
├── Resources
└── static
│ ├── example1.pdf
│ ├── digitalface.jpg
│ ├── 404.html
│ ├── index.html
│ ├── index.css
│ └── 404.css
├── src
└── bexmod
│ ├── logging.kt
│ ├── http
│ ├── HttpWorker.kt
│ ├── requestline.kt
│ ├── HttpResponse.kt
│ └── HttpRequest.kt
│ └── webserver
│ ├── Router.kt
│ ├── Server.kt
│ └── handlers.kt
├── .gitignore
├── test
├── httpTests
│ ├── HttpRequestTest.kt
│ └── HttpResponseTest.kt
└── requestlineTests
│ └── MethodTests.kt
├── README.md
└── LICENSE
/out/test/WebServerKt/file.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/out/test/WebServerKt/testResources/file.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/out/test/WebServerKt/META-INF/WebServerKt.kotlin_module:
--------------------------------------------------------------------------------
1 | " *
--------------------------------------------------------------------------------
/Resources/static/example1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bexxmodd/WebServerKt/main/Resources/static/example1.pdf
--------------------------------------------------------------------------------
/Resources/static/digitalface.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bexxmodd/WebServerKt/main/Resources/static/digitalface.jpg
--------------------------------------------------------------------------------
/out/production/WebServerKt/static/example1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bexxmodd/WebServerKt/main/out/production/WebServerKt/static/example1.pdf
--------------------------------------------------------------------------------
/out/production/WebServerKt/static/digitalface.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bexxmodd/WebServerKt/main/out/production/WebServerKt/static/digitalface.jpg
--------------------------------------------------------------------------------
/out/production/WebServerKt/META-INF/WebServerKt.kotlin_module:
--------------------------------------------------------------------------------
1 |
2 |
3 | bexmod.http
RequestlineKt
4 |
5 | bexmod.webserverServerKt" *
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/bexmod/logging.kt:
--------------------------------------------------------------------------------
1 | package bexmod
2 |
3 | import java.util.logging.Logger
4 |
5 | class WebLogger() {
6 |
7 | companion object {
8 | val LOG = Logger.getLogger(WebLogger::class.java.name)
9 | }
10 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Resources/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404
7 |
8 |
9 |
10 |
11 |
Error 404
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/bexmod/http/HttpWorker.kt:
--------------------------------------------------------------------------------
1 | package bexmod.http
2 |
3 | import bexmod.webserver.Router
4 | import java.net.Socket
5 |
6 | class HttpWorker(private val socket: Socket, private val request: String) : Runnable {
7 |
8 | override fun run() {
9 | val httpRequest = HttpRequest(request)
10 | Router.route(httpRequest, socket)
11 | }
12 | }
--------------------------------------------------------------------------------
/out/production/WebServerKt/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404
7 |
8 |
9 |
10 |
11 |
Error 404
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 |
--------------------------------------------------------------------------------
/Resources/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Index Page
6 |
7 |
8 |
9 | I'm Welcome Page, I Guess
10 | I was built with Kotlin for scratch as a practice to learn language and a challenge of building static web server from scratch
11 | Here's
12 | Github Repo for the project
13 |
14 |
--------------------------------------------------------------------------------
/test/httpTests/HttpRequestTest.kt:
--------------------------------------------------------------------------------
1 | package httpTests
2 |
3 | import bexmod.http.HttpRequest
4 | import bexmod.http.Method
5 | import bexmod.http.Version
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Test
8 |
9 | class HttpRequestTest {
10 |
11 | @Test
12 | fun simpleOkRequestTest() {
13 | val req = HttpRequest("GET /api HTTP/1.1")
14 | assertEquals(req.method, Method.GET)
15 | assertEquals(req.version, Version.V1_1)
16 | assertEquals(req.resource.path, "/api")
17 | }
18 | }
--------------------------------------------------------------------------------
/test/requestlineTests/MethodTests.kt:
--------------------------------------------------------------------------------
1 | package requestlineTests
2 |
3 | import bexmod.http.Method
4 | import org.junit.Test
5 | import org.junit.Assert.assertEquals
6 |
7 | class MethodTests {
8 |
9 | @Test
10 | fun createMethodFromStringTest() {
11 | val actual = Method.from("GET")
12 | assertEquals(Method.GET, actual)
13 | }
14 |
15 | @Test
16 | fun wrongMethodTest() {
17 | val actual = Method.from("NOTAMETHOD")
18 | assertEquals(Method.UNINITIALIZED, actual)
19 | }
20 | }
--------------------------------------------------------------------------------
/out/production/WebServerKt/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Index Page
6 |
7 |
8 |
9 | I'm Welcome Page, I Guess
10 | I was built with Kotlin for scratch as a practice to learn language and a challenge of building static web server from scratch
11 | Here's
12 | Github Repo for the project
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Multithreaded Web Server written from scratch in Kotlin
2 |
3 | Web Server runs on top of TCP and is able to process HTTP requests like GET and HEAD
4 |
5 | Build and start the server by supplied port number as a program argument
6 | Then you can open it in web browser at `localhost:port` for the main page
7 |
8 | You can also interact with Web Server through `curl`.
9 |
10 | This is how you can request home page (Assuming that server listens on port 8000):
11 |
12 | ```bash
13 | curl localhost:8000/index.html
14 | ```
15 |
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/Resources/static/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | text-align: center;
4 | padding: 3rem;
5 | font-size: 1.125rem;
6 | line-height: 1.5;
7 | transition: all 725ms ease-in-out;
8 | }
9 |
10 | h1 {
11 | font-size: 2rem;
12 | font-weight: bolder;
13 | margin-bottom: 1rem;
14 | }
15 |
16 | p {
17 | margin-bottom: 1rem;
18 | color: tomato;
19 | }
20 |
21 | h3 {
22 | margin-bottom: 1rem;
23 | color: firebrick;
24 | }
25 |
26 | button {
27 | cursor: pointer;
28 | appearance: none;
29 | border-radius: 4px;
30 | font-size: 1.25rem;
31 | padding: 0.75rem 1rem;
32 | border: 1px solid navy;
33 | background-color: dodgerblue;
34 | color: white;
35 | }
--------------------------------------------------------------------------------
/out/production/WebServerKt/static/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | text-align: center;
4 | padding: 3rem;
5 | font-size: 1.125rem;
6 | line-height: 1.5;
7 | transition: all 725ms ease-in-out;
8 | }
9 |
10 | h1 {
11 | font-size: 2rem;
12 | font-weight: bolder;
13 | margin-bottom: 1rem;
14 | }
15 |
16 | p {
17 | margin-bottom: 1rem;
18 | color: tomato;
19 | }
20 |
21 | h3 {
22 | margin-bottom: 1rem;
23 | color: firebrick;
24 | }
25 |
26 | button {
27 | cursor: pointer;
28 | appearance: none;
29 | border-radius: 4px;
30 | font-size: 1.25rem;
31 | padding: 0.75rem 1rem;
32 | border: 1px solid navy;
33 | background-color: dodgerblue;
34 | color: white;
35 | }
--------------------------------------------------------------------------------
/Resources/static/404.css:
--------------------------------------------------------------------------------
1 | *{
2 | transition: all 0.6s;
3 | }
4 |
5 | html {
6 | height: 100%;
7 | }
8 |
9 | body{
10 | font-family: 'Lato', sans-serif;
11 | color: #888;
12 | margin: 0;
13 | }
14 |
15 | #main{
16 | display: table;
17 | width: 100%;
18 | height: 100vh;
19 | text-align: center;
20 | }
21 |
22 | .fof{
23 | display: table-cell;
24 | vertical-align: middle;
25 | }
26 |
27 | .fof h1{
28 | font-size: 50px;
29 | display: inline-block;
30 | padding-right: 12px;
31 | animation: type .5s alternate infinite;
32 | }
33 |
34 | @keyframes type{
35 | from{box-shadow: inset -3px 0px 0px #888;}
36 | to{box-shadow: inset -3px 0px 0px transparent;}
37 | }
--------------------------------------------------------------------------------
/out/production/WebServerKt/static/404.css:
--------------------------------------------------------------------------------
1 | *{
2 | transition: all 0.6s;
3 | }
4 |
5 | html {
6 | height: 100%;
7 | }
8 |
9 | body{
10 | font-family: 'Lato', sans-serif;
11 | color: #888;
12 | margin: 0;
13 | }
14 |
15 | #main{
16 | display: table;
17 | width: 100%;
18 | height: 100vh;
19 | text-align: center;
20 | }
21 |
22 | .fof{
23 | display: table-cell;
24 | vertical-align: middle;
25 | }
26 |
27 | .fof h1{
28 | font-size: 50px;
29 | display: inline-block;
30 | padding-right: 12px;
31 | animation: type .5s alternate infinite;
32 | }
33 |
34 | @keyframes type{
35 | from{box-shadow: inset -3px 0px 0px #888;}
36 | to{box-shadow: inset -3px 0px 0px transparent;}
37 | }
--------------------------------------------------------------------------------
/src/bexmod/webserver/Router.kt:
--------------------------------------------------------------------------------
1 | package bexmod.webserver
2 |
3 | import bexmod.http.HttpRequest
4 | import bexmod.http.HttpResponse
5 | import bexmod.http.Method
6 | import java.net.Socket
7 |
8 | class Router {
9 | companion object {
10 | @JvmStatic
11 | fun route(req: HttpRequest, socket: Socket) {
12 | val rsp = when (req.method) {
13 | Method.HEAD, Method.GET -> {
14 | val routes = req.resource.path.split("/")
15 | when (routes[0]) {
16 | "" -> StaticPageHandler().handle(req)
17 | "api" -> DynamicPageHandler().handle(req)
18 | "data" -> RawDataHandler().handle(req)
19 | else -> null
20 | }
21 | }
22 | Method.PUT, Method.POST -> HttpResponse(405)
23 | Method.UNINITIALIZED -> if (req.isBadRequest) HttpResponse(400) else HttpResponse(501)
24 | }
25 |
26 | rsp?.let { socket.getOutputStream().sendResponse(it) }
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/.idea/libraries/jetbrains_kotlinx_serialization_json.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Beka Modebadze
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.idea/WebServerKt.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/bexmod/http/requestline.kt:
--------------------------------------------------------------------------------
1 | package bexmod.http
2 |
3 | enum class Method(val method: String) {
4 | GET("GET"),
5 | HEAD("HEAD"),
6 | POST("POST"),
7 | PUT("PUT"),
8 | UNINITIALIZED("UNINITIALIZED");
9 |
10 | companion object {
11 | fun from(m: String): Method {
12 | return when (m) {
13 | "GET" -> GET
14 | "HEAD" -> HEAD
15 | "POST" -> POST
16 | "PUT" -> PUT
17 | else -> UNINITIALIZED
18 | }
19 | }
20 | }
21 | }
22 |
23 | enum class Version(val version: String) {
24 | V0_9("HTTP/0.9"),
25 | V1_1("HTTP/1.1"),
26 | V2_0("HTTP/2.0");
27 |
28 | companion object {
29 | fun from(v: String): Version {
30 | return when (v) {
31 | "HTTP/1.1" -> V1_1
32 | "HTTP/2.0" -> V2_0
33 | else -> V0_9
34 | }
35 | }
36 | }
37 | }
38 |
39 | data class Resource(val path: String)
40 |
41 | fun statusTextFrom(code: Int) = when(code) {
42 | 200 -> "Succeess"
43 | 304 -> "Not Modified"
44 | 400 -> "Bad Request"
45 | 403 -> "Forbidden"
46 | 404 -> "Not Found"
47 | 405 -> "Not Allowed"
48 | 501 -> "Not Implemented"
49 | 505 -> "HTTP Version Not Supported"
50 | else -> throw IllegalArgumentException("Code $code is invalid")
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/test/httpTests/HttpResponseTest.kt:
--------------------------------------------------------------------------------
1 | package httpTests
2 |
3 | import bexmod.http.HttpRequest
4 | import bexmod.http.HttpResponse
5 | import java.util.*
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Test
8 |
9 | class HttpResponseTest {
10 |
11 | @Test
12 | fun testHttpRspSimple() {
13 | val actual = HttpResponse().toString()
14 | val expected = "HTTP/1.1 200 Success\r\n\r\n"
15 | assertEquals(expected, actual)
16 | }
17 |
18 | @Test
19 | fun testHttpRspWithHeadersOnly() {
20 | val h = sortedMapOf("Content-type" to "text/html")
21 | val actual = HttpResponse(200, h, Optional.empty()).toString()
22 | val expected = "HTTP/1.1 200 Success\r\nContent-Length: 0\r\nContent-type: text/html\r\n\r\n"
23 | assertEquals(expected, actual)
24 | }
25 |
26 | @Test
27 | fun testHttpRspWithHeadersAndBody() {
28 | val h = sortedMapOf("Content-type" to "text/html")
29 | val actual = HttpResponse(200, h, Optional.of("Test")).toString()
30 | val expected = "HTTP/1.1 200 Success\r\nContent-Length: 4\r\nContent-type: text/html\r\n\r\nTest"
31 | assertEquals(expected, actual)
32 | }
33 |
34 | @Test
35 | fun testHttpRspWithLotsHeadersAndBody() {
36 | val h = sortedMapOf("Content-type" to "text/html", "Modified" to "Dec 24 2022")
37 | val actual = HttpResponse(200, h, Optional.of("Test12345")).toString()
38 | val expected = "HTTP/1.1 200 Success\r\nContent-Length: 9\r\nContent-type: text/html\r\nModified: Dec 24 2022\r\n\r\nTest12345"
39 | assertEquals(expected, actual)
40 | }
41 |
42 | @Test
43 | fun testHttpRspReadingFile() {
44 | val req = HttpRequest("GET /textResources/file1.txt HTTP/1.1")
45 | val actual = HttpResponse()
46 | }
47 | }
--------------------------------------------------------------------------------
/src/bexmod/http/HttpResponse.kt:
--------------------------------------------------------------------------------
1 | package bexmod.http
2 |
3 | import java.util.*
4 |
5 | class HttpResponse() {
6 | var version = Version.V1_1
7 | var statusCode = 200
8 | var statusText = "Success"
9 | var headers: SortedMap
10 | = sortedMapOf(
11 | "Content-Type" to "application/octet-stream",
12 | "Content-Length" to "0",
13 | "Server" to "Bexx@${System.getProperty("os.name")}"
14 | )
15 | var body: Optional = Optional.empty()
16 | private var onlyHead = false
17 |
18 |
19 | constructor(status: Int) : this() {
20 | if (status != 200) {
21 | statusCode = status
22 | statusText = statusTextFrom(status)
23 | }
24 | }
25 |
26 | constructor(
27 | status: Int,
28 | rspHeaders: SortedMap,
29 | rspBody: Optional,
30 | head: Boolean
31 | ) : this(status, rspHeaders, rspBody) {
32 | onlyHead = head
33 | }
34 |
35 | constructor(
36 | status: Int,
37 | rspHeaders: SortedMap,
38 | rspBody: Optional
39 | ) : this(status) {
40 | if (rspBody.isPresent) {
41 | body = rspBody
42 | }
43 |
44 | rspHeaders.forEach { e -> headers[e.key] = e.value }
45 | headers["Content-Length"] =
46 | (if (body.isPresent) body.get().length else 0).toString()
47 | }
48 |
49 | override fun toString(): String {
50 | return "${version.version} $statusCode $statusText\r\n" +
51 | "${headersAsStrings()}\r\n" +
52 | if (body.isPresent && !onlyHead) body.get() else ""
53 | }
54 |
55 | private fun headersAsStrings(): String {
56 | val sb = StringBuilder()
57 | headers.forEach {
58 | (k, v) ->
59 | sb.append(k)
60 | sb.append(": ")
61 | sb.append(v)
62 | sb.append("\r\n")
63 | }
64 | return sb.toString()
65 | }
66 | }
--------------------------------------------------------------------------------
/src/bexmod/http/HttpRequest.kt:
--------------------------------------------------------------------------------
1 | package bexmod.http
2 |
3 | import bexmod.WebLogger
4 |
5 | import java.util.logging.Level
6 |
7 |
8 | class HttpRequest(request: String) {
9 | var method = Method.UNINITIALIZED
10 | private set
11 | var version = Version.V1_1
12 | private set
13 | var resource = Resource("/")
14 | private set
15 | var headers = mutableMapOf()
16 | private set
17 | var body = ""
18 | private set
19 | var isBadRequest = true
20 | private set
21 |
22 | init {
23 | request.lines()
24 | .forEach {
25 | if (it.contains("HTTP")) {
26 | try {
27 | val (m, v, r) = processRequest(it)
28 | method = m
29 | version = v
30 | resource = r
31 | isBadRequest = false
32 | } catch (e: Exception) {
33 | WebLogger.LOG.log(Level.SEVERE, "can't parse request line -> $e")
34 | }
35 | } else if (it.contains(":")) {
36 | val (key, value) = processHeader(it)
37 | headers[key] = value
38 | } else if (it.isNotBlank()) {
39 | body = it
40 | }
41 | }
42 | }
43 |
44 | private fun processRequest(line: String): Triple {
45 | val pcs = line.split(" ")
46 | if (pcs.size != 3) {
47 | throw IllegalArgumentException("Check Request Line")
48 | }
49 | WebLogger.LOG.log(Level.INFO, "Request Line: $line")
50 | return Triple(
51 | Method.from(pcs[0]),
52 | Version.from(pcs[2]),
53 | Resource(pcs[1])
54 | )
55 | }
56 |
57 | private fun processHeader(line: String): Pair {
58 | val pcs = line.split(":", limit = 2)
59 | return if (pcs.size > 1)
60 | Pair(pcs[0].lowercase(), pcs[1].trim())
61 | else Pair(pcs[0].lowercase(), "")
62 | }
63 | }
--------------------------------------------------------------------------------
/.idea/libraries/KotlinJavaRuntime.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/bexmod/webserver/Server.kt:
--------------------------------------------------------------------------------
1 | package bexmod.webserver
2 |
3 | import bexmod.WebLogger
4 | import bexmod.http.HttpResponse
5 | import bexmod.http.HttpWorker
6 | import java.io.BufferedReader
7 | import java.io.InputStreamReader
8 | import java.io.OutputStream
9 | import java.net.ServerSocket
10 | import java.util.concurrent.Executors
11 | import java.util.logging.Level
12 |
13 | fun main(args: Array) {
14 | if (args.isEmpty()) {
15 | WebLogger.LOG.log(Level.SEVERE,
16 | "Port number where Socker Server should run is not supplied")
17 | throw IllegalArgumentException("Please provide Port number");
18 | }
19 | val portNumber: Int = args[0].toInt()
20 | Server(portNumber).run()
21 | }
22 |
23 | class Server(private val port: Int) {
24 | companion object {
25 | val N_THREADS = Runtime.getRuntime().availableProcessors()
26 | }
27 | fun run() {
28 | val poolExecutor = Executors.newFixedThreadPool(N_THREADS)
29 | val serverSocket = ServerSocket(port)
30 | WebLogger.LOG.log(Level.INFO, "Started Web Server - Listening on port : $port")
31 |
32 | while (true) {
33 | val socket = serverSocket.accept()
34 | poolExecutor.execute {
35 | WebLogger.LOG.log(Level.INFO,
36 | "Accepted connection from: ${socket.inetAddress.hostAddress}:${socket.port}")
37 |
38 | val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
39 | var bytesRead: Int
40 | val data = CharArray(16384)
41 | val buffer = StringBuilder()
42 |
43 | while (!socket.isInputShutdown) {
44 | bytesRead = reader.read(data, 0, data.size)
45 | if (bytesRead == -1) {
46 | socket.shutdownOutput()
47 | WebLogger.LOG.log(Level.WARNING, "Shutting down output on : $socket")
48 | break
49 | }
50 |
51 | for (i in 0 until bytesRead) buffer.append(data[i])
52 |
53 | if (buffer.contains("\r\n\r\n"))
54 | poolExecutor.execute(HttpWorker(socket, buffer.toString()))
55 | }
56 | WebLogger.LOG.log(Level.WARNING,
57 | "Closing connection with ${socket.inetAddress.hostAddress}:${socket.port}")
58 | socket.close()
59 | }
60 | }
61 | }
62 | }
63 | fun OutputStream.sendResponse(rsp: HttpResponse) {
64 | WebLogger.LOG.log(
65 | Level.INFO,
66 | "\n\t\tResponse Line: ${rsp.version} ${rsp.statusCode} ${rsp.statusText}" +
67 | "\n\t\tHeaders: ${rsp.headers}\n")
68 | write(rsp.toString().toByteArray())
69 | flush()
70 | }
71 |
--------------------------------------------------------------------------------
/src/bexmod/webserver/handlers.kt:
--------------------------------------------------------------------------------
1 | package bexmod.webserver
2 |
3 | import bexmod.WebLogger
4 | import bexmod.http.HttpRequest
5 | import bexmod.http.HttpResponse
6 | import bexmod.http.Method
7 | import bexmod.http.Version
8 |
9 | import java.io.File
10 | import java.nio.charset.Charset
11 | import java.text.SimpleDateFormat
12 | import java.util.Optional
13 | import java.util.Date
14 | import java.util.logging.Level
15 |
16 |
17 | interface Handler {
18 |
19 | fun handle(req: HttpRequest): HttpResponse
20 |
21 | companion object {
22 | fun loadFile(path: String, encoding: Charset = Charsets.UTF_8): Optional {
23 | val f = File(path)
24 | if (f.exists())
25 | return Optional.of(String(f.readBytes(), encoding))
26 | WebLogger.LOG.log(Level.WARNING, "Can't load file from $path")
27 | return Optional.empty()
28 | }
29 | }
30 | }
31 |
32 | class StaticPageHandler : Handler {
33 | private val STATIC = "static"
34 |
35 | override fun handle(req: HttpRequest): HttpResponse {
36 | WebLogger.LOG.log(Level.INFO, "Received ${req.method} Request")
37 | if (req.isBadRequest) return HttpResponse(400)
38 | if (req.resource.path == "..") return HttpResponse(403)
39 | if (req.version != Version.V1_1) return HttpResponse(505)
40 |
41 | val routes = req.resource.path.split("/")
42 | val path = routes[1].ifBlank { "index.html" }
43 | val headers = sortedMapOf()
44 | return when (path) {
45 | "health" -> HttpResponse(200)
46 | path -> {
47 | if (Handler.loadFile("Resources/$STATIC/$path").isPresent) {
48 | val ext = path.split(".")
49 | when (ext[ext.size - 1]) {
50 | "html" -> headers["Content-Type"] = "text/html"
51 | "css" -> headers["Content-Type"] = "text/css"
52 | "js" -> headers["Content-Type"] = "text/javascript"
53 | "txt" -> headers["Content-Type"] = "text/plain"
54 | "pdf" -> headers["Content-Type"] = "text/pdf"
55 | "jpeg", "jpg" -> headers["Content-Type"] = "media/image"
56 | else -> headers["Content-Type"] = "application/octet-stream"
57 | }
58 |
59 | HttpResponse(200, headers, Handler.loadFile("Resources/$STATIC/$path"), req.method == Method.HEAD)
60 | } else {
61 | headers["Content-Type"] = "text/html"
62 | HttpResponse(404, headers, Handler.loadFile("Resources/$STATIC/404.html"))
63 | }
64 | }
65 | else -> HttpResponse(200)
66 | }
67 | }
68 |
69 | }
70 |
71 | class DynamicPageHandler() : Handler {
72 | override fun handle(req: HttpRequest): HttpResponse {
73 | WebLogger.LOG.log(Level.WARNING, "I'm Dynamic")
74 | TODO("Not yet implemented")
75 | }
76 | }
77 |
78 | class RawDataHandler() : Handler {
79 |
80 | override fun handle(req: HttpRequest): HttpResponse {
81 | WebLogger.LOG.log(Level.INFO, "Handling raw data request")
82 | val path = req.resource.path
83 | val routes = path.split("/")
84 | return when (routes[1]) {
85 | "file1.txt" -> createRspWithBody(req, path)
86 | else -> HttpResponse(404)
87 | }
88 | }
89 | private fun createRspWithBody(req: HttpRequest, path: String): HttpResponse {
90 | val file = File(path)
91 | if (file.exists() || file.isHidden) {
92 | return HttpResponse(404)
93 | }
94 |
95 | val fileModified = file.lastModified()
96 | val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z")
97 | val headers = req.headers
98 | if (headers.containsKey("If-Modified-Since")) {
99 | val sinceModified = headers["If-Modified-Since"]
100 | val d = dateFormat.parse(sinceModified)
101 | if (d.time > fileModified) {
102 | return HttpResponse(304)
103 | }
104 | }
105 |
106 | val data = Handler.loadFile(path)
107 | val newHeader = sortedMapOf()
108 | val t: String = dateFormat.format(Date(fileModified))
109 | newHeader["Last-Modified"] = t
110 | newHeader["Content-type"] = "text/html"
111 | return HttpResponse(200, newHeader, Optional.of(data.get()))
112 | }
113 | }
--------------------------------------------------------------------------------
/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------