okhttp3.internal.cache2.Relay.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of impersonator Show documentation
Show all versions of impersonator Show documentation
Spoof TLS/JA3/JA4 and HTTP/2 fingerprints in Java
The newest version!
/*
* Copyright (C) 2016 Square, Inc.
*
* 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 okhttp3.internal.cache2
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import okhttp3.internal.closeQuietly
import okhttp3.internal.notifyAll
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.encodeUtf8
import okio.Source
import okio.Timeout
/**
* Replicates a single upstream source into multiple downstream sources. Each downstream source
* returns the same bytes as the upstream source. Downstream sources may read data either as it
* is returned by upstream, or after the upstream source has been exhausted.
*
* As bytes are returned from upstream they are written to a local file. Downstream sources read
* from this file as necessary.
*
* This class also keeps a small buffer of bytes recently read from upstream. This is intended to
* save a small amount of file I/O and data copying.
*/
class Relay private constructor(
/**
* Read/write persistence of the upstream source and its metadata. Its layout is as follows:
*
* * 16 bytes: either `OkHttp cache v1\n` if the persisted file is complete. This is another
* sequence of bytes if the file is incomplete and should not be used.
* * 8 bytes: *n*: upstream data size
* * 8 bytes: *m*: metadata size
* * *n* bytes: upstream data
* * *m* bytes: metadata
*
* This is closed and assigned to null when the last source is closed and no further sources
* are permitted.
*/
var file: RandomAccessFile?,
/**
* Null once the file has a complete copy of the upstream bytes. Only the [upstreamReader] thread
* may access this source.
*/
var upstream: Source?,
/** The number of bytes consumed from [upstream]. Guarded by this. */
var upstreamPos: Long,
/** User-supplied additional data persisted with the source data. */
private val metadata: ByteString,
/** The maximum size of [buffer]. */
val bufferMaxSize: Long
) {
/** The thread that currently has access to upstream. Possibly null. Guarded by this. */
var upstreamReader: Thread? = null
/**
* A buffer for [upstreamReader] to use when pulling bytes from upstream. Only the
* [upstreamReader] thread may access this buffer.
*/
val upstreamBuffer = Buffer()
/** True if there are no further bytes to read from [upstream]. Guarded by this. */
var complete = (upstream == null)
/** The most recently read bytes from [upstream]. This is a suffix of [file]. Guarded by this. */
val buffer = Buffer()
/**
* Reference count of the number of active sources reading this stream. When decremented to 0
* resources are released and all following calls to [.newSource] return null. Guarded by this.
*/
var sourceCount = 0
val isClosed: Boolean
get() = file == null
@Throws(IOException::class)
private fun writeHeader(
prefix: ByteString,
upstreamSize: Long,
metadataSize: Long
) {
val header = Buffer().apply {
write(prefix)
writeLong(upstreamSize)
writeLong(metadataSize)
require(size == FILE_HEADER_SIZE)
}
val fileOperator = FileOperator(file!!.channel)
fileOperator.write(0, header, FILE_HEADER_SIZE)
}
@Throws(IOException::class)
private fun writeMetadata(upstreamSize: Long) {
val metadataBuffer = Buffer()
metadataBuffer.write(metadata)
val fileOperator = FileOperator(file!!.channel)
fileOperator.write(FILE_HEADER_SIZE + upstreamSize, metadataBuffer, metadata.size.toLong())
}
@Throws(IOException::class)
fun commit(upstreamSize: Long) {
// Write metadata to the end of the file.
writeMetadata(upstreamSize)
file!!.channel.force(false)
// Once everything else is in place we can swap the dirty header for a clean one.
writeHeader(PREFIX_CLEAN, upstreamSize, metadata.size.toLong())
file!!.channel.force(false)
// This file is complete.
synchronized(this@Relay) {
complete = true
}
upstream?.closeQuietly()
upstream = null
}
fun metadata(): ByteString = metadata
/**
* Returns a new source that returns the same bytes as upstream. Returns null if this relay has
* been closed and no further sources are possible. In that case callers should retry after
* building a new relay with [.read].
*/
fun newSource(): Source? {
synchronized(this@Relay) {
if (file == null) return null
sourceCount++
}
return RelaySource()
}
internal inner class RelaySource : Source {
private val timeout = Timeout()
/** The operator to read and write the shared file. Null if this source is closed. */
private var fileOperator: FileOperator? = FileOperator(file!!.channel)
/** The next byte to read. This is always less than or equal to [upstreamPos]. */
private var sourcePos = 0L
/**
* Selects where to find the bytes for a read and read them. This is one of three sources.
*
* ## Upstream
*
* In this case the current thread is assigned as the upstream reader. We read bytes from
* upstream and copy them to both the file and to the buffer. Finally we release the upstream
* reader lock and return the new bytes.
*
* ## The file
*
* In this case we copy bytes from the file to the [sink].
*
* ## The buffer
*
* In this case the bytes are immediately copied into [sink] and the number of bytes copied is
* returned.
*
* If upstream would be selected but another thread is already reading upstream this will
* block until that read completes. It is possible to time out while waiting for that.
*/
@Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long {
check(fileOperator != null)
val source: Int = synchronized(this@Relay) {
// We need new data from upstream.
while (true) {
val upstreamPos = [email protected]
if (sourcePos != upstreamPos) break
// No more data upstream. We're done.
if (complete) return -1L
// Another thread is already reading. Wait for that.
if (upstreamReader != null) {
timeout.waitUntilNotified(this@Relay)
continue
}
// We will do the read.
upstreamReader = Thread.currentThread()
return@synchronized SOURCE_UPSTREAM
}
val bufferPos = upstreamPos - buffer.size
// Bytes of the read precede the buffer. Read from the file.
if (sourcePos < bufferPos) {
return@synchronized SOURCE_FILE
}
// The buffer has the data we need. Read from there and return immediately.
val bytesToRead = minOf(byteCount, upstreamPos - sourcePos)
buffer.copyTo(sink, sourcePos - bufferPos, bytesToRead)
sourcePos += bytesToRead
return bytesToRead
}
// Read from the file.
if (source == SOURCE_FILE) {
val bytesToRead = minOf(byteCount, upstreamPos - sourcePos)
fileOperator!!.read(FILE_HEADER_SIZE + sourcePos, sink, bytesToRead)
sourcePos += bytesToRead
return bytesToRead
}
// Read from upstream. This always reads a full buffer: that might be more than what the
// current call to Source.read() has requested.
try {
val upstreamBytesRead = upstream!!.read(upstreamBuffer, bufferMaxSize)
// If we've exhausted upstream, we're done.
if (upstreamBytesRead == -1L) {
commit(upstreamPos)
return -1L
}
// Update this source and prepare this call's result.
val bytesRead = minOf(upstreamBytesRead, byteCount)
upstreamBuffer.copyTo(sink, 0, bytesRead)
sourcePos += bytesRead
// Append the upstream bytes to the file.
fileOperator!!.write(
FILE_HEADER_SIZE + upstreamPos, upstreamBuffer.clone(), upstreamBytesRead)
synchronized(this@Relay) {
// Append new upstream bytes into the buffer. Trim it to its max size.
buffer.write(upstreamBuffer, upstreamBytesRead)
if (buffer.size > bufferMaxSize) {
buffer.skip(buffer.size - bufferMaxSize)
}
// Now that the file and buffer have bytes, adjust upstreamPos.
[email protected] += upstreamBytesRead
}
return bytesRead
} finally {
synchronized(this@Relay) {
upstreamReader = null
[email protected]()
}
}
}
override fun timeout(): Timeout = timeout
@Throws(IOException::class)
override fun close() {
if (fileOperator == null) return // Already closed.
fileOperator = null
var fileToClose: RandomAccessFile? = null
synchronized(this@Relay) {
sourceCount--
if (sourceCount == 0) {
fileToClose = file
file = null
}
}
fileToClose?.closeQuietly()
}
}
companion object {
// TODO(jwilson): what to do about timeouts? They could be different and unfortunately when any
// timeout is hit we like to tear down the whole stream.
private const val SOURCE_UPSTREAM = 1
private const val SOURCE_FILE = 2
@JvmField val PREFIX_CLEAN = "OkHttp cache v1\n".encodeUtf8()
@JvmField val PREFIX_DIRTY = "OkHttp DIRTY :(\n".encodeUtf8()
private const val FILE_HEADER_SIZE = 32L
/**
* Creates a new relay that reads a live stream from [upstream], using [file] to share that data
* with other sources.
*
* **Warning:** callers to this method must immediately call [newSource] to create a source and
* close that when they're done. Otherwise a handle to [file] will be leaked.
*/
@Throws(IOException::class)
fun edit(
file: File,
upstream: Source,
metadata: ByteString,
bufferMaxSize: Long
): Relay {
val randomAccessFile = RandomAccessFile(file, "rw")
val result = Relay(randomAccessFile, upstream, 0L, metadata, bufferMaxSize)
// Write a dirty header. That way if we crash we won't attempt to recover this.
randomAccessFile.setLength(0L)
result.writeHeader(PREFIX_DIRTY, -1L, -1L)
return result
}
/**
* Creates a relay that reads a recorded stream from [file].
*
* **Warning:** callers to this method must immediately call [newSource] to create a source and
* close that when they're done. Otherwise a handle to [file] will be leaked.
*/
@Throws(IOException::class)
fun read(file: File): Relay {
val randomAccessFile = RandomAccessFile(file, "rw")
val fileOperator = FileOperator(randomAccessFile.channel)
// Read the header.
val header = Buffer()
fileOperator.read(0, header, FILE_HEADER_SIZE)
val prefix = header.readByteString(PREFIX_CLEAN.size.toLong())
if (prefix != PREFIX_CLEAN) throw IOException("unreadable cache file")
val upstreamSize = header.readLong()
val metadataSize = header.readLong()
// Read the metadata.
val metadataBuffer = Buffer()
fileOperator.read(FILE_HEADER_SIZE + upstreamSize, metadataBuffer, metadataSize)
val metadata = metadataBuffer.readByteString()
// Return the result.
return Relay(randomAccessFile, null, upstreamSize, metadata, 0L)
}
}
}