mirror of
https://github.com/apple/swift-nio-extras.git
synced 2025-05-15 01:18:58 +08:00
Motivation Currently we don't confirm that the decompression has completed successfully. This means that we can incorrectly spin forever attempting to decompress past the end of a message, and that we can fail to notice that a message is truncated. Neither of these is good. Modifications Propagate the message zlib gives us as to whether or not decompression is done, and keep track of it. Add some tests written by @vojtarylko to validate the behaviour. Result Correctly police the bounds of the messages.
338 lines
16 KiB
Swift
338 lines
16 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import XCTest
|
|
import CNIOExtrasZlib
|
|
@testable import NIO
|
|
@testable import NIOHTTP1
|
|
@testable import NIOHTTPCompression
|
|
|
|
class HTTPResponseDecompressorTest: XCTestCase {
|
|
func testDecompressionNoLimit() throws {
|
|
let channel = EmbeddedChannel()
|
|
try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .none)).wait()
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate"), ("Content-Length", "13")])
|
|
try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers)))
|
|
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(body)))
|
|
}
|
|
|
|
func testDecompressionLimitSizeWithContentLenghtHeaderSucceeds() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .size(272))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate"), ("Content-Length", "13")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(body)))
|
|
}
|
|
|
|
func testDecompressionLimitSizeWithContentLenghtHeaderFails() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .size(271))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate"), ("Content-Length", "13")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertThrowsError(try channel.writeInbound(HTTPClientResponsePart.body(body))) { error in
|
|
XCTAssertEqual(error as? NIOHTTPDecompression.DecompressionError, .limit)
|
|
}
|
|
}
|
|
|
|
func testDecompressionLimitSizeWithoutContentLenghtHeaderSucceeds() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .size(272))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(body)))
|
|
}
|
|
|
|
func testDecompressionLimitSizeWithoutContentLenghtHeaderFails() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .size(271))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertThrowsError(try channel.writeInbound(HTTPClientResponsePart.body(body))) { error in
|
|
XCTAssertEqual(error as? NIOHTTPDecompression.DecompressionError, .limit)
|
|
}
|
|
}
|
|
|
|
func testDecompressionLimitRatioWithContentLenghtHeaderSucceeds() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .ratio(21))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate"), ("Content-Length", "13")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(body)))
|
|
}
|
|
|
|
func testDecompressionLimitRatioWithContentLenghtHeaderFails() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .ratio(20))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate"), ("Content-Length", "13")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertThrowsError(try channel.writeInbound(HTTPClientResponsePart.body(body))) { error in
|
|
XCTAssertEqual(error as? NIOHTTPDecompression.DecompressionError, .limit)
|
|
}
|
|
}
|
|
|
|
func testDecompressionLimitRatioWithoutContentLenghtHeaderSucceeds() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .ratio(21))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(body)))
|
|
}
|
|
|
|
func testDecompressionLimitRatioWithoutContentLenghtHeaderFails() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .ratio(20))).wait())
|
|
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate")])
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
|
|
// this compressed payload is 272 bytes long uncompressed
|
|
let body = ByteBuffer.of(bytes: [120, 156, 75, 76, 28, 5, 200, 0, 0, 248, 66, 103, 17])
|
|
XCTAssertThrowsError(try channel.writeInbound(HTTPClientResponsePart.body(body))) { error in
|
|
XCTAssertEqual(error as? NIOHTTPDecompression.DecompressionError, .limit)
|
|
}
|
|
}
|
|
|
|
func testDecompression() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .none)).wait())
|
|
|
|
var body = ""
|
|
for _ in 1...1000 {
|
|
body += "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
|
}
|
|
|
|
for algorithm in [nil, "gzip", "deflate"] {
|
|
let compressed: ByteBuffer
|
|
var headers = HTTPHeaders()
|
|
if let algorithm = algorithm {
|
|
headers.add(name: "Content-Encoding", value: algorithm)
|
|
compressed = compress(ByteBuffer.of(string: body), algorithm)
|
|
} else {
|
|
compressed = ByteBuffer.of(string: body)
|
|
}
|
|
headers.add(name: "Content-Length", value: "\(compressed.readableBytes)")
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(compressed)))
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.end(nil)))
|
|
|
|
var head: HTTPClientResponsePart?
|
|
XCTAssertNoThrow(head = try channel.readInbound(as: HTTPClientResponsePart.self))
|
|
XCTAssertEqual(head, HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers)))
|
|
|
|
// the response is chunked
|
|
var next: HTTPClientResponsePart?
|
|
XCTAssertNoThrow(next = try channel.readInbound(as: HTTPClientResponsePart.self))
|
|
var buffer = ByteBuffer.of(bytes: [])
|
|
while let part = next {
|
|
switch part {
|
|
case .head:
|
|
XCTFail("Unexpected head http part")
|
|
case .body(var input):
|
|
buffer.writeBuffer(&input)
|
|
case .end:
|
|
break
|
|
}
|
|
XCTAssertNoThrow(next = try channel.readInbound(as: HTTPClientResponsePart.self))
|
|
}
|
|
XCTAssertEqual(buffer, ByteBuffer.of(string: body))
|
|
}
|
|
}
|
|
|
|
func testDecompressionWithoutContentLength() {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .none)).wait())
|
|
|
|
var body = ""
|
|
for _ in 1...1000 {
|
|
body += "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
|
}
|
|
|
|
for algorithm in [nil, "gzip", "deflate"] {
|
|
let compressed: ByteBuffer
|
|
var headers = HTTPHeaders()
|
|
if let algorithm = algorithm {
|
|
headers.add(name: "Content-Encoding", value: algorithm)
|
|
compressed = compress(ByteBuffer.of(string: body), algorithm)
|
|
} else {
|
|
compressed = ByteBuffer.of(string: body)
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers))))
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(compressed)))
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.end(nil)))
|
|
|
|
var head: HTTPClientResponsePart?
|
|
XCTAssertNoThrow(head = try channel.readInbound(as: HTTPClientResponsePart.self))
|
|
XCTAssertEqual(head, HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers)))
|
|
|
|
// the response is chunked
|
|
var next: HTTPClientResponsePart?
|
|
XCTAssertNoThrow(next = try channel.readInbound(as: HTTPClientResponsePart.self))
|
|
var buffer = ByteBuffer.of(bytes: [])
|
|
while let part = next {
|
|
switch part {
|
|
case .head:
|
|
XCTFail("Unexpected head http part")
|
|
case .body(var input):
|
|
buffer.writeBuffer(&input)
|
|
case .end:
|
|
break
|
|
}
|
|
XCTAssertNoThrow(next = try channel.readInbound(as: HTTPClientResponsePart.self))
|
|
}
|
|
|
|
XCTAssertEqual(buffer, ByteBuffer.of(string: body))
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.end(nil)))
|
|
}
|
|
|
|
func testDecompressionTrailingData() throws {
|
|
// Valid compressed data with some trailing garbage
|
|
let compressed = ByteBuffer(bytes: [120, 156, 99, 0, 0, 0, 1, 0, 1] + [1, 2, 3])
|
|
|
|
let channel = EmbeddedChannel()
|
|
try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .none)).wait()
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate"), ("Content-Length", "\(compressed.readableBytes)")])
|
|
try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers)))
|
|
|
|
XCTAssertThrowsError(try channel.writeInbound(HTTPClientResponsePart.body(compressed)))
|
|
}
|
|
|
|
func testDecompressionTruncatedInput() throws {
|
|
// Truncated compressed data
|
|
let compressed = ByteBuffer(bytes: [120, 156, 99, 0])
|
|
|
|
let channel = EmbeddedChannel()
|
|
try channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: .none)).wait()
|
|
let headers = HTTPHeaders([("Content-Encoding", "deflate"), ("Content-Length", "\(compressed.readableBytes)")])
|
|
try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: headers)))
|
|
|
|
XCTAssertNoThrow(try channel.writeInbound(HTTPClientResponsePart.body(compressed)))
|
|
XCTAssertThrowsError(try channel.writeInbound(HTTPClientResponsePart.end(nil)))
|
|
}
|
|
|
|
private func compress(_ body: ByteBuffer, _ algorithm: String) -> ByteBuffer {
|
|
var stream = z_stream()
|
|
|
|
stream.zalloc = nil
|
|
stream.zfree = nil
|
|
stream.opaque = nil
|
|
|
|
var buffer = ByteBufferAllocator().buffer(capacity: 1000)
|
|
|
|
let windowBits: Int32
|
|
switch algorithm {
|
|
case "deflate":
|
|
windowBits = 15
|
|
case "gzip":
|
|
windowBits = 16 + 15
|
|
default:
|
|
XCTFail("Unsupported algorithm: \(algorithm)")
|
|
return buffer
|
|
}
|
|
|
|
let rc = CNIOExtrasZlib_deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, windowBits, 8, Z_DEFAULT_STRATEGY)
|
|
XCTAssertEqual(Z_OK, rc)
|
|
|
|
defer {
|
|
stream.avail_in = 0
|
|
stream.next_in = nil
|
|
stream.avail_out = 0
|
|
stream.next_out = nil
|
|
}
|
|
|
|
var body = body
|
|
|
|
body.readWithUnsafeMutableReadableBytes { dataPtr in
|
|
let typedPtr = dataPtr.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
|
let typedDataPtr = UnsafeMutableBufferPointer(start: typedPtr,
|
|
count: dataPtr.count)
|
|
|
|
stream.avail_in = UInt32(typedDataPtr.count)
|
|
stream.next_in = typedDataPtr.baseAddress!
|
|
|
|
buffer.writeWithUnsafeMutableBytes(minimumWritableBytes: 0) { outputPtr in
|
|
let typedOutputPtr = UnsafeMutableBufferPointer(start: outputPtr.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
|
count: outputPtr.count)
|
|
stream.avail_out = UInt32(typedOutputPtr.count)
|
|
stream.next_out = typedOutputPtr.baseAddress!
|
|
let rc = deflate(&stream, Z_FINISH)
|
|
XCTAssertTrue(rc == Z_OK || rc == Z_STREAM_END)
|
|
return typedOutputPtr.count - Int(stream.avail_out)
|
|
}
|
|
|
|
return typedDataPtr.count - Int(stream.avail_in)
|
|
}
|
|
|
|
deflateEnd(&stream)
|
|
|
|
return buffer
|
|
}
|
|
}
|
|
|
|
extension ByteBuffer {
|
|
fileprivate static func of(string: String) -> ByteBuffer {
|
|
var buffer = ByteBufferAllocator().buffer(capacity: string.count)
|
|
buffer.writeString(string)
|
|
return buffer
|
|
}
|
|
|
|
fileprivate static func of(bytes: [UInt8]) -> ByteBuffer {
|
|
var buffer = ByteBufferAllocator().buffer(capacity: bytes.count)
|
|
buffer.writeBytes(bytes)
|
|
return buffer
|
|
}
|
|
}
|