17d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawkepackage fi.iki.elonen; 27d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 38b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie/* 48b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * #%L 58b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * NanoHttpd-Core 68b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * %% 78b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * Copyright (C) 2012 - 2015 nanohttpd 88b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * %% 98b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * Redistribution and use in source and binary forms, with or without modification, 108b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * are permitted provided that the following conditions are met: 119e1ec7bff40d70d31953a04dd448665aaf549395ritchie * 128b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * 1. Redistributions of source code must retain the above copyright notice, this 138b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * list of conditions and the following disclaimer. 149e1ec7bff40d70d31953a04dd448665aaf549395ritchie * 158b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * 2. Redistributions in binary form must reproduce the above copyright notice, 168b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * this list of conditions and the following disclaimer in the documentation 178b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * and/or other materials provided with the distribution. 189e1ec7bff40d70d31953a04dd448665aaf549395ritchie * 198b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * 3. Neither the name of the nanohttpd nor the names of its contributors 208b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * may be used to endorse or promote products derived from this software without 218b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * specific prior written permission. 229e1ec7bff40d70d31953a04dd448665aaf549395ritchie * 238b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 248b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 258b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 268b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 278b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 288b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 298b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 308b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 318b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 328b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * OF THE POSSIBILITY OF SUCH DAMAGE. 338b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie * #L% 348b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie */ 358b3f33ce61812b29a5cacd1e7e6d350b40531f9britchie 365e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchieimport static junit.framework.Assert.assertEquals; 377d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 386964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawkeimport java.io.BufferedReader; 396964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawkeimport java.io.FileReader; 406bed46385ee916d2e82c7a31d76a0bfb1079a8d9Gaziz Tazhenovimport java.util.ArrayList; 416964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawkeimport java.util.List; 427d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawkeimport java.util.UUID; 437d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 445e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchieimport org.junit.Test; 457d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 467d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawkepublic class HttpPostRequestTest extends HttpServerTest { 477d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 487d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke public static final String CONTENT_LENGTH = "Content-Length: "; 499058464950a9734da0a7ff2dc47f3081bbb5117critchie 507d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke public static final String FIELD = "caption"; 519058464950a9734da0a7ff2dc47f3081bbb5117critchie 527d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke public static final String VALUE = "Summer vacation"; 539058464950a9734da0a7ff2dc47f3081bbb5117critchie 547d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke public static final String FIELD2 = "location"; 559058464950a9734da0a7ff2dc47f3081bbb5117critchie 567d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke public static final String VALUE2 = "Grand Canyon"; 579058464950a9734da0a7ff2dc47f3081bbb5117critchie 58e9a1b5310ed98d7c80095fbefd856a0b1277a1d2unknown public static final String POST_RAW_CONTENT_FILE_ENTRY = "postData"; 599058464950a9734da0a7ff2dc47f3081bbb5117critchie 60e9a1b5310ed98d7c80095fbefd856a0b1277a1d2unknown public static final String VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS = "Test raw data & Result value"; 61e9a1b5310ed98d7c80095fbefd856a0b1277a1d2unknown 625e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie /** 635e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie * contains common preparation steps for testing POST with Multipart Form 649e1ec7bff40d70d31953a04dd448665aaf549395ritchie * 655e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie * @param fileName 665e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie * Name of file to be uploaded 675e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie * @param fileContent 685e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie * Content of file to be uploaded 695e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie * @return input String with POST request complete information including 705e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie * header, length and content 715e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie */ 725e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie private String preparePostWithMultipartForm(String fileName, String fileContent) { 735e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String divider = UUID.randomUUID().toString(); 743cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data, boundary=" + divider + "\r\n"; 755e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String content = 763cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"; filename=\"" + fileName + "\"\r\n" 773cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen + "Content-Type: image/jpeg\r\n" + "\r\n" + fileContent + "\r\n" + "--" + divider + "--\r\n"; 78e9a1b5310ed98d7c80095fbefd856a0b1277a1d2unknown int size = content.length() + header.length(); 79e9a1b5310ed98d7c80095fbefd856a0b1277a1d2unknown int contentLengthHeaderValueSize = String.valueOf(size).length(); 805e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); 815e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 5) + "\r\n\r\n" + content; 825e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie 835e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie return input; 845e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie } 855e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie 865e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie @Test 875e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie public void testPostWithMultipartFormUpload() throws Exception { 885e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String filename = "GrandCanyon.txt"; 895e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String fileContent = HttpPostRequestTest.VALUE; 905e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String input = preparePostWithMultipartForm(filename, fileContent); 915e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie 92e9a1b5310ed98d7c80095fbefd856a0b1277a1d2unknown invokeServer(input); 935e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie 945e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(1, this.testServer.parms.size()); 955e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie BufferedReader reader = new BufferedReader(new FileReader(this.testServer.files.get(HttpPostRequestTest.FIELD))); 965e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie List<String> lines = readLinesFromFile(reader); 975e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertLinesOfText(new String[]{ 985e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie fileContent 995e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie }, lines); 100e9a1b5310ed98d7c80095fbefd856a0b1277a1d2unknown } 1017d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 1027d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke @Test 1035e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie public void testPostWithMultipartFormUploadFilenameHasSpaces() throws Exception { 1045e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String fileNameWithSpace = "Grand Canyon.txt"; 1055e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String fileContent = HttpPostRequestTest.VALUE; 1065e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String input = preparePostWithMultipartForm(fileNameWithSpace, fileContent); 1075e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie 1087d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke invokeServer(input); 1097d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 1105e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String fileNameAfter = new ArrayList<String>(this.testServer.parms.values()).get(0); 1115e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie 1125e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(fileNameWithSpace, fileNameAfter); 1137d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke } 1147d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 1157d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke @Test 1167d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke public void testPostWithMultipleMultipartFormFields() throws Exception { 1177d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke String divider = UUID.randomUUID().toString(); 1185e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data; boundary=" + divider + "\n"; 1199058464950a9734da0a7ff2dc47f3081bbb5117critchie String content = 1203cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n" 1213cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD2 + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE2 1223cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen + "\r\n" + "--" + divider + "--\r\n"; 1237d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke int size = content.length() + header.length(); 1247d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke int contentLengthHeaderValueSize = String.valueOf(size).length(); 1255e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); 1265e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; 1277d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke invokeServer(input); 1287d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke 1295e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(2, this.testServer.parms.size()); 1305e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD)); 1315e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(HttpPostRequestTest.VALUE2, this.testServer.parms.get(HttpPostRequestTest.FIELD2)); 1327d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke } 1336964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke 1346964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke @Test 1356964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke public void testPostWithMultipleMultipartFormFieldsWhereContentTypeWasSeparatedByComma() throws Exception { 1366964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke String divider = UUID.randomUUID().toString(); 1373cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data, boundary=" + divider + "\r\n"; 1389058464950a9734da0a7ff2dc47f3081bbb5117critchie String content = 1393cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n" 1403cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen + "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD2 + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE2 1413cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen + "\r\n" + "--" + divider + "--\r\n"; 1426964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke int size = content.length() + header.length(); 1436964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke int contentLengthHeaderValueSize = String.valueOf(size).length(); 1445e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); 1455e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; 1466964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke invokeServer(input); 1476964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke 1485e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(2, this.testServer.parms.size()); 1495e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD)); 1505e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(HttpPostRequestTest.VALUE2, this.testServer.parms.get(HttpPostRequestTest.FIELD2)); 1516964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke } 1529058464950a9734da0a7ff2dc47f3081bbb5117critchie 1536964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke @Test 1545e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie public void testSimplePostWithSingleMultipartFormField() throws Exception { 1555e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String divider = UUID.randomUUID().toString(); 1563cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen String header = "POST " + HttpServerTest.URI + " HTTP/1.1\nContent-Type: " + "multipart/form-data; boundary=" + divider + "\r\n"; 1575e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String content = 1583cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen "--" + divider + "\r\n" + "Content-Disposition: form-data; name=\"" + HttpPostRequestTest.FIELD + "\"\r\n" + "\r\n" + HttpPostRequestTest.VALUE + "\r\n" 1593cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen + "--" + divider + "--\r\n"; 1605e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie int size = content.length() + header.length(); 1615e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie int contentLengthHeaderValueSize = String.valueOf(size).length(); 1625e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); 1635e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; 1646bed46385ee916d2e82c7a31d76a0bfb1079a8d9Gaziz Tazhenov invokeServer(input); 1659058464950a9734da0a7ff2dc47f3081bbb5117critchie 1665e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(1, this.testServer.parms.size()); 1675e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(HttpPostRequestTest.VALUE, this.testServer.parms.get(HttpPostRequestTest.FIELD)); 1686bed46385ee916d2e82c7a31d76a0bfb1079a8d9Gaziz Tazhenov } 1699058464950a9734da0a7ff2dc47f3081bbb5117critchie 1706bed46385ee916d2e82c7a31d76a0bfb1079a8d9Gaziz Tazhenov @Test 1715e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie public void testSimpleRawPostData() throws Exception { 1725e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String header = "POST " + HttpServerTest.URI + " HTTP/1.1\n"; 1733cbc1769b317f45ad257f33bd648ac97b595f84bJarno Elonen String content = HttpPostRequestTest.VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS + "\r\n"; 1746964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke int size = content.length() + header.length(); 1756964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke int contentLengthHeaderValueSize = String.valueOf(size).length(); 1765e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie int contentLength = size + contentLengthHeaderValueSize + HttpPostRequestTest.CONTENT_LENGTH.length(); 1775e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie String input = header + HttpPostRequestTest.CONTENT_LENGTH + (contentLength + 4) + "\r\n\r\n" + content; 1785e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie invokeServer(input); 1795e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(0, this.testServer.parms.size()); 1805e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(1, this.testServer.files.size()); 1815e2e2f19e06b9f4a01c6da83a2297eb18fe2b546ritchie assertEquals(HttpPostRequestTest.VALUE_TEST_SIMPLE_RAW_DATA_WITH_AMPHASIS, this.testServer.files.get(HttpPostRequestTest.POST_RAW_CONTENT_FILE_ENTRY)); 1826964c6f930efb87083c36bbcd1f9298c7246d280Paul Hawke } 1836bed46385ee916d2e82c7a31d76a0bfb1079a8d9Gaziz Tazhenov 1847d7cd1f01f66c527c49a5887d1d6f60e26be8c7fPaul Hawke} 185