diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java index e62f21836..d29589139 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java @@ -18,26 +18,48 @@ public final class StatusLine { // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 - // We allow empty message without leading white space since some servers - // do not send the white space when the message is empty. - boolean hasMessage = statusLine.length() > 13; - if (!statusLine.startsWith("HTTP/1.") - || statusLine.length() < 12 - || statusLine.charAt(8) != ' ' - || (hasMessage && statusLine.charAt(12) != ' ')) { + // Parse protocol like "HTTP/1.1" followed by a space. + int codeStart; + int httpMinorVersion; + if (statusLine.startsWith("HTTP/1.")) { + if (statusLine.length() < 9 || statusLine.charAt(8) != ' ') { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + httpMinorVersion = statusLine.charAt(7) - '0'; + codeStart = 9; + if (httpMinorVersion < 0 || httpMinorVersion > 9) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + } else if (statusLine.startsWith("ICY ")) { + // Shoutcast uses ICY instead of "HTTP/1.0". + httpMinorVersion = 0; + codeStart = 4; + } else { throw new ProtocolException("Unexpected status line: " + statusLine); } - int httpMinorVersion = statusLine.charAt(7) - '0'; - if (httpMinorVersion < 0 || httpMinorVersion > 9) { + + // Parse response code like "200". Always 3 digits. + if (statusLine.length() < codeStart + 3) { throw new ProtocolException("Unexpected status line: " + statusLine); } int responseCode; try { - responseCode = Integer.parseInt(statusLine.substring(9, 12)); + responseCode = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3)); } catch (NumberFormatException e) { throw new ProtocolException("Unexpected status line: " + statusLine); } - this.responseMessage = hasMessage ? statusLine.substring(13) : ""; + + // Parse an optional response message like "OK" or "Not Modified". If it + // exists, it is separated from the response code by a space. + String responseMessage = ""; + if (statusLine.length() > codeStart + 3) { + if (statusLine.charAt(codeStart + 3) != ' ') { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + responseMessage = statusLine.substring(codeStart + 4); + } + + this.responseMessage = responseMessage; this.responseCode = responseCode; this.statusLine = statusLine; this.httpMinorVersion = httpMinorVersion; @@ -64,5 +86,4 @@ public final class StatusLine { public String message() { return responseMessage; } - } diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java index bc92397e6..c3ea3a982 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java @@ -16,8 +16,10 @@ package com.squareup.okhttp.internal.http; import java.io.IOException; +import java.net.ProtocolException; import org.junit.Test; +import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; public final class StatusLineTest { @@ -53,4 +55,63 @@ public final class StatusLineTest { assertEquals(version, statusLine.httpMinorVersion()); assertEquals(code, statusLine.code()); } + + // https://github.com/square/okhttp/issues/386 + @Test public void shoutcast() throws IOException { + StatusLine statusLine = new StatusLine("ICY 200 OK"); + assertEquals("OK", statusLine.message()); + assertEquals(0, statusLine.httpMinorVersion()); + assertEquals(200, statusLine.code()); + } + + @Test public void missingProtocol() throws IOException { + assertInvalid(""); + assertInvalid(" "); + assertInvalid("200 OK"); + assertInvalid(" 200 OK"); + } + + @Test public void protocolVersions() throws IOException { + assertInvalid("HTTP/2.0 200 OK"); + assertInvalid("HTTP/2.1 200 OK"); + assertInvalid("HTTP/-.1 200 OK"); + assertInvalid("HTTP/1.- 200 OK"); + assertInvalid("HTTP/0.1 200 OK"); + assertInvalid("HTTP/101 200 OK"); + assertInvalid("HTTP/1.1_200 OK"); + } + + @Test public void nonThreeDigitCode() throws IOException { + assertInvalid("HTTP/1.1 OK"); + assertInvalid("HTTP/1.1 2 OK"); + assertInvalid("HTTP/1.1 20 OK"); + assertInvalid("HTTP/1.1 2000 OK"); + assertInvalid("HTTP/1.1 two OK"); + assertInvalid("HTTP/1.1 2"); + assertInvalid("HTTP/1.1 2000"); + assertInvalid("HTTP/1.1 two"); + } + + @Test public void truncated() throws IOException { + assertInvalid(""); + assertInvalid("H"); + assertInvalid("HTTP/1"); + assertInvalid("HTTP/1."); + assertInvalid("HTTP/1.1"); + assertInvalid("HTTP/1.1 "); + assertInvalid("HTTP/1.1 2"); + assertInvalid("HTTP/1.1 20"); + } + + @Test public void wrongMessageDelimiter() throws IOException { + assertInvalid("HTTP/1.1 200_"); + } + + private void assertInvalid(String statusLine) throws IOException { + try { + new StatusLine(statusLine); + fail(); + } catch (ProtocolException expected) { + } + } } diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java index 04734ccbc..5704b6b5c 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java @@ -1324,6 +1324,32 @@ public final class URLConnectionTest { } } + @Test public void shoutcast() throws Exception { + server.enqueue(new MockResponse().setStatus("ICY 200 OK") + // .addHeader("HTTP/1.0 200 OK") + .addHeader("Accept-Ranges: none") + .addHeader("Content-Type: audio/mpeg") + .addHeader("icy-br:128") + .addHeader("ice-audio-info: bitrate=128;samplerate=44100;channels=2") + .addHeader("icy-br:128") + .addHeader("icy-description:Rock") + .addHeader("icy-genre:riders") + .addHeader("icy-name:A2RRock") + .addHeader("icy-pub:1") + .addHeader("icy-url:http://www.A2Rradio.com") + .addHeader("Server: Icecast 2.3.3-kh8") + .addHeader("Cache-Control: no-cache") + .addHeader("Pragma: no-cache") + .addHeader("Expires: Mon, 26 Jul 1997 05:00:00 GMT") + .addHeader("icy-metaint:16000") + .setBody("mp3 data")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertContent("mp3 data", connection); + } + @Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception { server.play(); HttpURLConnection connection = client.open(server.getUrl("/"));