/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.apollographql.apollo.internal.cache.http; import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.Collections; import java.util.List; import okhttp3.CipherSuite; import okhttp3.Handshake; import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.TlsVersion; import okhttp3.internal.http.HttpHeaders; import okhttp3.internal.http.StatusLine; import okhttp3.internal.platform.Platform; import okio.Buffer; import okio.BufferedSink; import okio.BufferedSource; import okio.ByteString; import okio.Okio; import okio.Sink; import okio.Source; /** * Class was copied and modified from {@link okhttp3.Cache.Entry} */ final class ResponseHeaderRecord { /** Synthetic response header: the local time when the request was sent. */ private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis"; /** Synthetic response header: the local time when the response was received. */ private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis"; private final String url; private final Headers varyHeaders; private final String requestMethod; private final Protocol protocol; private final int code; private final String message; private final Headers responseHeaders; private final Handshake handshake; private final long sentRequestMillis; private final long receivedResponseMillis; /** * Reads an entry from an input stream. A typical entry looks like this: * <pre>{@code * http://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * }</pre> * * <p>A typical HTTPS file looks like this: * <pre>{@code * https://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * * AES_256_WITH_MD5 * 2 * base64-encoded peerCertificate[0] * base64-encoded peerCertificate[1] * -1 * TLSv1.2 * }</pre> * The file is newline separated. The first two lines are the URL and the request method. Next * is the number of HTTP Vary request header lines, followed by those lines. * * <p>Next is the response status line, followed by the number of HTTP response header lines, * followed by those lines. * * <p>HTTPS responses also contain SSL session information. This begins with a blank line, and * then a line containing the cipher suite. Next is the length of the peer certificate chain. * These certificates are base64-encoded and appear each on their own line. The next line * contains the length of the local certificate chain. These certificates are also * base64-encoded and appear each on their own line. A length of -1 is used to encode a null * array. The last line is optional. If present, it contains the TLS version. */ ResponseHeaderRecord(Source in) throws IOException { try { BufferedSource source = Okio.buffer(in); url = source.readUtf8LineStrict(); requestMethod = source.readUtf8LineStrict(); Headers.Builder varyHeadersBuilder = new Headers.Builder(); int varyRequestHeaderLineCount = readInt(source); for (int i = 0; i < varyRequestHeaderLineCount; i++) { addHeaderLenient(varyHeadersBuilder, source.readUtf8LineStrict()); } varyHeaders = varyHeadersBuilder.build(); StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict()); protocol = statusLine.protocol; code = statusLine.code; message = statusLine.message; Headers.Builder responseHeadersBuilder = new Headers.Builder(); int responseHeaderLineCount = readInt(source); for (int i = 0; i < responseHeaderLineCount; i++) { addHeaderLenient(responseHeadersBuilder, source.readUtf8LineStrict()); } String sendRequestMillisString = responseHeadersBuilder.get(SENT_MILLIS); String receivedResponseMillisString = responseHeadersBuilder.get(RECEIVED_MILLIS); responseHeadersBuilder.removeAll(SENT_MILLIS); responseHeadersBuilder.removeAll(RECEIVED_MILLIS); sentRequestMillis = sendRequestMillisString != null ? Long.parseLong(sendRequestMillisString) : 0L; receivedResponseMillis = receivedResponseMillisString != null ? Long.parseLong(receivedResponseMillisString) : 0L; responseHeaders = responseHeadersBuilder.build(); if (isHttps()) { String blank = source.readUtf8LineStrict(); if (blank.length() > 0) { throw new IOException("expected \"\" but was \"" + blank + "\""); } String cipherSuiteString = source.readUtf8LineStrict(); CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString); List<Certificate> peerCertificates = readCertificateList(source); List<Certificate> localCertificates = readCertificateList(source); TlsVersion tlsVersion = !source.exhausted() ? TlsVersion.forJavaName(source.readUtf8LineStrict()) : null; handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates); } else { handshake = null; } } finally { in.close(); } } ResponseHeaderRecord(Response response) { this.url = response.request().url().toString(); this.varyHeaders = HttpHeaders.varyHeaders(response); this.requestMethod = response.request().method(); this.protocol = response.protocol(); this.code = response.code(); this.message = response.message(); this.responseHeaders = response.headers(); this.handshake = response.handshake(); this.sentRequestMillis = response.sentRequestAtMillis(); this.receivedResponseMillis = response.receivedResponseAtMillis(); } void writeTo(Sink sink) throws IOException { BufferedSink bufferedSink = Okio.buffer(sink); bufferedSink.writeUtf8(url) .writeByte('\n'); bufferedSink.writeUtf8(requestMethod) .writeByte('\n'); bufferedSink.writeDecimalLong(varyHeaders.size()) .writeByte('\n'); for (int i = 0, size = varyHeaders.size(); i < size; i++) { bufferedSink.writeUtf8(varyHeaders.name(i)) .writeUtf8(": ") .writeUtf8(varyHeaders.value(i)) .writeByte('\n'); } bufferedSink.writeUtf8(new StatusLine(protocol, code, message).toString()) .writeByte('\n'); bufferedSink.writeDecimalLong(responseHeaders.size() + 2) .writeByte('\n'); for (int i = 0, size = responseHeaders.size(); i < size; i++) { bufferedSink.writeUtf8(responseHeaders.name(i)) .writeUtf8(": ") .writeUtf8(responseHeaders.value(i)) .writeByte('\n'); } bufferedSink.writeUtf8(SENT_MILLIS) .writeUtf8(": ") .writeDecimalLong(sentRequestMillis) .writeByte('\n'); bufferedSink.writeUtf8(RECEIVED_MILLIS) .writeUtf8(": ") .writeDecimalLong(receivedResponseMillis) .writeByte('\n'); if (isHttps()) { bufferedSink.writeByte('\n'); bufferedSink.writeUtf8(handshake.cipherSuite().javaName()) .writeByte('\n'); writeCertList(bufferedSink, handshake.peerCertificates()); writeCertList(bufferedSink, handshake.localCertificates()); // The handshake’s TLS version is null on HttpsURLConnection and on older cached responses. if (handshake.tlsVersion() != null) { bufferedSink.writeUtf8(handshake.tlsVersion().javaName()) .writeByte('\n'); } } bufferedSink.close(); } private boolean isHttps() { return url.startsWith("https://"); } private List<Certificate> readCertificateList(BufferedSource source) throws IOException { int length = readInt(source); if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null. try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); List<Certificate> result = new ArrayList<>(length); for (int i = 0; i < length; i++) { String line = source.readUtf8LineStrict(); Buffer bytes = new Buffer(); bytes.write(ByteString.decodeBase64(line)); result.add(certificateFactory.generateCertificate(bytes.inputStream())); } return result; } catch (CertificateException e) { throw new IOException(e.getMessage()); } } private void writeCertList(BufferedSink sink, List<Certificate> certificates) throws IOException { try { sink.writeDecimalLong(certificates.size()) .writeByte('\n'); for (int i = 0, size = certificates.size(); i < size; i++) { byte[] bytes = certificates.get(i).getEncoded(); String line = ByteString.of(bytes).base64(); sink.writeUtf8(line) .writeByte('\n'); } } catch (CertificateEncodingException e) { throw new IOException(e.getMessage()); } } Response response() { Request cacheRequest = new Request.Builder() .url(url) .method(requestMethod, RequestBody.create(MediaType.parse("application/json; charset=utf-8"), "")) .headers(varyHeaders) .build(); return new Response.Builder() .request(cacheRequest) .protocol(protocol) .code(code) .message(message) .headers(responseHeaders) .handshake(handshake) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(receivedResponseMillis) .build(); } private static int readInt(BufferedSource source) throws IOException { try { long result = source.readDecimalLong(); String line = source.readUtf8LineStrict(); if (result < 0 || result > Integer.MAX_VALUE || !line.isEmpty()) { throw new IOException("expected an int but was \"" + result + line + "\""); } return (int) result; } catch (NumberFormatException e) { throw new IOException(e.getMessage()); } } private void addHeaderLenient(Headers.Builder headersBuilder, String line) { int index = line.indexOf(":", 1); if (index != -1) { headersBuilder.add(line.substring(0, index), line.substring(index + 1)); } else if (line.startsWith(":")) { // Work around empty header names and header names that start with a // colon (created by old broken SPDY versions of the response cache). headersBuilder.add("", line.substring(1)); // Empty header name. } else { headersBuilder.add("", line); // No header name. } } }