-
Notifications
You must be signed in to change notification settings - Fork 125
fail if user tries writing bytes after request is sent #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
009bfd7
86faf47
b2199f0
fa12314
9ef725a
0a33554
373d19e
d112695
e947698
8187a97
5123115
414bba6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -415,20 +415,21 @@ class HTTPClientInternalTests: XCTestCase { | |
} | ||
|
||
let group = getDefaultEventLoopGroup(numberOfThreads: 3) | ||
let serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) | ||
defer { | ||
XCTAssertNoThrow(try group.syncShutdownGracefully()) | ||
XCTAssertNoThrow(try serverGroup.syncShutdownGracefully()) | ||
} | ||
|
||
let channelEL = group.next() | ||
let delegateEL = group.next() | ||
let randoEL = group.next() | ||
|
||
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group)) | ||
let promise: EventLoopPromise<Channel> = httpClient.eventLoopGroup.next().makePromise() | ||
let httpBin = HTTPBin(channelPromise: promise) | ||
let server = NIOHTTP1TestServer(group: serverGroup) | ||
defer { | ||
XCTAssertNoThrow(try server.stop()) | ||
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true)) | ||
XCTAssertNoThrow(try httpBin.shutdown()) | ||
} | ||
|
||
let body: HTTPClient.Body = .stream(length: 8) { writer in | ||
|
@@ -439,21 +440,26 @@ class HTTPClientInternalTests: XCTestCase { | |
} | ||
} | ||
|
||
let request = try Request(url: "http://127.0.0.1:\(httpBin.port)/custom", | ||
let request = try Request(url: "http://127.0.0.1:\(server.serverPort)/custom", | ||
body: body) | ||
let delegate = Delegate(expectedEventLoop: delegateEL, randomOtherEventLoop: randoEL) | ||
let future = httpClient.execute(request: request, | ||
delegate: delegate, | ||
eventLoop: .init(.testOnly_exact(channelOn: channelEL, | ||
delegateOn: delegateEL))).futureResult | ||
|
||
let channel = try promise.futureResult.wait() | ||
XCTAssertNoThrow(try server.readInbound()) // .head | ||
XCTAssertNoThrow(try server.readInbound()) // .body | ||
XCTAssertNoThrow(try server.readInbound()) // .end | ||
|
||
// Send 3 parts, but only one should be received until the future is complete | ||
let buffer = channel.allocator.buffer(string: "1234") | ||
try channel.writeAndFlush(HTTPServerResponsePart.body(.byteBuffer(buffer))).wait() | ||
XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), | ||
status: .ok, | ||
headers: HTTPHeaders([("Transfer-Encoding", "chunked")]))))) | ||
let buffer = ByteBuffer(string: "1234") | ||
XCTAssertNoThrow(try server.writeOutbound(.body(.byteBuffer(buffer)))) | ||
XCTAssertNoThrow(try server.writeOutbound(.end(nil))) | ||
|
||
try channel.writeAndFlush(HTTPServerResponsePart.end(nil)).wait() | ||
let (receivedMessages, sentMessages) = try future.wait() | ||
XCTAssertEqual(2, receivedMessages.count) | ||
XCTAssertEqual(4, sentMessages.count) | ||
|
@@ -488,7 +494,7 @@ class HTTPClientInternalTests: XCTestCase { | |
|
||
switch receivedMessages.dropFirst(0).first { | ||
case .some(.head(let head)): | ||
XCTAssertEqual(["transfer-encoding": "chunked"], head.headers) | ||
XCTAssertEqual(head.headers["transfer-encoding"].first, "chunked") | ||
default: | ||
XCTFail("wrong message") | ||
} | ||
|
@@ -1025,4 +1031,53 @@ class HTTPClientInternalTests: XCTestCase { | |
XCTAssertEqual(request5.socketPath, "/tmp/file") | ||
XCTAssertEqual(request5.uri, "/file/path") | ||
} | ||
|
||
func testBodyPartStreamStateChangedBeforeNotification() throws { | ||
class StateValidationDelegate: HTTPClientResponseDelegate { | ||
typealias Response = Void | ||
|
||
var handler: TaskHandler<StateValidationDelegate>! | ||
var triggered = false | ||
|
||
func didReceiveError(task: HTTPClient.Task<Response>, _ error: Error) { | ||
self.triggered = true | ||
switch self.handler.state { | ||
case .endOrError: | ||
// expected | ||
break | ||
default: | ||
XCTFail("unexpected state: \(self.handler.state)") | ||
} | ||
} | ||
|
||
func didFinishRequest(task: HTTPClient.Task<Void>) throws {} | ||
} | ||
|
||
let channel = EmbeddedChannel() | ||
XCTAssertNoThrow(try channel.connect(to: try SocketAddress(unixDomainSocketPath: "/fake")).wait()) | ||
|
||
let task = Task<Void>(eventLoop: channel.eventLoop, logger: HTTPClient.loggingDisabled) | ||
|
||
let delegate = StateValidationDelegate() | ||
let handler = TaskHandler(task: task, | ||
kind: .host, | ||
delegate: delegate, | ||
redirectHandler: nil, | ||
ignoreUncleanSSLShutdown: false, | ||
logger: HTTPClient.loggingDisabled) | ||
|
||
delegate.handler = handler | ||
try channel.pipeline.addHandler(handler).wait() | ||
|
||
var request = try Request(url: "http://localhost:8080/post") | ||
request.body = .stream(length: 1) { writer in | ||
writer.write(.byteBuffer(ByteBuffer(string: "1234"))) | ||
} | ||
|
||
XCTAssertThrowsError(try channel.writeOutbound(request)) | ||
XCTAssertTrue(delegate.triggered) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can also check on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great idea, done! |
||
|
||
XCTAssertNoThrow(try channel.readOutbound(as: HTTPClientRequestPart.self)) // .head | ||
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean)) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think (would need a test here too) that we're policing the body length too late here, no? We seem to just add the readableBytes but I can't see that we check that it's actually less than or equal the number of body bytes allowed.
What happens if we do say
content-length: 1
and then sendXABCDEFG
, aren't we then still sendingABCDEFG
? I think we are and that'd be a security vulnerability because we could smuggle a request (say we didXGET /something HTTP/1.1\r\n\r\n
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thats a great point, yeah, we only check in the end, let me see how I can handle it here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I added an early fail here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@artemredkin this looks good but given that it's security relevant, could we add a test specifically for that case (one that'd fail without the new condition)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we already have it, by accident, we have two tests for content-length/body-length checks:
testContentLengthTooLongFails
testContentLengthTooShortFails
Last one will fail at the end of the request, before we send
.end
, but the first one will now be failed by this new checkThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially we can split the error message, just to be sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@artemredkin I think we need a new one that tests that we do not send the bytes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because as soon as the bytes are on the wire, we created a security vulnerability. Even if we detect later that something's wrong, it'll be too late.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lukasa same here, I've added a test to check that we do not send more bytes then body length, although I'm not 100% sure its foolproof