SwifterTestsHttpParser.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. //
  2. // SwifterTests.swift
  3. // SwifterTests
  4. //
  5. // Copyright © 2016 Damian Kołakowski. All rights reserved.
  6. //
  7. import XCTest
  8. @testable import Swifter
  9. class SwifterTestsHttpParser: XCTestCase {
  10. /// A specialized Socket which creates a linked socket pair with a pipe, and
  11. /// immediately writes in fixed data. This enables tests to static fixture
  12. /// data into the regular Socket flow.
  13. class TestSocket: Socket {
  14. init(_ content: String) {
  15. /// Create an array to hold the read and write sockets that pipe creates
  16. var fds = [Int32](repeating: 0, count: 2)
  17. fds.withUnsafeMutableBufferPointer { ptr in
  18. let received = pipe(ptr.baseAddress!)
  19. guard received >= 0 else { fatalError("Pipe error!") }
  20. }
  21. // Extract the read and write handles into friendly variables
  22. let fdRead = fds[0]
  23. let fdWrite = fds[1]
  24. // Set non-blocking I/O on both sockets. This is required!
  25. _ = fcntl(fdWrite, F_SETFL, O_NONBLOCK)
  26. _ = fcntl(fdRead, F_SETFL, O_NONBLOCK)
  27. // Push the content bytes into the write socket.
  28. _ = content.withCString { stringPointer in
  29. // Count will be either >=0 to indicate bytes written, or -1
  30. // if the bytes will be written later (non-blocking).
  31. let count = write(fdWrite, stringPointer, content.lengthOfBytes(using: .utf8) + 1)
  32. guard count != -1 || errno == EAGAIN else { fatalError("Write error!") }
  33. }
  34. // Close the write socket immediately. The OS will add an EOF byte
  35. // and the read socket will remain open.
  36. #if os(Linux)
  37. Glibc.close(fdWrite)
  38. #else
  39. Darwin.close(fdWrite) // the super instance will close fdRead in deinit!
  40. #endif
  41. super.init(socketFileDescriptor: fdRead)
  42. }
  43. }
  44. // swiftlint:disable function_body_length
  45. func testParser() {
  46. let parser = HttpParser()
  47. do {
  48. _ = try parser.readHttpRequest(TestSocket(""))
  49. XCTAssert(false, "Parser should throw an error if socket is empty.")
  50. } catch { }
  51. do {
  52. _ = try parser.readHttpRequest(TestSocket("12345678"))
  53. XCTAssert(false, "Parser should throw an error if status line has single token.")
  54. } catch { }
  55. do {
  56. _ = try parser.readHttpRequest(TestSocket("GET HTTP/1.0"))
  57. XCTAssert(false, "Parser should throw an error if status line has not enough tokens.")
  58. } catch { }
  59. do {
  60. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0"))
  61. XCTAssert(false, "Parser should throw an error if there is no next line symbol.")
  62. } catch { }
  63. do {
  64. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0"))
  65. XCTAssert(false, "Parser should throw an error if there is no next line symbol.")
  66. } catch { }
  67. do {
  68. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\r"))
  69. XCTAssert(false, "Parser should throw an error if there is no next line symbol.")
  70. } catch { }
  71. do {
  72. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\n"))
  73. XCTAssert(false, "Parser should throw an error if there is no 'Content-Length' header.")
  74. } catch { }
  75. do {
  76. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n"))
  77. } catch {
  78. XCTAssert(false, "Parser should not throw any errors if there is a valid 'Content-Length' header.")
  79. }
  80. do {
  81. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nContent-Length: 0\r\n\n"))
  82. } catch {
  83. XCTAssert(false, "Parser should not throw any errors if there is a valid 'Content-Length' header.")
  84. }
  85. do {
  86. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nContent-Length: 5\n\n12345"))
  87. } catch {
  88. XCTAssert(false, "Parser should not throw any errors if there is a valid 'Content-Length' header.")
  89. }
  90. do {
  91. _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nContent-Length: 10\r\n\n"))
  92. XCTAssert(false, "Parser should throw an error if request' body is too short.")
  93. } catch { }
  94. do { // test payload less than 1 read segmant
  95. let contentLength = Socket.kBufferLength - 128
  96. let bodyString = [String](repeating: "A", count: contentLength).joined(separator: "")
  97. let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
  98. let request = try parser.readHttpRequest(TestSocket(payload))
  99. XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")
  100. let unicodeBytes = bodyString.utf8.map { return $0 }
  101. XCTAssert(request.body == unicodeBytes, "Request body must be correct")
  102. } catch { }
  103. do { // test payload equal to 1 read segmant
  104. let contentLength = Socket.kBufferLength
  105. let bodyString = [String](repeating: "B", count: contentLength).joined(separator: "")
  106. let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
  107. let request = try parser.readHttpRequest(TestSocket(payload))
  108. XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")
  109. let unicodeBytes = bodyString.utf8.map { return $0 }
  110. XCTAssert(request.body == unicodeBytes, "Request body must be correct")
  111. } catch { }
  112. do { // test very large multi-segment payload
  113. let contentLength = Socket.kBufferLength * 4
  114. let bodyString = [String](repeating: "C", count: contentLength).joined(separator: "")
  115. let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
  116. let request = try parser.readHttpRequest(TestSocket(payload))
  117. XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")
  118. let unicodeBytes = bodyString.utf8.map { return $0 }
  119. XCTAssert(request.body == unicodeBytes, "Request body must be correct")
  120. } catch { }
  121. var resp = try? parser.readHttpRequest(TestSocket("GET /open?link=https://www.youtube.com/watch?v=D2cUBG4PnOA HTTP/1.0\nContent-Length: 10\n\n1234567890"))
  122. XCTAssertEqual(resp?.queryParams.filter({ $0.0 == "link"}).first?.1, "https://www.youtube.com/watch?v=D2cUBG4PnOA")
  123. XCTAssertEqual(resp?.method, "GET", "Parser should extract HTTP method name from the status line.")
  124. XCTAssertEqual(resp?.path, "/open", "Parser should extract HTTP path value from the status line.")
  125. XCTAssertEqual(resp?.headers["content-length"], "10", "Parser should extract Content-Length header value.")
  126. resp = try? parser.readHttpRequest(TestSocket("POST / HTTP/1.0\nContent-Length: 10\n\n1234567890"))
  127. XCTAssertEqual(resp?.method, "POST", "Parser should extract HTTP method name from the status line.")
  128. resp = try? parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nHeader1: 1:1:34\nHeader2: 12345\nContent-Length: 0\n\n"))
  129. XCTAssertEqual(resp?.headers["header1"], "1:1:34", "Parser should properly extract header name and value in case the value has ':' character.")
  130. resp = try? parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nHeader1: 1\nHeader2: 2\nContent-Length: 0\n\n"))
  131. XCTAssertEqual(resp?.headers["header1"], "1", "Parser should extract multiple headers from the request.")
  132. XCTAssertEqual(resp?.headers["header2"], "2", "Parser should extract multiple headers from the request.")
  133. resp = try? parser.readHttpRequest(TestSocket("GET /some/path?subscript_query[]=1&subscript_query[]=2 HTTP/1.0\nContent-Length: 10\n\n1234567890"))
  134. let queryPairs = resp?.queryParams ?? []
  135. XCTAssertEqual(queryPairs.count, 2)
  136. XCTAssertEqual(queryPairs.first?.0, "subscript_query[]")
  137. XCTAssertEqual(queryPairs.first?.1, "1")
  138. XCTAssertEqual(queryPairs.last?.0, "subscript_query[]")
  139. XCTAssertEqual(queryPairs.last?.1, "2")
  140. XCTAssertEqual(resp?.method, "GET", "Parser should extract HTTP method name from the status line.")
  141. XCTAssertEqual(resp?.path, "/some/path", "Parser should extract HTTP path value from the status line.")
  142. XCTAssertEqual(resp?.headers["content-length"], "10", "Parser should extract Content-Length header value.")
  143. }
  144. }