From 5933dc55a1ac40467a23bd01d2ae97a8e539013b Mon Sep 17 00:00:00 2001 From: sharkovadarya Date: Tue, 26 Mar 2019 18:46:40 +0300 Subject: [PATCH 1/3] http client initial commit --- .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 + .../buildOutputCleanup/outputFiles.bin | Bin 0 -> 18857 bytes http/client/.gradle/vcs-1/gc.properties | 0 http/client/build.gradle | 40 +++++ .../hse/spb/networks/sharkova/http/Client.kt | 150 ++++++++++++++++++ .../ru/hse/spb/networks/sharkova/http/Main.kt | 26 +++ .../sharkova/http/httputils/HttpRequest.kt | 38 +++++ .../sharkova/http/httputils/HttpResponse.kt | 62 ++++++++ .../http/httputils/HttpResponseParser.kt | 96 +++++++++++ .../http/httputils/MalformedHttpException.kt | 3 + 11 files changed, 417 insertions(+) create mode 100644 http/client/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 http/client/.gradle/buildOutputCleanup/cache.properties create mode 100644 http/client/.gradle/buildOutputCleanup/outputFiles.bin create mode 100644 http/client/.gradle/vcs-1/gc.properties create mode 100644 http/client/build.gradle create mode 100644 http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/Client.kt create mode 100644 http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/Main.kt create mode 100644 http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpRequest.kt create mode 100644 http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponse.kt create mode 100644 http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponseParser.kt create mode 100644 http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/MalformedHttpException.kt diff --git a/http/client/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/http/client/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000000000000000000000000000000000000..2c7421b6f40762f9e9dc33edb6229a3cfa81b3fc GIT binary patch literal 17 UcmZSnwY+ZX9E+wa3=p6P06vff5C8xG literal 0 HcmV?d00001 diff --git a/http/client/.gradle/buildOutputCleanup/cache.properties b/http/client/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..3ae2a5d --- /dev/null +++ b/http/client/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Tue Mar 26 18:33:56 MSK 2019 +gradle.version=4.10 diff --git a/http/client/.gradle/buildOutputCleanup/outputFiles.bin b/http/client/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000000000000000000000000000000000000..7b8956e8df2d52bffbbba1b296bff2130b951c2a GIT binary patch literal 18857 zcmeI%Pe_wt9Ki8MDVC{OxWEdvC3MkAvr}qeAdv$#k}etO(7<^RQbY)X8S~(A7L91F zgJ78C!9O4lDlo}JT#E?VDV-O=i;^b~`p*4+NO%!%$@hi#-NX0&?SbcWeXEVhJG z*}u;3FIgdg00IagfB*srAbXTz32AWHsjQNBw^A`qag-+AD)@>7TWST)BZqRqNhVuequHUH5oSZ`7SH z^ZnXCc-Lbi_T?MWx3rJu2giTL6HT&DpZ4iLQ%ze14`-y?w9l@LJ|7w`3QDinKKI3W zFWh{oK=yCYK7Zomoz1znC7BOu|1o_(ePJxiF7pZPzgrjM@rjq)(tEVqyyXsuzwVdp z)1}?<_nGU} { + println("Input product name, price, and amount, each on a new line") + val name = readLine() + val price = readLine()?.toIntOrNull() + val amount = readLine()?.toIntOrNull() + if (name == null) { + outputIncorrectArgumentMessage("name") + } else if (price == null || price < 0) { + outputIncorrectArgumentMessage("price") + } else if (amount == null || amount < 0) { + outputIncorrectArgumentMessage("amount") + } else { + addProduct(name, price.toString(), amount.toString()) + } + } + LIST_PRODUCTS -> { + listProducts() + } + BUY_PRODUCT -> { + println("Input product id. " + + "If you don't know the id, input -1 to return to the main menu " + + "and input 2 to list all products") + val id = readLine()?.toIntOrNull() ?: outputIncorrectArgumentMessage("id") + if (id != -1) { + buyProduct(id.toString()) + } + } + EXIT -> { + stop() + return + } + HELP -> { + outputUsage() + } + else -> { + println("Incorrect command number.") + outputUsage() + } + } + println() + } + } + + private fun addProduct(name: String, price: String, amount: String) { + val request = HttpRequest("POST", "add", + mapOf("name" to name, "price" to price, "amount" to amount), mapOf("Host" to host)) + outputStreamWriter.write(request.toString()) + outputStreamWriter.flush() + val response = HttpResponseParser.parseInput(serverOutput) + if (response.getStatusCode() != HttpResponse.StatusCode.OK) { + println("Could not add product.") + } + + } + + private fun listProducts() { + val request = HttpRequest("GET", "list", fields = mapOf("Host" to host)) + outputStreamWriter.write(request.toString()) + outputStreamWriter.flush() + val response = HttpResponseParser.parseInput(serverOutput) + if (response.getStatusCode() == HttpResponse.StatusCode.OK) { + val products = jsonToProducts(response.body) + for (product in products) { + println("Id: ${product.id}; name: ${product.name.replace("%20", " ")}; " + + "price: ${product.price}; amount: ${product.amount}") + } + } else { + println("Could not retrieve product list. ${response.statusCode} ${response.reasonPhrase}") + } + } + + private fun buyProduct(id: String) { + val request = HttpRequest("POST", "buy", mapOf("id" to id), mapOf("Host" to host)) + outputStreamWriter.write(request.toString()) + outputStreamWriter.flush() + val response = HttpResponseParser.parseInput(serverOutput) + if (response.getStatusCode() == HttpResponse.StatusCode.OK) { + println(String(response.body)) + } else { + println("Could not buy product. ${response.statusCode} ${response.reasonPhrase}") + } + } + + private fun stop() { + serverSocket.close() + } + + private fun outputUsage() { + println("Input a single number corresponding to the following commands:") + println("1: Add product") + println("2: List products") + println("3: Buy product") + println("4: Exit program") + println("5: Output this help message") + } + + private fun outputIncorrectArgumentMessage(argumentName: String) { + println("Incorrect argument: $argumentName") + } + + private fun jsonToProducts(byteArray: ByteArray): List { + val reader = JsonReader(StringReader(String(byteArray))) + val listType = object : TypeToken>(){}.type + return Gson().fromJson(reader, listType) + } + + + private data class Product(val id: Int, val name: String, val price: Int, var amount: Int) +} \ No newline at end of file diff --git a/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/Main.kt b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/Main.kt new file mode 100644 index 0000000..c9ae450 --- /dev/null +++ b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/Main.kt @@ -0,0 +1,26 @@ +package ru.hse.spb.networks.sharkova.http + +import ru.hse.spb.networks.sharkova.http.httputils.MalformedHttpException +import java.io.IOException +import java.lang.IllegalArgumentException + +fun main(args: Array) { + if (args.size != 2 || args[1].toIntOrNull() == null) { + println("Incorrect arguments. Required arguments: [host name] [port number]") + return + } + + try { + val client = Client(args[0], args[1].toInt()) + client.run() + } catch (e: IOException) { + outputErrorMessage() + println("Server might currently be unavailable.") + } catch (e: IllegalArgumentException) { + outputErrorMessage() + } catch (e: MalformedHttpException) { + println("Server was unable to response. ") + } +} + +private fun outputErrorMessage() = println("Unable to connect to the server. Make sure the arguments are correct.") diff --git a/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpRequest.kt b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpRequest.kt new file mode 100644 index 0000000..b084c6f --- /dev/null +++ b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpRequest.kt @@ -0,0 +1,38 @@ +package ru.hse.spb.networks.sharkova.http.httputils + +/** + * This class represents an HTTP request. + */ +data class HttpRequest(private val method: String, + private val urlStart: String, + private val queryParameters: Map = emptyMap(), + private val fields: Map = emptyMap(), + private val body: String = "") { + private val completeUrlString: String + init { + val urlBuilder = StringBuilder("/$urlStart") + if (queryParameters.isNotEmpty()) { + urlBuilder.append("?") + for (parameter in queryParameters) { + urlBuilder.append(parameter.key) + urlBuilder.append("=") + urlBuilder.append(parameter.value.replace(Regex("[\\s]"), "%20")) + urlBuilder.append("&") + } + urlBuilder.setLength(urlBuilder.length - 1) + } + completeUrlString = urlBuilder.toString() + } + + override fun toString(): String { + val requestBuilder = StringBuilder("$method $completeUrlString HTTP/1.1\r\n") + for (field in fields) { + requestBuilder.append("${field.key}: ${field.value}\r\n") + } + requestBuilder.append("\r\n") + requestBuilder.append(body) + return requestBuilder.toString() + } + + +} \ No newline at end of file diff --git a/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponse.kt b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponse.kt new file mode 100644 index 0000000..2812ef0 --- /dev/null +++ b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponse.kt @@ -0,0 +1,62 @@ +package ru.hse.spb.networks.sharkova.http.httputils + +/** + * This class represents an HTTP response. + */ +data class HttpResponse(val httpVersion: HttpVersion, + val statusCode: Int, + val reasonPhrase: String, + val fields: Map, + val body: ByteArray) { + + /** + * This method allows us to check the status code and process it accordingly. + * It is more readable than simply using constants in client code. + */ + fun getStatusCode(): StatusCode = when (statusCode) { + 200 -> StatusCode.OK + 400 -> StatusCode.BAD_REQUEST + 404 -> StatusCode.NOT_FOUND + 405 -> StatusCode.METHOD_NOT_ALLOWED + 422 -> StatusCode.UNPROCESSABLE_ENTITY + 500 -> StatusCode.SERVER_ERROR + 501 -> StatusCode.NOT_IMPLEMENTED + else -> StatusCode.NOT_IMPLEMENTED + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HttpResponse + + if (httpVersion != other.httpVersion) return false + if (statusCode != other.statusCode) return false + if (reasonPhrase != other.reasonPhrase) return false + if (fields != other.fields) return false + if (!body.contentEquals(other.body)) return false + + return true + } + + override fun hashCode(): Int { + var result = httpVersion.hashCode() + result = 31 * result + statusCode + result = 31 * result + reasonPhrase.hashCode() + result = 31 * result + fields.hashCode() + result = 31 * result + body.contentHashCode() + return result + } + + data class HttpVersion(val majorNumber: Int, val minorNumber: Int) + + enum class StatusCode { + OK, + BAD_REQUEST, + NOT_FOUND, + METHOD_NOT_ALLOWED, + UNPROCESSABLE_ENTITY, + SERVER_ERROR, + NOT_IMPLEMENTED + } +} \ No newline at end of file diff --git a/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponseParser.kt b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponseParser.kt new file mode 100644 index 0000000..a970e14 --- /dev/null +++ b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/HttpResponseParser.kt @@ -0,0 +1,96 @@ +package ru.hse.spb.networks.sharkova.http.httputils + +import java.io.* +import java.util.* + +/** + * This class parses an HTTP response from an input stream. + */ +class HttpResponseParser { + companion object { + private val statusLineRegex = Regex("""HTTP/(\d+)\.(\d+)\s+(\d+)\s+([^\s]+)""") + private val fieldRegex = Regex("""\s*([^\s]+)\s*:\s*(.*)\s*""") + + /** + * Reads an HTTP response from input stream. + * @param inputStream stream from which HTTP response is read + * @return parsed HTTP response + */ + fun parseInput(inputStream: DataInputStream): HttpResponse { + val bufferedReader = BufferedReader(InputStreamReader(getHeaderStream(inputStream))) + var line = bufferedReader.readLine() + + while (line != null && line.isEmpty()) { + line = readLine() + } + if (line == null) { + throw MalformedHttpException() + } + + val matchResult = statusLineRegex.matchEntire(line) ?: throw MalformedHttpException() + val groups = matchResult.groupValues + val httpVersion = HttpResponse.HttpVersion(groups[1].toInt(), groups[2].toInt()) + val statusCode = groups[3].toIntOrNull() ?: throw MalformedHttpException() + val reasonPhrase = groups[4] + + val fields = parseFields(bufferedReader) + val body = parseBody(inputStream, fields["Content-Length"]) + + return HttpResponse(httpVersion, statusCode, reasonPhrase, fields, body) + } + + private fun parseFields(bufferedReader: BufferedReader): Map { + val fields = HashMap() + + var line = bufferedReader.readLine() + while (line != null && !line.isEmpty()) { + val matchResult = fieldRegex.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 = bodySize.toIntOrNull() ?: throw MalformedHttpException() + val result = ByteArray(size) + input.readFully(result) + result + } + + private fun getHeaderStream(inputStream: DataInputStream): InputStream { + val output = ByteArrayOutputStream() + val slidingWindow = ArrayDeque() + + var ch = inputStream.read() + while (ch != -1) { + if (slidingWindow.size == 4) { + slidingWindow.removeFirst() + } + slidingWindow.addLast(ch) + output.write(ch) + + val buffer = slidingWindow.toIntArray() + if (isContainsEmptyLine(buffer)) { + break + } + ch = inputStream.read() + } + + if (ch == -1) { + throw MalformedHttpException() + } + + return ByteArrayInputStream(output.toByteArray()) + } + + private fun isContainsEmptyLine(array: IntArray): Boolean = + array.size == 4 && array[0] == '\r'.toInt() && array[1] == '\n'.toInt() && array[2] == '\r'.toInt() && array[3] == '\n'.toInt() + } + +} \ No newline at end of file diff --git a/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/MalformedHttpException.kt b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/MalformedHttpException.kt new file mode 100644 index 0000000..2308e68 --- /dev/null +++ b/http/client/src/main/kotlin/ru/hse/spb/networks/sharkova/http/httputils/MalformedHttpException.kt @@ -0,0 +1,3 @@ +package ru.hse.spb.networks.sharkova.http.httputils + +class MalformedHttpException : Exception() \ No newline at end of file From b350054d66ff701de3de565ab6fa88e8a2f0a381 Mon Sep 17 00:00:00 2001 From: sharkovadarya Date: Tue, 26 Mar 2019 18:47:03 +0300 Subject: [PATCH 2/3] removed extra files --- .../buildOutputCleanup/buildOutputCleanup.lock | Bin 17 -> 0 bytes .../.gradle/buildOutputCleanup/cache.properties | 2 -- .../.gradle/buildOutputCleanup/outputFiles.bin | Bin 18857 -> 0 bytes http/client/.gradle/vcs-1/gc.properties | 0 4 files changed, 2 deletions(-) delete mode 100644 http/client/.gradle/buildOutputCleanup/buildOutputCleanup.lock delete mode 100644 http/client/.gradle/buildOutputCleanup/cache.properties delete mode 100644 http/client/.gradle/buildOutputCleanup/outputFiles.bin delete mode 100644 http/client/.gradle/vcs-1/gc.properties diff --git a/http/client/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/http/client/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 2c7421b6f40762f9e9dc33edb6229a3cfa81b3fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 UcmZSnwY+ZX9E+wa3=p6P06vff5C8xG diff --git a/http/client/.gradle/buildOutputCleanup/cache.properties b/http/client/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index 3ae2a5d..0000000 --- a/http/client/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Tue Mar 26 18:33:56 MSK 2019 -gradle.version=4.10 diff --git a/http/client/.gradle/buildOutputCleanup/outputFiles.bin b/http/client/.gradle/buildOutputCleanup/outputFiles.bin deleted file mode 100644 index 7b8956e8df2d52bffbbba1b296bff2130b951c2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18857 zcmeI%Pe_wt9Ki8MDVC{OxWEdvC3MkAvr}qeAdv$#k}etO(7<^RQbY)X8S~(A7L91F zgJ78C!9O4lDlo}JT#E?VDV-O=i;^b~`p*4+NO%!%$@hi#-NX0&?SbcWeXEVhJG z*}u;3FIgdg00IagfB*srAbXTz32AWHsjQNBw^A`qag-+AD)@>7TWST)BZqRqNhVuequHUH5oSZ`7SH z^ZnXCc-Lbi_T?MWx3rJu2giTL6HT&DpZ4iLQ%ze14`-y?w9l@LJ|7w`3QDinKKI3W zFWh{oK=yCYK7Zomoz1znC7BOu|1o_(ePJxiF7pZPzgrjM@rjq)(tEVqyyXsuzwVdp z)1}?<_nGU} Date: Tue, 26 Mar 2019 18:49:10 +0300 Subject: [PATCH 3/3] updated .gitignore --- .gitignore | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.gitignore b/.gitignore index f2e1fa5..6cbb994 100644 --- a/.gitignore +++ b/.gitignore @@ -597,6 +597,39 @@ __pycache__/ ### VisualStudio Patch ### # By default, sensitive information, such as encrypted password + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +!gradle/wrapper/gradle-wrapper.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +build +.gradle +.idea +*.iml +out/ + + # should be stored in the .pubxml.user file. # End of https://www.gitignore.io/api/c++,latex,cmake,netbeans,qtcreator,visualstudio