CipherInputStream 跳过方法不适用于动态流式传输时的加密远程音频文件

问题描述 投票:0回答:1

我需要解密 AES/ECB/PCSK5Padding 加密的远程音频文件并使用 ExoPlayer 即时播放它。我实现了一个自定义

DataSource
来处理
read()
open()
方法。当音频文件从开头(位置 = 0)开始时它工作正常,但当它从其他位置开始或当我寻找音频时,密码会崩溃并裁剪文件,导致 ExoPlayer 错误。 通过搜索,我意识到问题涉及 CipherInputStream 的
skip()
方法,该方法应该在加密算法方面被重写。 有了以上条件,我该如何实现
skip()
方法呢?

这里是

HttpCipherEncryptedDataSource
作为自定义
DataSource


class HttpCipherEncryptedDataSource(
    private val key: ByteArray,
) : DataSource {

    private val connectionMaker = HttpConnectionMaker()

    private var connection: HttpURLConnection? = null
    private var cipherInputStream: CipherHttpInputStream? = null
    private var dataSpec: DataSpec? = null
    private var uri: Uri? = null

    private var bytesToRead: Long = 0
    private var bytesRead: Long = 0
    private var isOpen = false

    override fun open(dataSpec: DataSpec): Long {
        this.uri = dataSpec.uri
        this.dataSpec = dataSpec

        // make server connection
        connection = connectionMaker.make(dataSpec)
        val responseCode = connection!!.responseCode
        val responseMessage = connection!!.responseMessage

        // Check for a valid response code.
        if (responseCode < 200 || responseCode > 299) {
            val headers = connection!!.headerFields
            if (responseCode == 416) {
                val documentSize =
                    HttpUtil.getDocumentSize(connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE))
                if (dataSpec.position == documentSize) {
                    isOpen = true
                    return if (dataSpec.length != C.LENGTH_UNSET.toLong()) dataSpec.length else 0
                }
            }
            val errorStream = connection!!.errorStream
            val errorResponseBody = try {
                if (errorStream != null) Util.toByteArray(errorStream) else Util.EMPTY_BYTE_ARRAY
            } catch (e: IOException) {
                Util.EMPTY_BYTE_ARRAY
            }
            connectionMaker.closeConnection()
            val cause: IOException? =
                if (responseCode == 416) DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) else null
            throw InvalidResponseCodeException(
                responseCode, responseMessage, cause, headers, dataSpec, errorResponseBody
            )
        }

        // calculate current position
        val bytesToSkip =
            if (responseCode == 200 && dataSpec.position != 0L) dataSpec.position else 0

        // Determine the length of the data to be read, after skipping.
        val isCompressed = isCompressed(connection!!)
        if (!isCompressed) {
            bytesToRead = if (dataSpec.length != C.LENGTH_UNSET.toLong()) {
                dataSpec.length
            } else {
                val contentLength = HttpUtil.getContentLength(
                    connection!!.getHeaderField(HttpHeaders.CONTENT_LENGTH),
                    connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE)
                )
                if (contentLength != C.LENGTH_UNSET.toLong()) contentLength - bytesToSkip else C.LENGTH_UNSET.toLong()
            }
        } else {
            // Gzip is enabled. If the server opts to use gzip then the content length in the response
            // will be that of the compressed data, which isn't what we want. Always use the dataSpec
            // length in this case.
            bytesToRead = dataSpec.length
        }

        var encryptedStream: InputStream?
        try {
            encryptedStream = connection!!.inputStream
            if (isCompressed) {
                encryptedStream = GZIPInputStream(encryptedStream)
            }
            setupCipherInputStream(encryptedStream!!)
            cipherInputStream?.forceSkip(dataSpec.position)
        } catch (e: IOException) {
            connectionMaker.closeConnection()
            throw HttpDataSourceException(
                e,
                dataSpec,
                PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
                HttpDataSourceException.TYPE_OPEN
            )
        }
        isOpen = true
        return bytesToRead
    }

    private fun setupCipherInputStream(encryptedFileStream: InputStream) {
        val keySpec = SecretKeySpec(
            key,
            "AES"
        )
        val cipher = Cipher.getInstance(
            "AES/ECB/PCSK5Padding"
        )
        cipherInputStream = CipherHttpInputStream(
            encryptedFileStream,
            cipher,
            keySpec
        )
    }

    private fun isCompressed(connection: HttpURLConnection): Boolean {
        val contentEncoding = connection.getHeaderField("Content-Encoding")
        return "gzip".equals(contentEncoding, ignoreCase = true)
    }

    @Throws(HttpDataSourceException::class)
    override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
        try {
            var readLength = length
            if (readLength == 0) {
                return 0
            }
            if (bytesToRead != C.LENGTH_UNSET.toLong()) {
                val bytesRemaining: Long = bytesToRead - bytesRead
                if (bytesRemaining == 0L) {
                    return C.RESULT_END_OF_INPUT
                }
                readLength = Math.min(readLength.toLong(), bytesRemaining).toInt()
            }

            val read = Util.castNonNull<InputStream>(cipherInputStream).read(buffer, offset, readLength)
            if (read == -1) {
                return C.RESULT_END_OF_INPUT
            }

            bytesRead += read.toLong()
            return read
        } catch (e: IOException) {
            throw HttpDataSourceException.createForIOException(
                e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
            )
        }
    }

    override fun addTransferListener(transferListener: TransferListener) {}

    override fun getUri() = uri

    @Throws(HttpDataSourceException::class)
    override fun close() {
        try {
            val inputStream: InputStream? = this.cipherInputStream
            if (inputStream != null) {
                val bytesRemaining =
                    if (bytesToRead == C.LENGTH_UNSET.toLong()) C.LENGTH_UNSET.toLong() else bytesToRead - bytesRead
                maybeTerminateInputStream(connection, bytesRemaining)
                try {
                    inputStream.close()
                } catch (e: IOException) {
                    throw HttpDataSourceException(
                        e,
                        Util.castNonNull(dataSpec),
                        PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
                        HttpDataSourceException.TYPE_CLOSE
                    )
                }
            }
        } finally {
            cipherInputStream = null
            connectionMaker.closeConnection()
            if (isOpen) {
                isOpen = false
            }
        }
    }

    private fun maybeTerminateInputStream(connection: HttpURLConnection?, bytesRemaining: Long) {
        if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
            return
        }
        try {
            val inputStream = connection.inputStream
            if (bytesRemaining == C.LENGTH_UNSET.toLong()) {
                // If the input stream has already ended, do nothing. The socket may be re-used.
                if (inputStream.read() == -1) {
                    return
                }
            } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
                // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
                // re-used.
                return
            }
            val className = inputStream.javaClass.name
            if ("com.android.okhttp.internal.http.HttpTransport\$ChunkedInputStream" == className
                || ("com.android.okhttp.internal.http.HttpTransport\$FixedLengthInputStream"
                        == className)
            ) {
                val superclass: Class<in InputStream>? = inputStream.javaClass.superclass
                val unexpectedEndOfInput =
                    Assertions.checkNotNull(superclass).getDeclaredMethod("unexpectedEndOfInput")
                unexpectedEndOfInput.isAccessible = true
                unexpectedEndOfInput.invoke(inputStream)
            }
        } catch (e: Exception) {
            // If an IOException then the connection didn't ever have an input stream, or it was closed
            // already. If another type of exception then something went wrong, most likely the device
            // isn't using okhttp.
            e.printStackTrace()
        }
    }

    companion object {
        private const val MAX_BYTES_TO_DRAIN: Long = 2048
    }
}

还有

CipherHttpInputStream
处理
skip
方法:


class CipherHttpInputStream(
    private val upstream: InputStream,
    private val cipher: Cipher,
    private val secretKeySpec: SecretKeySpec,
) : CipherInputStream(upstream, cipher) {

    private val MAX_SKIP_BUFFER_SIZE = 2048

    fun forceSkip(bytesToSkip: Long) {
        var remaining: Long = bytesToSkip
        var nr: Int

        if (bytesToSkip <= 0) {
            return
        }
        val size = Math.min(MAX_SKIP_BUFFER_SIZE.toLong(), remaining).toInt()
        val skipBuffer = ByteArray(size)
        initCipher()
        while (remaining > 0) {
            nr = upstream.read(skipBuffer, 0, Math.min(size.toLong(), remaining).toInt())
            if (nr < 0) {
                break
            }
            remaining -= nr.toLong()
        }
    }

    private fun initCipher() {
        cipher.init(
            Cipher.DECRYPT_MODE,
            secretKeySpec,
        )
    }

    override fun available(): Int {
        return upstream.available()
    }

}
android encryption aes exoplayer
1个回答
0
投票

经过几天的搜索和调试,最终我发现了问题所在。由于此 DataSource 负责播放远程加密音频文件,因此每次调用

open()
时,都会实例化一个新的
InputStream
来包装文件流,从 ExoPlayer 应该启动的确切位置开始。所以这种场景下就没有必要使用
skip()
方法了。但是,该错误仍然引用文件的起始位置。由于 CipherInputStream 逐块(
cipher.blockSize
)读取上游,如果起始位置不能被
cipher.blockSize
整除,则会导致
ERROR_CODE_PARSING_CONTAINER_MALFORMED
ExoPlayer 错误。 为了解决这个障碍,我编写了以下函数来计算有关密码块大小的适当起始位置并将其存储在
dataSpec
中。

这是函数:

// check if the new position divided by cipher.blockSize
// results in zero. If not truncate the remaining.
private fun modifyBytesBlocks(dataSpec: DataSpec): DataSpec {
    val bytesSinceStartOfCurrentBlock = dataSpec.position % cipher.blockSize
    var bytesUntilPreviousBlockStart =
        dataSpec.position - bytesSinceStartOfCurrentBlock - cipher.blockSize
    if (bytesUntilPreviousBlockStart < 0) bytesUntilPreviousBlockStart = 0

    return DataSpec(
        dataSpec.uri,
        dataSpec.httpMethod,
        dataSpec.httpBody,
        bytesUntilPreviousBlockStart,
        bytesUntilPreviousBlockStart,
        dataSpec.length,
        dataSpec.key,
        dataSpec.flags,
        dataSpec.httpRequestHeaders
    )
}

应该用在

open
的开头并替换原来的
dataSpec
。这是整个工作习惯
DataSource
来源:


class HttpCipherEncryptedDataSource(key: ByteArray) : DataSource {

    private val connectionMaker = HttpConnectionMaker()
    private val keySpec = SecretKeySpec(
        key,
        "AES"
    )
   private val cipher = Cipher.getInstance(
        "AES/ECB/PKCS5Padding"
    )

    private var connection: HttpURLConnection? = null
    private var cipherInputStream: CipherHttpInputStream? = null
    private var updatedDataSpec: DataSpec? = null
    private var uri: Uri? = null

    private var bytesToRead: Long = 0
    private var bytesRead: Long = 0
    private var isOpen = false

    override fun open(dataSpec: DataSpec): Long {
        bytesRead = 0
        bytesToRead = 0
        this.uri = dataSpec.uri
        this.updatedDataSpec = modifyBytesBlocks(dataSpec)
        val responseCode: Int
        val responseMessage: String
        try {
            // make server connection
            connection = connectionMaker.make(updatedDataSpec!!)
             responseCode = connection!!.responseCode
             responseMessage = connection!!.responseMessage
        } catch (e: IOException) {
            connectionMaker.closeConnection()
            throw HttpDataSourceException.createForIOException(
                e, dataSpec, HttpDataSourceException.TYPE_OPEN
            )
        }
        // Check for a valid response code.
        if (responseCode < 200 || responseCode > 299) {
            val headers = connection!!.headerFields
            if (responseCode == 416) {
                val documentSize =
                    HttpUtil.getDocumentSize(connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE))
                if (updatedDataSpec!!.position == documentSize) {
                    isOpen = true
                    return if (updatedDataSpec!!.length != C.LENGTH_UNSET.toLong()) updatedDataSpec!!.length else 0
                }
            }
            val errorStream = connection!!.errorStream
            val errorResponseBody = try {
                if (errorStream != null) Util.toByteArray(errorStream) else Util.EMPTY_BYTE_ARRAY
            } catch (e: IOException) {
                Util.EMPTY_BYTE_ARRAY
            }
            connectionMaker.closeConnection()
            val cause: IOException? =
                if (responseCode == 416) DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) else null
            throw InvalidResponseCodeException(
                responseCode, responseMessage, cause, headers, updatedDataSpec!!, errorResponseBody
            )
        }

        // calculate current position
        val bytesToSkip =
            if (responseCode == 200 && updatedDataSpec!!.position != 0L) updatedDataSpec!!.position else 0

        // Determine the length of the data to be read, after skipping.
        val isCompressed = isCompressed(connection!!)
        if (!isCompressed) {
            bytesToRead = if (updatedDataSpec!!.length != C.LENGTH_UNSET.toLong()) {
                updatedDataSpec!!.length
            } else {
                val contentLength = HttpUtil.getContentLength(
                    connection!!.getHeaderField(HttpHeaders.CONTENT_LENGTH),
                    connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE)
                )
                if (contentLength != C.LENGTH_UNSET.toLong()) contentLength - bytesToSkip else C.LENGTH_UNSET.toLong()
            }
        } else {
            // Gzip is enabled. If the server opts to use gzip then the content length in the response
            // will be that of the compressed data, which isn't what we want. Always use the dataSpec
            // length in this case.
            bytesToRead = updatedDataSpec!!.length
        }

        var encryptedStream: InputStream?
        try {
            encryptedStream = connection!!.inputStream
            if (isCompressed) {
                encryptedStream = GZIPInputStream(encryptedStream)
            }
            setupCipherInputStream(encryptedStream!!)
        } catch (e: IOException) {
            connectionMaker.closeConnection()
            throw HttpDataSourceException(
                e,
                updatedDataSpec!!,
                PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
                HttpDataSourceException.TYPE_OPEN
            )
        }
        isOpen = true
        return bytesToRead
    }

    // check if the new position divided by cipher.blockSize
    // results in zero. If not truncate the remaining.
    private fun modifyBytesBlocks(dataSpec: DataSpec): DataSpec {
        val bytesSinceStartOfCurrentBlock = dataSpec.position % cipher.blockSize
        var bytesUntilPreviousBlockStart =
            dataSpec.position - bytesSinceStartOfCurrentBlock - cipher.blockSize
        if (bytesUntilPreviousBlockStart < 0) bytesUntilPreviousBlockStart = 0

        return DataSpec(
            dataSpec.uri,
            dataSpec.httpMethod,
            dataSpec.httpBody,
            bytesUntilPreviousBlockStart,
            bytesUntilPreviousBlockStart,
            dataSpec.length,
            dataSpec.key,
            dataSpec.flags,
            dataSpec.httpRequestHeaders
        )
    }

    private fun setupCipherInputStream(encryptedFileStream: InputStream) {
        cipher.init(Cipher.DECRYPT_MODE, keySpec)
        cipherInputStream = CipherHttpInputStream(
            encryptedFileStream,
            cipher,
            keySpec
        )
    }

    private fun isCompressed(connection: HttpURLConnection): Boolean {
        val contentEncoding = connection.getHeaderField("Content-Encoding")
        return "gzip".equals(contentEncoding, ignoreCase = true)
    }

    @Throws(HttpDataSourceException::class)
    override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
        try {
            var readLength = length
            if (readLength == 0) {
                return 0
            }
            if (bytesToRead != C.LENGTH_UNSET.toLong()) {
                val bytesRemaining: Long = bytesToRead - bytesRead
                if (bytesRemaining == 0L) {
                    return C.RESULT_END_OF_INPUT
                }
                readLength = Math.min(readLength.toLong(), bytesRemaining).toInt()
            }

            val read =
                Util.castNonNull<InputStream>(cipherInputStream).read(buffer, offset, readLength)
            if (read == -1) {
                return C.RESULT_END_OF_INPUT
            }

            bytesRead += read.toLong()
            return read
        } catch (e: IOException) {
            throw HttpDataSourceException.createForIOException(
                e, Util.castNonNull(updatedDataSpec), HttpDataSourceException.TYPE_READ
            )
        }
    }

    override fun addTransferListener(transferListener: TransferListener) {}

    override fun getUri() = uri

    @Throws(HttpDataSourceException::class)
    override fun close() {
        try {
            val inputStream: InputStream? = this.cipherInputStream
            if (inputStream != null) {
                val bytesRemaining =
                    if (bytesToRead == C.LENGTH_UNSET.toLong()) C.LENGTH_UNSET.toLong() else bytesToRead - bytesRead
                maybeTerminateInputStream(connection, bytesRemaining)
                try {
                    inputStream.close()
                } catch (e: IOException) {
                    throw HttpDataSourceException(
                        e,
                        Util.castNonNull(updatedDataSpec),
                        PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
                        HttpDataSourceException.TYPE_CLOSE
                    )
                }
            }
        } finally {
            cipherInputStream = null
            connectionMaker.closeConnection()
            if (isOpen) {
                isOpen = false
            }
        }
    }

    private fun maybeTerminateInputStream(connection: HttpURLConnection?, bytesRemaining: Long) {
        if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
            return
        }
        try {
            val inputStream = connection.inputStream
            if (bytesRemaining == C.LENGTH_UNSET.toLong()) {
                // If the input stream has already ended, do nothing. The socket may be re-used.
                if (inputStream.read() == -1) {
                    return
                }
            } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
                // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
                // re-used.
                return
            }
            val className = inputStream.javaClass.name
            if ("com.android.okhttp.internal.http.HttpTransport\$ChunkedInputStream" == className
                || ("com.android.okhttp.internal.http.HttpTransport\$FixedLengthInputStream"
                        == className)
            ) {
                val superclass: Class<in InputStream>? = inputStream.javaClass.superclass
                val unexpectedEndOfInput =
                    Assertions.checkNotNull(superclass).getDeclaredMethod("unexpectedEndOfInput")
                unexpectedEndOfInput.isAccessible = true
                unexpectedEndOfInput.invoke(inputStream)
            }
        } catch (e: Exception) {
            // If an IOException then the connection didn't ever have an input stream, or it was closed
            // already. If another type of exception then something went wrong, most likely the device
            // isn't using okhttp.
            e.printStackTrace()
        }
    }

    class HttpConnectionMaker {

        private var defaultRequestProperties: HttpDataSource.RequestProperties? = null
        private var requestProperties = HttpDataSource.RequestProperties()
        private var readTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS
        private var connectTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS
        private var allowCrossProtocolRedirects = false
        private var keepPostFor302Redirects = false
        private var connection: HttpURLConnection? = null
        private var userAgent: String? = null

        @Throws(IOException::class)
        fun make(dataSpec: DataSpec): HttpURLConnection {
            var url = URL(dataSpec.uri.toString())
            var httpMethod: @DataSpec.HttpMethod Int = dataSpec.httpMethod
            var httpBody = dataSpec.httpBody
            val position = dataSpec.position
            val length = dataSpec.length
            val allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)
            if (!allowCrossProtocolRedirects && !keepPostFor302Redirects) {
                // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
                // automatically. This is the behavior we want, so use it.
                return make(
                    url,
                    httpMethod,
                    httpBody,
                    position,
                    length,
                    allowGzip,  /* followRedirects= */
                    true,
                    dataSpec.httpRequestHeaders
                )
            }

            // We need to handle redirects ourselves to allow cross-protocol redirects or to keep the POST
            // request method for 302.
            var redirectCount = 0
            while (redirectCount++ <= MAX_REDIRECTS) {
                connection = make(
                    url,
                    httpMethod,
                    httpBody,
                    position,
                    length,
                    allowGzip,  /* followRedirects= */
                    false,
                    dataSpec.httpRequestHeaders
                )
                val responseCode = connection?.responseCode
                val location = connection?.getHeaderField("Location")
                if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
                    && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
                            || responseCode == HttpURLConnection.HTTP_MOVED_PERM
                            || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                            || responseCode == HttpURLConnection.HTTP_SEE_OTHER
                            || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT
                            || responseCode == HTTP_STATUS_PERMANENT_REDIRECT)
                ) {
                    connection?.disconnect()
                    url = handleRedirect(url, location, dataSpec)
                } else if (httpMethod == DataSpec.HTTP_METHOD_POST
                    && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
                            || responseCode == HttpURLConnection.HTTP_MOVED_PERM
                            || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                            || responseCode == HttpURLConnection.HTTP_SEE_OTHER)
                ) {
                    connection?.disconnect()
                    val shouldKeepPost =
                        keepPostFor302Redirects && responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                    if (!shouldKeepPost) {
                        // POST request follows the redirect and is transformed into a GET request.
                        httpMethod = DataSpec.HTTP_METHOD_GET
                        httpBody = null
                    }
                    url = handleRedirect(url, location, dataSpec)
                } else {
                    return connection!!
                }
            }
            throw HttpDataSourceException(
                NoRouteToHostException("Too many redirects: $redirectCount"),
                dataSpec,
                PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
                HttpDataSourceException.TYPE_OPEN
            )
        }

        @Throws(IOException::class)
        private fun make(
            url: URL,
            httpMethod: @DataSpec.HttpMethod Int,
            httpBody: ByteArray?,
            position: Long,
            length: Long,
            allowGzip: Boolean,
            followRedirects: Boolean,
            requestParameters: Map<String, String>,
        ): HttpURLConnection {
            val connection = openConnection(url)
            connection.connectTimeout = connectTimeoutMillis
            connection.readTimeout = readTimeoutMillis
            val requestHeaders: MutableMap<String, String> = HashMap()
            if (defaultRequestProperties != null) {
                requestHeaders.putAll(defaultRequestProperties!!.snapshot)
            }
            requestHeaders.putAll(requestProperties.snapshot)
            requestHeaders.putAll(requestParameters)
            for ((key, value) in requestHeaders) {
                connection.setRequestProperty(key, value)
            }
            //header range
            val rangeHeader = buildRangeRequestHeader(position, length)
            if (rangeHeader != null) {
                connection.setRequestProperty(HttpHeaders.RANGE, rangeHeader)
            }
            if (userAgent != null) {
                connection.setRequestProperty(HttpHeaders.USER_AGENT, userAgent)
            }
            connection.setRequestProperty(
                HttpHeaders.ACCEPT_ENCODING,
                if (allowGzip) "gzip" else "identity"
            )
            connection.instanceFollowRedirects = followRedirects
            connection.doOutput = httpBody != null
            connection.requestMethod = DataSpec.getStringForHttpMethod(httpMethod)
            if (httpBody != null) {
                connection.setFixedLengthStreamingMode(httpBody.size)
                connection.connect()
                val os = connection.outputStream
                os.write(httpBody)
                os.close()
            } else {
                connection.connect()
            }
            return connection
        }

        private fun buildRangeRequestHeader(position: Long, length: Long): String? {
            if (position == 0L && length == C.LENGTH_UNSET.toLong()) {
                return null
            }
            val rangeValue = StringBuilder()
            rangeValue.append("bytes=")
            rangeValue.append(position)
            rangeValue.append("-")

            if (length != C.LENGTH_UNSET.toLong()) {
                rangeValue.append(position + length - 1)
            }
            return rangeValue.toString()
        }

        @VisibleForTesting
        @Throws(IOException::class)
        fun openConnection(url: URL): HttpURLConnection {
            return url.openConnection() as HttpURLConnection
        }

        @Throws(HttpDataSourceException::class)
        private fun handleRedirect(originalUrl: URL, location: String?, dataSpec: DataSpec): URL {
            if (location == null) {
                throw HttpDataSourceException(
                    "Null location redirect",
                    dataSpec,
                    PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
                    HttpDataSourceException.TYPE_OPEN
                )
            }
            // Form the new url.
            val url: URL = try {
                URL(originalUrl, location)
            } catch (e: MalformedURLException) {
                throw HttpDataSourceException(
                    e,
                    dataSpec,
                    PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
                    HttpDataSourceException.TYPE_OPEN
                )
            }

            // Check that the protocol of the new url is supported.
            val protocol = url.protocol
            if ("https" != protocol && "http" != protocol) {
                throw HttpDataSourceException(
                    "Unsupported protocol redirect: $protocol",
                    dataSpec,
                    PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
                    HttpDataSourceException.TYPE_OPEN
                )
            }
            if (!allowCrossProtocolRedirects && protocol != originalUrl.protocol) {
                throw HttpDataSourceException(
                    "Disallowed cross-protocol redirect ("
                            + originalUrl.protocol
                            + " to "
                            + protocol
                            + ")",
                    dataSpec,
                    PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
                    HttpDataSourceException.TYPE_OPEN
                )
            }
            return url
        }

        fun closeConnection() {
            if (connection != null) {
                try {
                    connection?.disconnect()
                } catch (e: Exception) {
                    e.printStackTrace()
                }
                connection = null
            }
        }

        companion object {
            private const val DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000

            /** The default read timeout, in milliseconds.  */
            private const val DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000

            private const val MAX_REDIRECTS = 20 // Same limit as okhttp.

            private const val HTTP_STATUS_TEMPORARY_REDIRECT = 307
            private const val HTTP_STATUS_PERMANENT_REDIRECT = 308
        }
    }

    companion object {
        private const val MAX_BYTES_TO_DRAIN: Long = 2048
    }
}

© www.soinside.com 2019 - 2024. All rights reserved.