Their connection is back in the pool and may be carrying another request by then (in Plexamp Lite this cut read-ahead downloads short). From Plexamp Lite 1.14.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
605 lines
25 KiB
Java
605 lines
25 KiB
Java
package com.minamp.net;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.BufferedOutputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.Socket;
|
|
import java.security.SecureRandom;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Vector;
|
|
|
|
import com.minamp.bc.tls.DefaultTlsClient;
|
|
import com.minamp.bc.tls.NameType;
|
|
import com.minamp.bc.tls.ServerName;
|
|
import com.minamp.bc.tls.TlsAuthentication;
|
|
import com.minamp.bc.tls.TlsClientProtocol;
|
|
import com.minamp.bc.tls.TlsCredentials;
|
|
import com.minamp.bc.tls.TlsServerCertificate;
|
|
import com.minamp.bc.tls.TlsSession;
|
|
import com.minamp.bc.tls.CertificateRequest;
|
|
import com.minamp.bc.tls.crypto.TlsCrypto;
|
|
import com.minamp.bc.tls.crypto.impl.bc.BcTlsCrypto;
|
|
|
|
/**
|
|
* Small HTTP/1.1 client with its own TLS 1.2 (Bouncy Castle), keep-alive pooling and
|
|
* session resumption. Android 2.x's built-in SSL only speaks TLS 1.0 and lacks today's roots,
|
|
* so every request in the app goes through here. From Plexamp Lite; keep the copies in sync.
|
|
*/
|
|
public final class Http {
|
|
|
|
public static String USER_AGENT = "PlexampLite/1.0";
|
|
|
|
/** Optional HTTP CONNECT proxy, only used when running the networking code on a desktop JVM. */
|
|
public static String proxyHost;
|
|
public static int proxyPort;
|
|
|
|
private static TlsCrypto crypto;
|
|
private static final Map<String, TlsSession> sessions = new HashMap<String, TlsSession>();
|
|
private static final Map<String, List<Conn>> pool = new HashMap<String, List<Conn>>();
|
|
private static final long IDLE_MS = 25000;
|
|
|
|
private Http() {}
|
|
|
|
private static synchronized TlsCrypto crypto() {
|
|
if (crypto == null) crypto = new BcTlsCrypto(new SecureRandom());
|
|
return crypto;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ URL
|
|
|
|
public static final class Url {
|
|
public final boolean https;
|
|
public final String host;
|
|
public final int port;
|
|
public final String path; // includes query
|
|
|
|
public Url(String s) throws IOException {
|
|
String lower = s.toLowerCase(Locale.US);
|
|
int start;
|
|
if (lower.startsWith("https://")) { https = true; start = 8; }
|
|
else if (lower.startsWith("http://")) { https = false; start = 7; }
|
|
else throw new IOException("Bad URL: " + s);
|
|
int slash = s.indexOf('/', start);
|
|
String hostPort = slash < 0 ? s.substring(start) : s.substring(start, slash);
|
|
path = slash < 0 ? "/" : s.substring(slash);
|
|
int colon = hostPort.lastIndexOf(':');
|
|
if (colon > 0 && hostPort.indexOf(']') < colon) {
|
|
host = hostPort.substring(0, colon);
|
|
try { port = Integer.parseInt(hostPort.substring(colon + 1)); }
|
|
catch (NumberFormatException e) { throw new IOException("Bad port in " + s); }
|
|
} else {
|
|
host = hostPort;
|
|
port = https ? 443 : 80;
|
|
}
|
|
}
|
|
|
|
String key() { return (https ? "s:" : "p:") + host + ":" + port; }
|
|
|
|
String hostHeader() {
|
|
boolean dflt = (https && port == 443) || (!https && port == 80);
|
|
return dflt ? host : host + ":" + port;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ connection
|
|
|
|
static final class Conn {
|
|
final String key;
|
|
Socket socket;
|
|
InputStream in;
|
|
OutputStream out;
|
|
TlsClientProtocol tls;
|
|
long idleSince;
|
|
|
|
Conn(String key) { this.key = key; }
|
|
|
|
void close() {
|
|
try { if (tls != null) tls.close(); } catch (Exception ignored) {}
|
|
try { if (socket != null) socket.close(); } catch (Exception ignored) {}
|
|
}
|
|
}
|
|
|
|
private static Conn take(String key) {
|
|
synchronized (pool) {
|
|
List<Conn> list = pool.get(key);
|
|
if (list == null) return null;
|
|
long now = System.currentTimeMillis();
|
|
while (!list.isEmpty()) {
|
|
Conn c = list.remove(list.size() - 1);
|
|
if (now - c.idleSince < IDLE_MS && !c.socket.isClosed()) return c;
|
|
c.close();
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static void release(Conn c) {
|
|
c.idleSince = System.currentTimeMillis();
|
|
synchronized (pool) {
|
|
List<Conn> list = pool.get(c.key);
|
|
if (list == null) { list = new ArrayList<Conn>(); pool.put(c.key, list); }
|
|
if (list.size() >= 4) { list.remove(0).close(); }
|
|
list.add(c);
|
|
}
|
|
}
|
|
|
|
/** Forget resumable TLS sessions (next connection to each host does a full, verified handshake). */
|
|
static void forgetSessions() {
|
|
synchronized (sessions) { sessions.clear(); }
|
|
evictAll();
|
|
}
|
|
|
|
/** Drop all pooled connections (e.g. after the network changed). */
|
|
public static void evictAll() {
|
|
final List<Conn> doomed = new ArrayList<Conn>();
|
|
synchronized (pool) {
|
|
for (Iterator<List<Conn>> it = pool.values().iterator(); it.hasNext();) doomed.addAll(it.next());
|
|
pool.clear();
|
|
}
|
|
if (doomed.isEmpty()) return;
|
|
// closing TLS sends an encrypted alert: keep that off the (small-stack) caller thread
|
|
Threads.start(new Runnable() {
|
|
public void run() { for (int i = 0; i < doomed.size(); i++) doomed.get(i).close(); }
|
|
}, "http-evict");
|
|
}
|
|
|
|
private static Conn connect(final Url u, int timeoutMs) throws IOException {
|
|
Conn c = new Conn(u.key());
|
|
Socket s = new Socket();
|
|
boolean ok = false;
|
|
try {
|
|
if (proxyHost != null) {
|
|
s.connect(new InetSocketAddress(proxyHost, proxyPort), timeoutMs);
|
|
s.setSoTimeout(timeoutMs);
|
|
OutputStream po = s.getOutputStream();
|
|
String req = "CONNECT " + u.host + ":" + u.port + " HTTP/1.1\r\nHost: " + u.host + ":" + u.port + "\r\n\r\n";
|
|
po.write(req.getBytes("ISO-8859-1"));
|
|
po.flush();
|
|
InputStream pi = s.getInputStream();
|
|
String status = readLine(pi);
|
|
if (status == null || status.indexOf(" 200") < 0) throw new IOException("Proxy refused: " + status);
|
|
String line;
|
|
while ((line = readLine(pi)) != null && line.length() > 0) { /* skip headers */ }
|
|
} else {
|
|
s.connect(new InetSocketAddress(u.host, u.port), timeoutMs);
|
|
s.setSoTimeout(timeoutMs);
|
|
}
|
|
s.setTcpNoDelay(true);
|
|
c.socket = s;
|
|
if (u.https) {
|
|
TlsClientProtocol proto = new TlsClientProtocol(s.getInputStream(), s.getOutputStream());
|
|
proto.connect(new Client(crypto(), u.host, u.key()));
|
|
c.tls = proto;
|
|
c.in = new BufferedInputStream(proto.getInputStream(), 8192);
|
|
c.out = new BufferedOutputStream(proto.getOutputStream(), 4096);
|
|
} else {
|
|
c.in = new BufferedInputStream(s.getInputStream(), 8192);
|
|
c.out = new BufferedOutputStream(s.getOutputStream(), 4096);
|
|
}
|
|
ok = true;
|
|
return c;
|
|
} finally {
|
|
if (!ok) { try { s.close(); } catch (Exception ignored) {} }
|
|
}
|
|
}
|
|
|
|
private static final class Client extends DefaultTlsClient {
|
|
private final String host;
|
|
private final String sessionKey;
|
|
|
|
private final long started = System.currentTimeMillis();
|
|
|
|
Client(TlsCrypto crypto, String host, String sessionKey) {
|
|
super(crypto);
|
|
this.host = host;
|
|
this.sessionKey = sessionKey;
|
|
}
|
|
|
|
@Override
|
|
protected Vector getSNIServerNames() {
|
|
if (CertVerifier.isIpLiteral(host)) return null;
|
|
Vector v = new Vector();
|
|
try {
|
|
v.addElement(new ServerName(NameType.host_name, host.getBytes("US-ASCII")));
|
|
} catch (java.io.UnsupportedEncodingException e) {
|
|
return null;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/**
|
|
* TLS 1.2 only: this TLS stack can resume 1.2 sessions but not 1.3 ones, and on a 600 MHz
|
|
* phone a resumed handshake (no key exchange, no signature checks) is the difference
|
|
* between ~10 s and well under a second for every new connection.
|
|
*/
|
|
@Override
|
|
protected com.minamp.bc.tls.ProtocolVersion[] getSupportedVersions() {
|
|
return com.minamp.bc.tls.ProtocolVersion.TLSv12.only();
|
|
}
|
|
|
|
@Override
|
|
public TlsSession getSessionToResume() {
|
|
synchronized (sessions) { return sessions.get(sessionKey); }
|
|
}
|
|
|
|
@Override
|
|
public void notifyHandshakeComplete() throws IOException {
|
|
super.notifyHandshakeComplete();
|
|
boolean resumed = context.getSecurityParametersConnection().isResumedSession();
|
|
DiagLog.log("TLS " + (resumed ? "resumed" : "full handshake") + " with " + host
|
|
+ " in " + (System.currentTimeMillis() - started) + "ms");
|
|
TlsSession s = context.getSession();
|
|
if (s != null && s.isResumable()) {
|
|
synchronized (sessions) { sessions.put(sessionKey, s); }
|
|
}
|
|
}
|
|
|
|
public TlsAuthentication getAuthentication() throws IOException {
|
|
return new TlsAuthentication() {
|
|
public void notifyServerCertificate(TlsServerCertificate sc) throws IOException {
|
|
com.minamp.bc.tls.Certificate cert = sc.getCertificate();
|
|
List<byte[]> chain = new ArrayList<byte[]>();
|
|
for (int i = 0; i < cert.getLength(); i++) chain.add(cert.getCertificateAt(i).getEncoded());
|
|
CertVerifier.verify(chain, host);
|
|
}
|
|
|
|
public TlsCredentials getClientCredentials(CertificateRequest req) {
|
|
return null;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ request / response
|
|
|
|
public static final class Response {
|
|
public final int code;
|
|
public final String status;
|
|
public final Map<String, String> headers; // lower-case names
|
|
public final InputStream body;
|
|
public final long contentLength; // -1 if unknown
|
|
public final boolean chunked;
|
|
|
|
Response(int code, String status, Map<String, String> headers, InputStream body, long len, boolean chunked) {
|
|
this.code = code; this.status = status; this.headers = headers; this.body = body;
|
|
this.contentLength = len; this.chunked = chunked;
|
|
}
|
|
|
|
public String header(String name) { return headers.get(name.toLowerCase(Locale.US)); }
|
|
|
|
public void close() {
|
|
try { body.close(); } catch (IOException ignored) {}
|
|
}
|
|
|
|
public byte[] readAll() throws IOException {
|
|
try {
|
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(contentLength > 0 && contentLength < 4000000 ? (int) contentLength : 8192);
|
|
byte[] buf = new byte[8192];
|
|
int n;
|
|
while ((n = body.read(buf)) > 0) bos.write(buf, 0, n);
|
|
return bos.toByteArray();
|
|
} finally {
|
|
close();
|
|
}
|
|
}
|
|
|
|
public String readString() throws IOException {
|
|
return new String(readAll(), "UTF-8");
|
|
}
|
|
}
|
|
|
|
public static Response get(String url, Map<String, String> headers, int timeoutMs) throws IOException {
|
|
return request("GET", url, headers, null, timeoutMs);
|
|
}
|
|
|
|
public static Response request(String method, String url, Map<String, String> headers, byte[] body, int timeoutMs) throws IOException {
|
|
return request(method, url, headers, body, timeoutMs, true);
|
|
}
|
|
|
|
/** A single request; 3xx responses are returned to the caller. */
|
|
public static Response requestNoRedirect(String method, String url, Map<String, String> headers, byte[] body, int timeoutMs) throws IOException {
|
|
return request(method, url, headers, body, timeoutMs, false);
|
|
}
|
|
|
|
private static Response request(String method, String url, Map<String, String> headers, byte[] body, int timeoutMs, boolean follow) throws IOException {
|
|
for (int redirects = 0; ; redirects++) {
|
|
long t0 = System.currentTimeMillis();
|
|
Response r;
|
|
try {
|
|
r = requestOnce(method, url, headers, body, timeoutMs);
|
|
} catch (IOException e) {
|
|
DiagLog.log(method + " " + DiagLog.redact(url) + " -> FAILED after " + (System.currentTimeMillis() - t0) + "ms: " + Threads.describe(e));
|
|
throw e;
|
|
} catch (RuntimeException e) {
|
|
DiagLog.log(method + " " + DiagLog.redact(url) + " -> FAILED: " + Threads.describe(e));
|
|
throw e;
|
|
}
|
|
if (!url.contains("/photo/:/transcode") && !url.contains("/cover")) { // artwork would drown out everything else
|
|
DiagLog.log(method + " " + DiagLog.redact(url) + " -> " + r.code + " (" + (System.currentTimeMillis() - t0) + "ms)");
|
|
}
|
|
if (follow && (r.code == 301 || r.code == 302 || r.code == 307 || r.code == 308) && redirects < 4
|
|
&& ("GET".equals(method) || "HEAD".equals(method))) {
|
|
String loc = r.header("location");
|
|
r.close();
|
|
if (loc == null) throw new IOException("Redirect without location");
|
|
if (loc.startsWith("/")) {
|
|
Url u = new Url(url);
|
|
loc = (u.https ? "https://" : "http://") + u.hostHeader() + loc;
|
|
}
|
|
url = loc;
|
|
continue;
|
|
}
|
|
return r;
|
|
}
|
|
}
|
|
|
|
private static Response requestOnce(String method, String url, Map<String, String> headers, byte[] body, int timeoutMs) throws IOException {
|
|
Url u = new Url(url);
|
|
// A pooled connection may have been closed by the server; retry once on a fresh one.
|
|
for (int attempt = 0; attempt < 2; attempt++) {
|
|
Conn c = take(u.key());
|
|
boolean pooled = c != null;
|
|
if (c == null) c = connect(u, timeoutMs);
|
|
try {
|
|
c.socket.setSoTimeout(timeoutMs);
|
|
writeRequest(c, u, method, headers, body);
|
|
return readResponse(c, method);
|
|
} catch (IOException e) {
|
|
c.close();
|
|
if (!pooled) throw e;
|
|
}
|
|
}
|
|
throw new IOException("Request failed");
|
|
}
|
|
|
|
private static void writeRequest(Conn c, Url u, String method, Map<String, String> headers, byte[] body) throws IOException {
|
|
StringBuilder sb = new StringBuilder(512);
|
|
sb.append(method).append(' ').append(u.path).append(" HTTP/1.1\r\n");
|
|
sb.append("Host: ").append(u.hostHeader()).append("\r\n");
|
|
boolean hasUa = false;
|
|
if (headers != null) {
|
|
for (Map.Entry<String, String> e : headers.entrySet()) {
|
|
if (e.getValue() == null) continue;
|
|
if ("user-agent".equalsIgnoreCase(e.getKey())) hasUa = true;
|
|
sb.append(e.getKey()).append(": ").append(e.getValue()).append("\r\n");
|
|
}
|
|
}
|
|
if (!hasUa) sb.append("User-Agent: ").append(USER_AGENT).append("\r\n");
|
|
sb.append("Connection: keep-alive\r\n");
|
|
if (body != null || "POST".equals(method) || "PUT".equals(method)) {
|
|
sb.append("Content-Length: ").append(body == null ? 0 : body.length).append("\r\n");
|
|
}
|
|
sb.append("\r\n");
|
|
c.out.write(sb.toString().getBytes("UTF-8"));
|
|
if (body != null) c.out.write(body);
|
|
c.out.flush();
|
|
}
|
|
|
|
private static Response readResponse(Conn c, String method) throws IOException {
|
|
String statusLine = readLine(c.in);
|
|
if (statusLine == null) throw new IOException("Connection closed");
|
|
// HTTP/1.1 200 OK
|
|
int sp = statusLine.indexOf(' ');
|
|
if (sp < 0) throw new IOException("Bad status line");
|
|
int sp2 = statusLine.indexOf(' ', sp + 1);
|
|
int code;
|
|
try {
|
|
code = Integer.parseInt(sp2 < 0 ? statusLine.substring(sp + 1).trim() : statusLine.substring(sp + 1, sp2));
|
|
} catch (NumberFormatException e) {
|
|
throw new IOException("Bad status line");
|
|
}
|
|
boolean http10 = statusLine.startsWith("HTTP/1.0");
|
|
Map<String, String> h = new LinkedHashMap<String, String>();
|
|
String line;
|
|
while ((line = readLine(c.in)) != null && line.length() > 0) {
|
|
int colon = line.indexOf(':');
|
|
if (colon <= 0) continue;
|
|
String name = line.substring(0, colon).trim().toLowerCase(Locale.US);
|
|
String val = line.substring(colon + 1).trim();
|
|
String prev = h.get(name);
|
|
h.put(name, prev == null ? val : prev + ", " + val);
|
|
}
|
|
String te = h.get("transfer-encoding");
|
|
String cl = h.get("content-length");
|
|
String conn = h.get("connection");
|
|
boolean keepAlive = !http10 && (conn == null || conn.toLowerCase(Locale.US).indexOf("close") < 0);
|
|
boolean chunked = te != null && te.toLowerCase(Locale.US).indexOf("chunked") >= 0;
|
|
long len = -1;
|
|
if (!chunked && cl != null) {
|
|
try { len = Long.parseLong(cl.trim()); } catch (NumberFormatException ignored) {}
|
|
}
|
|
InputStream body;
|
|
boolean noBody = "HEAD".equals(method) || code == 204 || code == 304 || (code >= 100 && code < 200);
|
|
if (noBody) {
|
|
body = new FixedStream(c, 0, keepAlive);
|
|
len = "HEAD".equals(method) ? len : 0;
|
|
} else if (chunked) {
|
|
body = new ChunkedStream(c, keepAlive);
|
|
} else if (len >= 0) {
|
|
body = new FixedStream(c, len, keepAlive);
|
|
} else {
|
|
body = new EofStream(c);
|
|
}
|
|
return new Response(code, statusLine, h, body, len, chunked);
|
|
}
|
|
|
|
static String readLine(InputStream in) throws IOException {
|
|
StringBuilder sb = new StringBuilder(64);
|
|
int b;
|
|
while ((b = in.read()) >= 0) {
|
|
if (b == '\n') {
|
|
int n = sb.length();
|
|
if (n > 0 && sb.charAt(n - 1) == '\r') sb.setLength(n - 1);
|
|
return sb.toString();
|
|
}
|
|
sb.append((char) b);
|
|
if (sb.length() > 16384) throw new IOException("Header line too long");
|
|
}
|
|
return sb.length() == 0 ? null : sb.toString();
|
|
}
|
|
|
|
// ------------------------------------------------------------------ body streams
|
|
|
|
private static final class FixedStream extends InputStream {
|
|
private final Conn c; private long left; private final boolean keepAlive; private volatile boolean closed;
|
|
|
|
FixedStream(Conn c, long len, boolean keepAlive) {
|
|
this.c = c; this.left = len; this.keepAlive = keepAlive;
|
|
if (len == 0) finish(true);
|
|
}
|
|
|
|
private synchronized void finish(boolean complete) {
|
|
if (closed) return;
|
|
closed = true;
|
|
if (complete && keepAlive) release(c); else c.close();
|
|
}
|
|
|
|
synchronized void abort() {
|
|
if (closed) return; // done: the connection may already be back in the pool, serving someone else
|
|
closed = true;
|
|
try { c.socket.close(); } catch (Exception ignored) {}
|
|
}
|
|
|
|
@Override public int read() throws IOException {
|
|
byte[] one = new byte[1];
|
|
int n = read(one, 0, 1);
|
|
return n <= 0 ? -1 : one[0] & 255;
|
|
}
|
|
|
|
@Override public int read(byte[] b, int off, int len) throws IOException {
|
|
if (left <= 0) { finish(true); return -1; }
|
|
if (closed) return -1;
|
|
int n = c.in.read(b, off, (int) Math.min(len, left));
|
|
if (n < 0) { finish(false); throw new IOException("Unexpected end of stream"); }
|
|
left -= n;
|
|
if (left == 0) finish(true);
|
|
return n;
|
|
}
|
|
|
|
@Override public int available() throws IOException {
|
|
if (closed) return 0;
|
|
return (int) Math.min(c.in.available(), left);
|
|
}
|
|
|
|
@Override public void close() { finish(left == 0); }
|
|
}
|
|
|
|
private static final class ChunkedStream extends InputStream {
|
|
private final Conn c; private final boolean keepAlive;
|
|
private long chunkLeft = 0; private boolean eof; private volatile boolean closed;
|
|
|
|
ChunkedStream(Conn c, boolean keepAlive) { this.c = c; this.keepAlive = keepAlive; }
|
|
|
|
private synchronized void finish(boolean complete) {
|
|
if (closed) return;
|
|
closed = true;
|
|
if (complete && keepAlive) release(c); else c.close();
|
|
}
|
|
|
|
synchronized void abort() {
|
|
if (closed) return; // done: the connection may already be back in the pool, serving someone else
|
|
closed = true;
|
|
try { c.socket.close(); } catch (Exception ignored) {}
|
|
}
|
|
|
|
private boolean nextChunk() throws IOException {
|
|
if (eof) return false;
|
|
String line = readLine(c.in);
|
|
if (line == null) throw new IOException("Unexpected end of chunked stream");
|
|
if (line.length() == 0) { line = readLine(c.in); if (line == null) throw new IOException("Bad chunk"); }
|
|
int semi = line.indexOf(';');
|
|
if (semi >= 0) line = line.substring(0, semi);
|
|
try { chunkLeft = Long.parseLong(line.trim(), 16); }
|
|
catch (NumberFormatException e) { throw new IOException("Bad chunk size"); }
|
|
if (chunkLeft == 0) {
|
|
String t;
|
|
while ((t = readLine(c.in)) != null && t.length() > 0) { /* trailers */ }
|
|
eof = true;
|
|
finish(true);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override public int read() throws IOException {
|
|
byte[] one = new byte[1];
|
|
int n = read(one, 0, 1);
|
|
return n <= 0 ? -1 : one[0] & 255;
|
|
}
|
|
|
|
@Override public int read(byte[] b, int off, int len) throws IOException {
|
|
if (closed) return -1;
|
|
if (chunkLeft == 0) {
|
|
if (!nextChunk()) return -1;
|
|
}
|
|
int n = c.in.read(b, off, (int) Math.min(len, chunkLeft));
|
|
if (n < 0) { finish(false); throw new IOException("Unexpected end of chunk"); }
|
|
chunkLeft -= n;
|
|
if (chunkLeft == 0) {
|
|
// consume CRLF after chunk data
|
|
String crlf = readLine(c.in);
|
|
if (crlf == null) { finish(false); }
|
|
else if (crlf.length() != 0) { finish(false); throw new IOException("Bad chunk terminator"); }
|
|
}
|
|
return n;
|
|
}
|
|
|
|
@Override public int available() throws IOException {
|
|
if (closed) return 0;
|
|
return (int) Math.min(c.in.available(), chunkLeft);
|
|
}
|
|
|
|
@Override public void close() { finish(eof); }
|
|
}
|
|
|
|
private static final class EofStream extends InputStream {
|
|
private final Conn c; private volatile boolean closed;
|
|
EofStream(Conn c) { this.c = c; }
|
|
|
|
@Override public int read() throws IOException {
|
|
if (closed) return -1;
|
|
int b = c.in.read();
|
|
if (b < 0) close();
|
|
return b;
|
|
}
|
|
|
|
@Override public int read(byte[] b, int off, int len) throws IOException {
|
|
if (closed) return -1;
|
|
int n = c.in.read(b, off, len);
|
|
if (n < 0) close();
|
|
return n;
|
|
}
|
|
|
|
@Override public synchronized void close() {
|
|
if (!closed) { closed = true; c.close(); }
|
|
}
|
|
|
|
synchronized void abort() {
|
|
if (closed) return;
|
|
closed = true;
|
|
try { c.socket.close(); } catch (Exception ignored) {}
|
|
}
|
|
}
|
|
|
|
/** Close one in-flight response's socket from another thread (used to abort streams). A response that has
|
|
* already been read to the end is left alone: its connection went back to the pool and may be carrying
|
|
* another request by now (closing it cut read-ahead downloads short: "Unexpected end of stream"). */
|
|
public static void abort(Response r) {
|
|
if (r == null) return;
|
|
InputStream b = r.body;
|
|
if (b instanceof FixedStream) ((FixedStream) b).abort();
|
|
else if (b instanceof ChunkedStream) ((ChunkedStream) b).abort();
|
|
else if (b instanceof EofStream) ((EofStream) b).abort();
|
|
}
|
|
}
|