Skip to content

Commit c3f0735

Browse files
weissikevints
authored andcommitted
fix URLSession crashing on HTTP/0.9 simple-responses (swiftlang#1097)
also add tests for misbehaving HTTP servers.
1 parent fc9d47c commit c3f0735

File tree

3 files changed

+190
-3
lines changed

3 files changed

+190
-3
lines changed

Foundation/NSURLSession/http/HTTPURLProtocol.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,14 @@ extension _HTTPURLProtocol {
439439
extension _HTTPURLProtocol: _EasyHandleDelegate {
440440

441441
func didReceive(data: Data) -> _EasyHandle._Action {
442-
guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
443-
guard ts.isHeaderComplete else { fatalError("Received body data, but the header is not complete, yet.") }
442+
guard case .transferInProgress(var ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
443+
if !ts.isHeaderComplete {
444+
ts.response = HTTPURLResponse(url: ts.url, statusCode: 200, httpVersion: "HTTP/0.9", headerFields: [:])
445+
/* we received body data before CURL tells us that the headers are complete, that happens for HTTP/0.9 simple responses, see
446+
- https://www.w3.org/Protocols/HTTP/1.0/spec.html#Message-Types
447+
- https://github.com/curl/curl/issues/467
448+
*/
449+
}
444450
notifyDelegate(aboutReceivedData: data)
445451
internalState = .transferInProgress(ts.byAppending(bodyData: data))
446452
return .proceed

TestFoundation/HTTPServer.swift

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ class _TCPSocket {
104104
return String(str[startIndex..<endIndex])
105105
}
106106
}
107+
108+
func writeRawData(_ data: Data) throws {
109+
let x = try data.withUnsafeBytes { ptr in
110+
try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, ptr, data.count)))
111+
}
112+
}
107113

108114
func writeData(header: String, body: String, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws {
109115
var header = Array(header.utf8)
@@ -173,6 +179,69 @@ class _HTTPServer {
173179
semaphore.wait()
174180

175181
}
182+
183+
func respondWithBrokenResponses(uri: String) throws {
184+
let responseData: Data
185+
switch uri {
186+
case "/LandOfTheLostCities/Pompeii":
187+
/* this is an example of what you get if you connect to an HTTP2
188+
server using HTTP/1.1. Curl interprets that as a HTTP/0.9
189+
simple-response and therefore sends this back as a response
190+
body. Go figure! */
191+
responseData = Data(bytes: [
192+
0x00, 0x00, 0x18, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
193+
0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
194+
0x01, 0x00, 0x05, 0x00, 0x00, 0x40, 0x00, 0x00, 0x06, 0x00,
195+
0x00, 0x1f, 0x40, 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, 0x00,
196+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
197+
0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, 0x20, 0x63, 0x6c, 0x69,
198+
0x65, 0x6e, 0x74, 0x20, 0x70, 0x72, 0x65, 0x66, 0x61, 0x63,
199+
0x65, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6d,
200+
0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x20, 0x6f, 0x72, 0x20,
201+
0x63, 0x6f, 0x72, 0x72, 0x75, 0x70, 0x74, 0x2e, 0x20, 0x48,
202+
0x65, 0x78, 0x20, 0x64, 0x75, 0x6d, 0x70, 0x20, 0x66, 0x6f,
203+
0x72, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64,
204+
0x20, 0x62, 0x79, 0x74, 0x65, 0x73, 0x3a, 0x20, 0x34, 0x37,
205+
0x34, 0x35, 0x35, 0x34, 0x32, 0x30, 0x32, 0x66, 0x33, 0x33,
206+
0x32, 0x66, 0x36, 0x34, 0x36, 0x35, 0x37, 0x36, 0x36, 0x39,
207+
0x36, 0x33, 0x36, 0x35, 0x32, 0x66, 0x33, 0x31, 0x33, 0x32,
208+
0x33, 0x33, 0x33, 0x34, 0x33, 0x35, 0x33, 0x36, 0x33, 0x37,
209+
0x33, 0x38, 0x33, 0x39, 0x33, 0x30])
210+
case "/LandOfTheLostCities/Sodom":
211+
/* a technically valid HTTP/0.9 simple-response */
212+
responseData = ("technically, this is a valid HTTP/0.9 " +
213+
"simple-response. I know it's odd but CURL supports it " +
214+
"still...\r\nFind out more in those URLs:\r\n " +
215+
" - https://www.w3.org/Protocols/HTTP/1.0/spec.html#Message-Types\r\n" +
216+
" - https://github.com/curl/curl/issues/467\r\n").data(using: .utf8)!
217+
case "/LandOfTheLostCities/Gomorrah":
218+
/* just broken, hope that's not officially HTTP/0.9 :p */
219+
responseData = "HTTP/1.1\r\n\r\n\r\n".data(using: .utf8)!
220+
case "/LandOfTheLostCities/Myndus":
221+
responseData = ("HTTP/1.1 200 OK\r\n" +
222+
"\r\n" +
223+
"this is a body that isn't legal as it's " +
224+
"neither chunked encoding nor any Content-Length\r\n").data(using: .utf8)!
225+
case "/LandOfTheLostCities/Kameiros":
226+
responseData = ("HTTP/1.1 999 Wrong Code\r\n" +
227+
"illegal: status code (too large)\r\n" +
228+
"\r\n").data(using: .utf8)!
229+
case "/LandOfTheLostCities/Dinavar":
230+
responseData = ("HTTP/1.1 20 Too Few Digits\r\n" +
231+
"illegal: status code (too few digits)\r\n" +
232+
"\r\n").data(using: .utf8)!
233+
case "/LandOfTheLostCities/Kuhikugu":
234+
responseData = ("HTTP/1.1 2000 Too Many Digits\r\n" +
235+
"illegal: status code (too many digits)\r\n" +
236+
"\r\n").data(using: .utf8)!
237+
default:
238+
responseData = ("HTTP/1.1 500 Internal Server Error\r\n" +
239+
"case-missing-in: TestFoundation/HTTPServer.swift\r\n" +
240+
"\r\n").data(using: .utf8)!
241+
}
242+
try self.socket.writeRawData(responseData)
243+
}
244+
176245
}
177246

178247
struct _HTTPRequest {
@@ -249,7 +318,13 @@ public class TestURLSessionServer {
249318
}
250319

251320
public func readAndRespond() throws {
252-
try httpServer.respond(with: process(request: httpServer.request()), startDelay: self.startDelay, sendDelay: self.sendDelay, bodyChunks: self.bodyChunks)
321+
let req = try httpServer.request()
322+
if req.uri.hasPrefix("/LandOfTheLostCities/") {
323+
/* these are all misbehaving servers */
324+
try httpServer.respondWithBrokenResponses(uri: req.uri)
325+
} else {
326+
try httpServer.respond(with: process(request: req), startDelay: self.startDelay, sendDelay: self.sendDelay, bodyChunks: self.bodyChunks)
327+
}
253328
}
254329

255330
func process(request: _HTTPRequest) -> _HTTPResponse {

TestFoundation/TestNSURLSession.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class TestURLSession : XCTestCase {
4141
("test_customProtocolResponseWithDelegate", test_customProtocolResponseWithDelegate),
4242
("test_httpRedirection", test_httpRedirection),
4343
("test_httpRedirectionTimeout", test_httpRedirectionTimeout),
44+
("test_http0_9SimpleResponses", test_http0_9SimpleResponses),
45+
("test_outOfRangeButCorrectlyFormattedHTTPCode", test_outOfRangeButCorrectlyFormattedHTTPCode),
46+
("test_missingContentLengthButStillABody", test_missingContentLengthButStillABody),
47+
("test_illegalHTTPServerResponses", test_illegalHTTPServerResponses),
4448
]
4549
}
4650

@@ -378,6 +382,108 @@ class TestURLSession : XCTestCase {
378382
task.resume()
379383
waitForExpectations(timeout: 12)
380384
}
385+
386+
func test_http0_9SimpleResponses() {
387+
for brokenCity in ["Pompeii", "Sodom"] {
388+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/LandOfTheLostCities/\(brokenCity)"
389+
let url = URL(string: urlString)!
390+
391+
let config = URLSessionConfiguration.default
392+
config.timeoutIntervalForRequest = 8
393+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
394+
let expect = expectation(description: "URL test with completion handler for \(brokenCity)")
395+
var expectedResult = "unknown"
396+
let task = session.dataTask(with: url) { data, response, error in
397+
XCTAssertNotNil(data)
398+
XCTAssertNotNil(response)
399+
XCTAssertNil(error)
400+
401+
defer { expect.fulfill() }
402+
403+
guard let httpResponse = response as? HTTPURLResponse else {
404+
XCTFail("response (\(response.debugDescription)) invalid")
405+
return
406+
}
407+
XCTAssertEqual(200, httpResponse.statusCode, "HTTP response code is not 200")
408+
}
409+
task.resume()
410+
waitForExpectations(timeout: 12)
411+
}
412+
}
413+
414+
func test_outOfRangeButCorrectlyFormattedHTTPCode() {
415+
let brokenCity = "Kameiros"
416+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/LandOfTheLostCities/\(brokenCity)"
417+
let url = URL(string: urlString)!
418+
419+
let config = URLSessionConfiguration.default
420+
config.timeoutIntervalForRequest = 8
421+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
422+
let expect = expectation(description: "URL test with completion handler for \(brokenCity)")
423+
let task = session.dataTask(with: url) { data, response, error in
424+
XCTAssertNotNil(data)
425+
XCTAssertNotNil(response)
426+
XCTAssertNil(error)
427+
428+
defer { expect.fulfill() }
429+
430+
guard let httpResponse = response as? HTTPURLResponse else {
431+
XCTFail("response (\(response.debugDescription)) invalid")
432+
return
433+
}
434+
XCTAssertEqual(999, httpResponse.statusCode, "HTTP response code is not 999")
435+
}
436+
task.resume()
437+
waitForExpectations(timeout: 12)
438+
}
439+
440+
func test_missingContentLengthButStillABody() {
441+
let brokenCity = "Myndus"
442+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/LandOfTheLostCities/\(brokenCity)"
443+
let url = URL(string: urlString)!
444+
445+
let config = URLSessionConfiguration.default
446+
config.timeoutIntervalForRequest = 8
447+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
448+
let expect = expectation(description: "URL test with completion handler for \(brokenCity)")
449+
let task = session.dataTask(with: url) { data, response, error in
450+
XCTAssertNotNil(data)
451+
XCTAssertNotNil(response)
452+
XCTAssertNil(error)
453+
454+
defer { expect.fulfill() }
455+
456+
guard let httpResponse = response as? HTTPURLResponse else {
457+
XCTFail("response (\(response.debugDescription)) invalid")
458+
return
459+
}
460+
XCTAssertEqual(200, httpResponse.statusCode, "HTTP response code is not 200")
461+
}
462+
task.resume()
463+
waitForExpectations(timeout: 12)
464+
}
465+
466+
467+
func test_illegalHTTPServerResponses() {
468+
for brokenCity in ["Gomorrah", "Dinavar", "Kuhikugu"] {
469+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/LandOfTheLostCities/\(brokenCity)"
470+
let url = URL(string: urlString)!
471+
472+
let config = URLSessionConfiguration.default
473+
config.timeoutIntervalForRequest = 8
474+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
475+
let expect = expectation(description: "URL test with completion handler for \(brokenCity)")
476+
let task = session.dataTask(with: url) { data, response, error in
477+
XCTAssertNil(data)
478+
XCTAssertNil(response)
479+
XCTAssertNotNil(error)
480+
481+
defer { expect.fulfill() }
482+
}
483+
task.resume()
484+
waitForExpectations(timeout: 12)
485+
}
486+
}
381487
}
382488

383489
class SessionDelegate: NSObject, URLSessionDelegate {

0 commit comments

Comments
 (0)