Skip to content

Commit e0eeb04

Browse files
vkillartemredkin
authored andcommitted
Ignore uncleanShutdown error when state is .head or .body (#77)
* Ignore uncleanShutdown error when state is head or body * Add ignoreNIOSSLUncleanShutdownError to Configuration * Revert old HTTPClient.init founctions * Run generate_linux_tests.rb * Rename ignoreNIOSSLUncleanShutdownError to ignoreUncleanSSLShutdown * Make tests compatible with swift 5.0
1 parent 690e8ee commit e0eeb04

File tree

6 files changed

+267
-4
lines changed

6 files changed

+267
-4
lines changed

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ public class HTTPClient {
211211
return channel.eventLoop.makeSucceededFuture(())
212212
}
213213
}.flatMap {
214-
let taskHandler = TaskHandler(task: task, delegate: delegate, redirectHandler: redirectHandler)
214+
let taskHandler = TaskHandler(task: task, delegate: delegate, redirectHandler: redirectHandler, ignoreUncleanSSLShutdown: self.configuration.ignoreUncleanSSLShutdown)
215215
return channel.pipeline.addHandler(taskHandler)
216216
}
217217
}
@@ -276,19 +276,31 @@ public class HTTPClient {
276276
public var timeout: Timeout
277277
/// Upstream proxy, defaults to no proxy.
278278
public var proxy: Proxy?
279+
/// Ignore TLS unclean shutdown error, defaults to `false`.
280+
public var ignoreUncleanSSLShutdown: Bool
279281

280282
public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil) {
283+
self.init(tlsConfiguration: tlsConfiguration, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false)
284+
}
285+
286+
public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) {
281287
self.tlsConfiguration = tlsConfiguration
282288
self.followRedirects = followRedirects
283289
self.timeout = timeout
284290
self.proxy = proxy
291+
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
285292
}
286293

287294
public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil) {
295+
self.init(certificateVerification: certificateVerification, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false)
296+
}
297+
298+
public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) {
288299
self.tlsConfiguration = TLSConfiguration.forClient(certificateVerification: certificateVerification)
289300
self.followRedirects = followRedirects
290301
self.timeout = timeout
291302
self.proxy = proxy
303+
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
292304
}
293305
}
294306

Sources/AsyncHTTPClient/HTTPHandler.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,15 +441,17 @@ internal class TaskHandler<T: HTTPClientResponseDelegate>: ChannelInboundHandler
441441
let task: HTTPClient.Task<T.Response>
442442
let delegate: T
443443
let redirectHandler: RedirectHandler<T.Response>?
444+
let ignoreUncleanSSLShutdown: Bool
444445

445446
var state: State = .idle
446447
var pendingRead = false
447448
var mayRead = true
448449

449-
init(task: HTTPClient.Task<T.Response>, delegate: T, redirectHandler: RedirectHandler<T.Response>?) {
450+
init(task: HTTPClient.Task<T.Response>, delegate: T, redirectHandler: RedirectHandler<T.Response>?, ignoreUncleanSSLShutdown: Bool) {
450451
self.task = task
451452
self.delegate = delegate
452453
self.redirectHandler = redirectHandler
454+
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
453455
}
454456

455457
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
@@ -619,6 +621,10 @@ internal class TaskHandler<T: HTTPClientResponseDelegate>: ChannelInboundHandler
619621
/// Some HTTP Servers can 'forget' to respond with CloseNotify when client is closing connection,
620622
/// this could lead to incomplete SSL shutdown. But since request is already processed, we can ignore this error.
621623
break
624+
case .head where self.ignoreUncleanSSLShutdown,
625+
.body where self.ignoreUncleanSSLShutdown:
626+
/// We can also ignore this error like `.end`.
627+
break
622628
default:
623629
self.state = .end
624630
self.delegate.didReceiveError(task: self.task, error)

Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class HTTPClientInternalTests: XCTestCase {
2828
let task = Task<Void>(eventLoop: channel.eventLoop)
2929

3030
try channel.pipeline.addHandler(recorder).wait()
31-
try channel.pipeline.addHandler(TaskHandler(task: task, delegate: TestHTTPDelegate(), redirectHandler: nil)).wait()
31+
try channel.pipeline.addHandler(TaskHandler(task: task, delegate: TestHTTPDelegate(), redirectHandler: nil, ignoreUncleanSSLShutdown: false)).wait()
3232

3333
var request = try Request(url: "http://localhost/get")
3434
request.headers.add(name: "X-Test-Header", value: "X-Test-Value")
@@ -53,7 +53,7 @@ class HTTPClientInternalTests: XCTestCase {
5353
let channel = EmbeddedChannel()
5454
let delegate = TestHTTPDelegate()
5555
let task = Task<Void>(eventLoop: channel.eventLoop)
56-
let handler = TaskHandler(task: task, delegate: delegate, redirectHandler: nil)
56+
let handler = TaskHandler(task: task, delegate: delegate, redirectHandler: nil, ignoreUncleanSSLShutdown: false)
5757

5858
try channel.pipeline.addHandler(handler).wait()
5959

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,105 @@ internal final class HttpBinHandler: ChannelInboundHandler {
332332
}
333333
}
334334

335+
internal class HttpBinForSSLUncleanShutdown {
336+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
337+
let serverChannel: Channel
338+
339+
var port: Int {
340+
return Int(self.serverChannel.localAddress!.port!)
341+
}
342+
343+
init(channelPromise: EventLoopPromise<Channel>? = nil) {
344+
self.serverChannel = try! ServerBootstrap(group: self.group)
345+
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
346+
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
347+
.childChannelInitializer { channel in
348+
let requestDecoder = HTTPRequestDecoder()
349+
return channel.pipeline.addHandler(ByteToMessageHandler(requestDecoder)).flatMap {
350+
let configuration = TLSConfiguration.forServer(certificateChain: [.certificate(try! NIOSSLCertificate(buffer: cert.utf8.map(Int8.init), format: .pem))],
351+
privateKey: .privateKey(try! NIOSSLPrivateKey(buffer: key.utf8.map(Int8.init), format: .pem)))
352+
let context = try! NIOSSLContext(configuration: configuration)
353+
return channel.pipeline.addHandler(try! NIOSSLServerHandler(context: context), name: "NIOSSLServerHandler", position: .first).flatMap {
354+
channel.pipeline.addHandler(HttpBinForSSLUncleanShutdownHandler(channelPromise: channelPromise))
355+
}
356+
}
357+
}.bind(host: "127.0.0.1", port: 0).wait()
358+
}
359+
360+
func shutdown() {
361+
try! self.group.syncShutdownGracefully()
362+
}
363+
}
364+
365+
internal final class HttpBinForSSLUncleanShutdownHandler: ChannelInboundHandler {
366+
typealias InboundIn = HTTPServerRequestPart
367+
typealias OutboundOut = ByteBuffer
368+
369+
let channelPromise: EventLoopPromise<Channel>?
370+
371+
init(channelPromise: EventLoopPromise<Channel>? = nil) {
372+
self.channelPromise = channelPromise
373+
}
374+
375+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
376+
switch self.unwrapInboundIn(data) {
377+
case .head(let req):
378+
if let promise = self.channelPromise {
379+
promise.succeed(context.channel)
380+
}
381+
382+
let response: String?
383+
switch req.uri {
384+
case "/nocontentlength":
385+
response = """
386+
HTTP/1.1 200 OK\r\n\
387+
Connection: close\r\n\
388+
\r\n\
389+
foo
390+
"""
391+
case "/nocontent":
392+
response = """
393+
HTTP/1.1 204 OK\r\n\
394+
Connection: close\r\n\
395+
\r\n
396+
"""
397+
case "/noresponse":
398+
response = nil
399+
case "/wrongcontentlength":
400+
response = """
401+
HTTP/1.1 200 OK\r\n\
402+
Connection: close\r\n\
403+
Content-Length: 6\r\n\
404+
\r\n\
405+
foo
406+
"""
407+
default:
408+
response = """
409+
HTTP/1.1 404 OK\r\n\
410+
Connection: close\r\n\
411+
Content-Length: 9\r\n\
412+
\r\n\
413+
Not Found
414+
"""
415+
}
416+
417+
if let response = response {
418+
var buffer = context.channel.allocator.buffer(capacity: response.count)
419+
buffer.writeString(response)
420+
context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil)
421+
}
422+
423+
_ = context.channel.pipeline.removeHandler(name: "NIOSSLServerHandler").map { _ in
424+
context.close(promise: nil)
425+
}
426+
case .body:
427+
()
428+
case .end:
429+
()
430+
}
431+
}
432+
}
433+
335434
extension ByteBuffer {
336435
public static func of(string: String) -> ByteBuffer {
337436
var buffer = ByteBufferAllocator().buffer(capacity: string.count)

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ extension HTTPClientTests {
4545
("testProxyPlaintext", testProxyPlaintext),
4646
("testProxyTLS", testProxyTLS),
4747
("testUploadStreaming", testUploadStreaming),
48+
("testNoContentLengthForSSLUncleanShutdown", testNoContentLengthForSSLUncleanShutdown),
49+
("testNoContentLengthWithIgnoreErrorForSSLUncleanShutdown", testNoContentLengthWithIgnoreErrorForSSLUncleanShutdown),
50+
("testCorrectContentLengthForSSLUncleanShutdown", testCorrectContentLengthForSSLUncleanShutdown),
51+
("testNoContentForSSLUncleanShutdown", testNoContentForSSLUncleanShutdown),
52+
("testNoResponseForSSLUncleanShutdown", testNoResponseForSSLUncleanShutdown),
53+
("testNoResponseWithIgnoreErrorForSSLUncleanShutdown", testNoResponseWithIgnoreErrorForSSLUncleanShutdown),
54+
("testWrongContentLengthForSSLUncleanShutdown", testWrongContentLengthForSSLUncleanShutdown),
55+
("testWrongContentLengthWithIgnoreErrorForSSLUncleanShutdown", testWrongContentLengthWithIgnoreErrorForSSLUncleanShutdown),
4856
]
4957
}
5058
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import AsyncHTTPClient
1616
import NIO
1717
import NIOFoundationCompat
1818
import NIOHTTP1
19+
import NIOSSL
1920
import XCTest
2021

2122
class HTTPClientTests: XCTestCase {
@@ -346,4 +347,141 @@ class HTTPClientTests: XCTestCase {
346347
XCTAssertEqual(.ok, response.status)
347348
XCTAssertEqual("12344321", data.data)
348349
}
350+
351+
func testNoContentLengthForSSLUncleanShutdown() throws {
352+
let httpBin = HttpBinForSSLUncleanShutdown()
353+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
354+
configuration: HTTPClient.Configuration(certificateVerification: .none))
355+
356+
defer {
357+
try! httpClient.syncShutdown()
358+
httpBin.shutdown()
359+
}
360+
361+
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/nocontentlength").wait(), "Should fail") { error in
362+
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
363+
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
364+
}
365+
}
366+
}
367+
368+
func testNoContentLengthWithIgnoreErrorForSSLUncleanShutdown() throws {
369+
let httpBin = HttpBinForSSLUncleanShutdown()
370+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
371+
configuration: HTTPClient.Configuration(certificateVerification: .none, ignoreUncleanSSLShutdown: true))
372+
373+
defer {
374+
try! httpClient.syncShutdown()
375+
httpBin.shutdown()
376+
}
377+
378+
let response = try httpClient.get(url: "https://localhost:\(httpBin.port)/nocontentlength").wait()
379+
let bytes = response.body.flatMap { $0.getData(at: 0, length: $0.readableBytes) }
380+
let string = String(decoding: bytes!, as: UTF8.self)
381+
382+
XCTAssertEqual(.ok, response.status)
383+
XCTAssertEqual("foo", string)
384+
}
385+
386+
func testCorrectContentLengthForSSLUncleanShutdown() throws {
387+
let httpBin = HttpBinForSSLUncleanShutdown()
388+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
389+
configuration: HTTPClient.Configuration(certificateVerification: .none))
390+
391+
defer {
392+
try! httpClient.syncShutdown()
393+
httpBin.shutdown()
394+
}
395+
396+
let response = try httpClient.get(url: "https://localhost:\(httpBin.port)/").wait()
397+
let bytes = response.body.flatMap { $0.getData(at: 0, length: $0.readableBytes) }
398+
let string = String(decoding: bytes!, as: UTF8.self)
399+
400+
XCTAssertEqual(.notFound, response.status)
401+
XCTAssertEqual("Not Found", string)
402+
}
403+
404+
func testNoContentForSSLUncleanShutdown() throws {
405+
let httpBin = HttpBinForSSLUncleanShutdown()
406+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
407+
configuration: HTTPClient.Configuration(certificateVerification: .none))
408+
409+
defer {
410+
try! httpClient.syncShutdown()
411+
httpBin.shutdown()
412+
}
413+
414+
let response = try httpClient.get(url: "https://localhost:\(httpBin.port)/nocontent").wait()
415+
416+
XCTAssertEqual(.noContent, response.status)
417+
XCTAssertEqual(response.body, nil)
418+
}
419+
420+
func testNoResponseForSSLUncleanShutdown() throws {
421+
let httpBin = HttpBinForSSLUncleanShutdown()
422+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
423+
configuration: HTTPClient.Configuration(certificateVerification: .none))
424+
425+
defer {
426+
try! httpClient.syncShutdown()
427+
httpBin.shutdown()
428+
}
429+
430+
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in
431+
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
432+
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
433+
}
434+
}
435+
}
436+
437+
func testNoResponseWithIgnoreErrorForSSLUncleanShutdown() throws {
438+
let httpBin = HttpBinForSSLUncleanShutdown()
439+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
440+
configuration: HTTPClient.Configuration(certificateVerification: .none, ignoreUncleanSSLShutdown: true))
441+
442+
defer {
443+
try! httpClient.syncShutdown()
444+
httpBin.shutdown()
445+
}
446+
447+
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in
448+
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
449+
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
450+
}
451+
}
452+
}
453+
454+
func testWrongContentLengthForSSLUncleanShutdown() throws {
455+
let httpBin = HttpBinForSSLUncleanShutdown()
456+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
457+
configuration: HTTPClient.Configuration(certificateVerification: .none))
458+
459+
defer {
460+
try! httpClient.syncShutdown()
461+
httpBin.shutdown()
462+
}
463+
464+
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/wrongcontentlength").wait(), "Should fail") { error in
465+
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
466+
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
467+
}
468+
}
469+
}
470+
471+
func testWrongContentLengthWithIgnoreErrorForSSLUncleanShutdown() throws {
472+
let httpBin = HttpBinForSSLUncleanShutdown()
473+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
474+
configuration: HTTPClient.Configuration(certificateVerification: .none, ignoreUncleanSSLShutdown: true))
475+
476+
defer {
477+
try! httpClient.syncShutdown()
478+
httpBin.shutdown()
479+
}
480+
481+
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/wrongcontentlength").wait(), "Should fail") { error in
482+
guard case let error = error as? HTTPParserError, error == .invalidEOFState else {
483+
return XCTFail("Should fail with HTTPParserError.invalidEOFState")
484+
}
485+
}
486+
}
349487
}

0 commit comments

Comments
 (0)