-
Notifications
You must be signed in to change notification settings - Fork 26
HW2, Kazakov Dmitry #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DmKazakov
wants to merge
14
commits into
h31:master
Choose a base branch
from
DmKazakov:http
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
1ec2c83
Added gitignore
DmKazakov e6bf051
Updated gitignore
DmKazakov 43c187d
Updated gitignore
DmKazakov 67e14d9
Updated gitignore
DmKazakov 3197da0
Init commit
DmKazakov 64fa9a2
Deleted extra files
DmKazakov d30a98b
Create README.md
DmKazakov 252b988
Implemented http server
DmKazakov f219151
Merge branch 'http' of github.com:DmKazakov/NetworksLab2019HSE into http
DmKazakov d7c1b20
Fixed typo
DmKazakov a8cefb4
Added exception catching
DmKazakov c7d5f8d
Merge branch 'http' of github.com:DmKazakov/NetworksLab2019HSE into http
DmKazakov 7d4edd8
Removed extra files
DmKazakov a030fae
Applied suggested changes
DmKazakov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| **/out/ | ||
| **/.gradle/ | ||
| **/build/ | ||
| **/libraries/ | ||
| **/.idea/ | ||
| local.properties |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Электронный магазин | ||
|
|
||
| ## Добавление товара | ||
| ### Формат запроса: | ||
| **POST /add?name=\<name>&price=\<price>&amount=\<amount>** | ||
| * **\<name>** — наименование товара | ||
| * **\<price>** — строковое представление целочисленной цены за единицу товара | ||
| * **\<amount>** — строковое представление количества добавляемого товара | ||
|
|
||
| ## Получение информации о товарах | ||
| ### Формат запроса: | ||
| **GET /list** | ||
|
|
||
| ### Формат ответа: | ||
| Тело ответа содержит JSON массив с элементами, имеющими следующие поля: | ||
| * **id** — строковое представление целочисленного идентификатора товара | ||
| * **name** — наименование товара | ||
| * **price** — строковое представление целочисленной цены за единицу товара | ||
| * **amount** — строковое представление количество товара | ||
|
|
||
| ## Покупка товара | ||
| ### Формат запроса: | ||
| **POST /buy?id=\<id>** | ||
| * **id** — идентификатор товара | ||
|
|
||
| ### Формат ответа: | ||
| Тело ответа содержит одну из следующих строк: | ||
| * **Confirmed** — товар успешно куплен | ||
| * **Not enough product in stock** — товар уже распродан | ||
| * **No product found** - запрошенного товара не существует |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| plugins { | ||
| id 'org.jetbrains.kotlin.jvm' version '1.3.21' | ||
| } | ||
|
|
||
| group 'ru.hse.spb.kazakov.server' | ||
| version '1.0' | ||
|
|
||
| repositories { | ||
| mavenCentral() | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" | ||
| implementation 'com.google.code.gson:gson:2.8.5' | ||
| testCompile group: 'junit', name: 'junit', version: '4.12' | ||
| } | ||
|
|
||
| compileKotlin { | ||
| kotlinOptions.jvmTarget = "1.8" | ||
| } | ||
| compileTestKotlin { | ||
| kotlinOptions.jvmTarget = "1.8" | ||
| } | ||
|
|
||
| jar { | ||
| manifest { | ||
| attributes 'Main-Class': 'ru.hse.spb.kazakov.server.MainKt' | ||
| } | ||
| from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } | ||
| } |
135 changes: 135 additions & 0 deletions
135
http/src/main/kotlin/ru/hse/spb/kazakov/server/Server.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| package ru.hse.spb.kazakov.server | ||
|
|
||
| import com.google.gson.Gson | ||
| import ru.hse.spb.kazakov.server.http.* | ||
| import java.io.DataInputStream | ||
| import java.io.OutputStreamWriter | ||
| import java.lang.Exception | ||
| import java.net.ServerSocket | ||
| import java.net.Socket | ||
| import java.net.SocketException | ||
| import java.nio.charset.Charset | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock | ||
|
|
||
| @ExperimentalUnsignedTypes | ||
| class Server(port: Int) { | ||
| private val serverSocket = ServerSocket(port) | ||
| private val clientsSockets = mutableListOf<Socket>() | ||
| private val serverThreads = mutableListOf<Thread>() | ||
| private val products = mutableListOf<Product>() | ||
| private val productsAccess = ReentrantReadWriteLock() | ||
|
|
||
| fun run() { | ||
| val acceptClientCycle = { | ||
| try { | ||
| while (true) { | ||
| val clientSocket = serverSocket.accept() | ||
| clientsSockets.add(clientSocket) | ||
| val clientThread = Thread(RequestResponseCycle(clientSocket)) | ||
| serverThreads.add(clientThread) | ||
| clientThread.start() | ||
| } | ||
| } catch (exception: SocketException) {} | ||
| } | ||
|
|
||
| val acceptClientsThread = Thread(acceptClientCycle) | ||
| serverThreads.add(acceptClientsThread) | ||
| acceptClientsThread.start() | ||
| } | ||
|
|
||
| fun stop() { | ||
| serverSocket.close() | ||
| clientsSockets.forEach { it.close() } | ||
| serverThreads.forEach { it.join() } | ||
| } | ||
|
|
||
| private fun productsToJson(): String { | ||
| productsAccess.readLock().lock() | ||
| val productsJson = Gson().toJson(products) | ||
| productsAccess.readLock().unlock() | ||
| return productsJson | ||
| } | ||
|
|
||
| private fun addProduct(name: String, price: UInt, amount: UInt) { | ||
| productsAccess.writeLock().lock() | ||
| val product = Product(products.size, name, price, amount) | ||
| products.add(product) | ||
| productsAccess.writeLock().unlock() | ||
| } | ||
|
|
||
| private data class Product(val id: Int, val name: String, val price: UInt, var amount: UInt) | ||
|
|
||
| private inner class RequestResponseCycle(clientSocket: Socket) : Runnable { | ||
| private val dataInputStream = DataInputStream(clientSocket.getInputStream()) | ||
| private val outputStream = OutputStreamWriter(clientSocket.getOutputStream(), Charset.forName("UTF-8")) | ||
|
|
||
| override fun run() { | ||
| while (true) { | ||
| val request = try { | ||
| parseHttpRequest(dataInputStream) | ||
| } catch (exception: MalformedHttpException) { | ||
| outputStream.write(HttpResponse(HttpResponseType.BAD_REQUEST).toString()) | ||
| continue | ||
| } catch (exception: Exception) { | ||
| outputStream.write(HttpResponse(HttpResponseType.SERVER_ERROR).toString()) | ||
| break | ||
| } | ||
|
|
||
| val response = processRequest(request) | ||
| outputStream.write(response.toString()) | ||
| outputStream.flush() | ||
| } | ||
| } | ||
|
|
||
| private fun processRequest(request: HttpRequest): HttpResponse = | ||
| when (request.requestLine.method) { | ||
| "GET" -> { | ||
| if (request.requestLine.url.path != "/list") { | ||
| HttpResponse(HttpResponseType.NOT_FOUND) | ||
| } else { | ||
| HttpResponse(HttpResponseType.OK, ContentType.JSON, productsToJson()) | ||
| } | ||
| } | ||
|
|
||
| "POST" -> { | ||
| when (request.requestLine.url.path) { | ||
| "/add" -> { | ||
| val queryParameters = request.requestLine.url.queryParameters | ||
| val name = queryParameters["name"] | ||
| val price = queryParameters["price"]?.toUInt() | ||
| val amount = queryParameters["amount"]?.toUInt() | ||
| if (name == null || price == null || amount == null || price == 0U || amount == 0U) { | ||
| HttpResponse(HttpResponseType.UNPROCESSABLE_ENTITY) | ||
| } else { | ||
| addProduct(name, price, amount) | ||
| HttpResponse(HttpResponseType.OK) | ||
| } | ||
| } | ||
|
|
||
| "/buy" -> { | ||
| val id = request.requestLine.url.queryParameters["id"]?.toInt() | ||
| productsAccess.writeLock().lock() | ||
| if (id == null) { | ||
| productsAccess.writeLock().unlock() | ||
| HttpResponse(HttpResponseType.UNPROCESSABLE_ENTITY) | ||
| } else if (id < 0 || id >= products.size) { | ||
| productsAccess.writeLock().unlock() | ||
| HttpResponse(HttpResponseType.OK, ContentType.TEXT, "No product found") | ||
| } else { | ||
| val product = products[id] | ||
| val resultMessage = | ||
| if (product.amount > 0U) "Confirmed".also { product.amount-- } else "Not enough product in stock" | ||
| productsAccess.writeLock().unlock() | ||
| HttpResponse(HttpResponseType.OK, ContentType.TEXT, resultMessage) | ||
| } | ||
| } | ||
|
|
||
| else -> HttpResponse(HttpResponseType.NOT_FOUND) | ||
| } | ||
| } | ||
|
|
||
| else -> HttpResponse(HttpResponseType.NOT_IMPLEMENTED) | ||
| } | ||
| } | ||
|
|
||
| } |
27 changes: 27 additions & 0 deletions
27
http/src/main/kotlin/ru/hse/spb/kazakov/server/http/HttpRequest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package ru.hse.spb.kazakov.server.http | ||
|
|
||
| data class HttpRequest( | ||
| val requestLine: RequestLine, | ||
| val messageBody: ByteArray, | ||
| val fields: Map<String, String> | ||
| ) { | ||
| override fun equals(other: Any?): Boolean { | ||
| if (this === other) return true | ||
| if (javaClass != other?.javaClass) return false | ||
|
|
||
| other as HttpRequest | ||
|
|
||
| if (requestLine != other.requestLine) return false | ||
| if (!messageBody.contentEquals(other.messageBody)) return false | ||
| if (fields != other.fields) return false | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| override fun hashCode(): Int { | ||
| var result = requestLine.hashCode() | ||
| result = 31 * result + messageBody.contentHashCode() | ||
| result = 31 * result + fields.hashCode() | ||
| return result | ||
| } | ||
| } | ||
94 changes: 94 additions & 0 deletions
94
http/src/main/kotlin/ru/hse/spb/kazakov/server/http/HttpRequestParser.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package ru.hse.spb.kazakov.server.http | ||
|
|
||
| import com.sun.xml.internal.messaging.saaj.util.TeeInputStream | ||
| import java.io.* | ||
| import java.net.MalformedURLException | ||
| import java.util.* | ||
|
|
||
|
|
||
| fun parseHttpRequest(inputStream: DataInputStream): HttpRequest { | ||
| val bufferedReader = BufferedReader(InputStreamReader(getHeaderStream(inputStream))) | ||
|
|
||
| val requestLine = try { | ||
| parseRequestLine(bufferedReader) | ||
| } catch (exception: MalformedURLException) { | ||
| throw MalformedHttpException() | ||
| } | ||
| val fields = parseFields(bufferedReader) | ||
| val body = parseBody(inputStream, fields["Content-Length"]) | ||
|
|
||
| return HttpRequest(requestLine, body, fields) | ||
| } | ||
|
|
||
| private fun getHeaderStream(inputStream: DataInputStream): InputStream { | ||
| val output = ByteArrayOutputStream() | ||
| val input = TeeInputStream(inputStream, output) | ||
| val slidingWindow = ArrayDeque<Int>() | ||
|
|
||
| var ch = input.read() | ||
| while (ch != -1) { | ||
| if (slidingWindow.size == "\r\n\r\n".length) { | ||
| slidingWindow.removeFirst() | ||
| } | ||
| slidingWindow.addLast(ch) | ||
|
|
||
| val stringVal = slidingWindow.toIntArray().joinToString("") { it.toChar().toString() } | ||
| if (stringVal == "\r\n\r\n") { | ||
| break | ||
| } | ||
| ch = input.read() | ||
| } | ||
|
|
||
| if (ch == -1) { | ||
| throw MalformedURLException() | ||
| } | ||
|
|
||
| return ByteArrayInputStream(output.toByteArray()) | ||
| } | ||
|
|
||
| private val requestLineRegexp = Regex("""([^\s]+)\s+([^\s]+)\s+HTTP/(\d+)\.(\d+)""") | ||
| private fun parseRequestLine(inputStream: BufferedReader): RequestLine { | ||
| var line = inputStream.readLine() | ||
| while (line != null && line.isEmpty()) { | ||
| line = inputStream.readLine() | ||
| } | ||
| if (line == null) { | ||
| throw MalformedHttpException() | ||
| } | ||
|
|
||
| val matchResult = requestLineRegexp.matchEntire(line) ?: throw MalformedHttpException() | ||
| val groups = matchResult.groupValues | ||
| val httpVersion = RequestLine.HttpVersion(groups[3].toInt(), groups[4].toInt()) | ||
|
|
||
| return RequestLine(matchResult.groupValues[1], httpVersion, URL("http://" + groups[2])) | ||
| } | ||
|
|
||
| private val fieldRegexp = Regex("""\s*([^\s]+)\s*:\s*(.*)\s*""") | ||
| private fun parseFields(bufferedReader: BufferedReader): Map<String, String> { | ||
| val fields = HashMap<String, String>() | ||
|
|
||
| var line = bufferedReader.readLine() | ||
| while (line != null && !line.isEmpty()) { | ||
| val matchResult = fieldRegexp.matchEntire(line) ?: throw MalformedHttpException() | ||
| val groups = matchResult.groupValues | ||
| fields[groups[1]] = groups[2] | ||
| line = bufferedReader.readLine() | ||
| } | ||
|
|
||
| return fields | ||
| } | ||
|
|
||
| private fun parseBody(input: DataInputStream, bodySize: String?): ByteArray = | ||
| if (bodySize == null) { | ||
| byteArrayOf() | ||
| } else { | ||
| val size = try { | ||
| bodySize.toInt() | ||
| } catch (exception: NumberFormatException) { | ||
| throw MalformedHttpException() | ||
| } | ||
| val result = ByteArray(size) | ||
| input.readFully(result) | ||
| result | ||
| } | ||
|
|
54 changes: 54 additions & 0 deletions
54
http/src/main/kotlin/ru/hse/spb/kazakov/server/http/HttpResponse.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package ru.hse.spb.kazakov.server.http | ||
|
|
||
| import java.nio.charset.Charset | ||
|
|
||
| class HttpResponse( | ||
| private val responseType: HttpResponseType, | ||
| private val contentType: ContentType = ContentType.TEXT, | ||
| private val body: String = "" | ||
| ) { | ||
| override fun toString(): String = | ||
| responseType.toString() + contentType + getContentLengthField() + "\r\n" + body | ||
|
|
||
| private fun getContentLengthField() = "Content-Length: ${body.toByteArray(Charset.forName("UTF-8")).size}\r\n" | ||
| } | ||
|
|
||
| enum class HttpResponseType { | ||
| OK { | ||
| override fun toString(): String = "HTTP/1.1 200 OK\r\n" | ||
DmKazakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
|
|
||
| BAD_REQUEST { | ||
| override fun toString(): String = "HTTP/1.1 400 Bad Request\r\n" | ||
| }, | ||
|
|
||
| NOT_FOUND { | ||
| override fun toString(): String = "HTTP/1.1 404 Not Found\r\n" | ||
| }, | ||
|
|
||
| METHOD_NOT_ALLOWED { | ||
| override fun toString(): String = "HTTP/1.1 405 Method Not Allowed\r\n" | ||
| }, | ||
|
|
||
| UNPROCESSABLE_ENTITY { | ||
| override fun toString(): String = "HTTP/1.1 422 Unprocessable Entity\r\n" | ||
| }, | ||
|
|
||
| SERVER_ERROR { | ||
| override fun toString(): String = "HTTP/1.1 500 Internal Server Error\r\n" | ||
| }, | ||
|
|
||
| NOT_IMPLEMENTED { | ||
| override fun toString(): String = "HTTP/1.1 501 Not Implemented\r\n" | ||
| } | ||
| } | ||
|
|
||
| enum class ContentType { | ||
| TEXT { | ||
| override fun toString(): String = "Content-Type: text/plain\r\n" | ||
| }, | ||
|
|
||
| JSON { | ||
| override fun toString(): String = "Content-Type: application/json\r\n" | ||
| } | ||
| } | ||
3 changes: 3 additions & 0 deletions
3
http/src/main/kotlin/ru/hse/spb/kazakov/server/http/MalformedHttpException.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package ru.hse.spb.kazakov.server.http | ||
|
|
||
| class MalformedHttpException : Exception() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.