Sources/Instrumentation/WKWebView/BodyCache/StreamingMultipartFormData.swift (429 lines of code) (raw):
//
// Copyright 2023 aliyun-sls Authors
//
// 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.
import Foundation
#if os(iOS) || os(watchOS) || os(tvOS)
import MobileCoreServices
#else
import CoreServices
#endif
struct StreamingMultipartFormData : MultipartFormData {
var request: URLRequest
var stringEncoding: String.Encoding = .utf8
var boundary: String?
var bodyStream: MultipartBodyStream?
init(urlRequest: URLRequest, stringEncoding: String.Encoding) {
self.request = urlRequest
self.stringEncoding = stringEncoding
self.boundary = HTTPBodyPart.OTelJSBridgeCreateMultipartFormBoundary()
self.bodyStream = MultipartBodyStream(endoding: stringEncoding)
}
func appendPart(fileURL: URL, name: String, error: Error?) -> Bool {
let fileName = fileURL.lastPathComponent
let mimeType = HTTPBodyPart.OTelJSBridgeContentTypeForPathExtension(ext: fileURL.pathExtension)
return self.appendPart(fileURL: fileURL, name: name, fileName: fileName, mimeType: mimeType, error: error)
}
func appendPart(fileURL: URL, name: String, fileName: String, mimeType: String, error: Error?) -> Bool{
if !fileURL.isFileURL {
// if let error = error {
// error = NSError.init(domain: "", code: URLError.badURL.rawValue, userInfo: ["": ""]) as Error
// error.
// }
return false
} else if let checked = try? fileURL.checkResourceIsReachable(), !checked {
return false
}
guard let fileAttributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path) else {
return false
}
var mutableHeaders = [String: String]()
mutableHeaders["Content-Disposition"] = "form-data; name=\"\(name)\"; filename=\"\(fileName)\""
mutableHeaders["Content-Type"] = mimeType
let bodyPart = HTTPBodyPart()
bodyPart.stringEncoding = self.stringEncoding
bodyPart.headers = mutableHeaders
bodyPart.boundary = self.boundary
bodyPart.body = fileURL
bodyPart.bodyContentLength = fileAttributes[FileAttributeKey.size] as! UInt64
self.bodyStream?.appendHTTPBodyPart(bodyPart: bodyPart)
return true
}
func appendPart(inputStream: InputStream, name: String, fileName: String, length: UInt64, mimeType: String) {
var mutableHeaders = [String: String]()
mutableHeaders["Content-Disposition"] = "form-data; name=\"\(name)\"; filename=\"\(fileName)\""
mutableHeaders["Content-Type"] = mimeType
let bodyPart = HTTPBodyPart()
bodyPart.stringEncoding = self.stringEncoding
bodyPart.headers = mutableHeaders
bodyPart.boundary = self.boundary
bodyPart.body = inputStream
bodyPart.bodyContentLength = length
self.bodyStream?.appendHTTPBodyPart(bodyPart: bodyPart)
}
func appendPart(data: Data, name: String, fileName: String, mimeType: String) {
var mutableHeaders = [String: String]()
mutableHeaders["Content-Disposition"] = "form-data; name=\"\(name)\"; filename=\"\(fileName)\""
mutableHeaders["Content-Type"] = mimeType
self.appendPart(headers: mutableHeaders, body: data)
}
func appendPart(data: Data, name: String) {
var mutableHeaders = [String: String]()
mutableHeaders["Content-Disposition"] = "form-data; name=\"\(name)\""
self.appendPart(headers: mutableHeaders, body: data)
}
func appendPart(headers: [String : String], body: Data) {
let bodyPart = HTTPBodyPart()
bodyPart.stringEncoding = self.stringEncoding
bodyPart.headers = headers
bodyPart.boundary = self.boundary
bodyPart.body = body
bodyPart.bodyContentLength = UInt64(body.count)
self.bodyStream?.appendHTTPBodyPart(bodyPart: bodyPart)
}
func throttleBandwidthWithPacketSize(numberOfBytes: Int, delay: TimeInterval) {
self.bodyStream?.numberOfBytesInPacket = numberOfBytes
self.bodyStream?.delay = delay
}
mutating func requestByFinalizingMultipartFormData() -> URLRequest {
guard let bodyStream = bodyStream, !bodyStream.isEmptry() else {
return self.request
}
bodyStream.setInitialAndFinalBoundaries()
request.httpBodyStream = bodyStream
request.setValue("multipart/form-data; boundary=\(self.boundary ?? "")", forHTTPHeaderField: "Content-Type")
request.setValue("\(bodyStream.contentLength)", forHTTPHeaderField: "Content-Length")
return request
}
}
//extension Stream {
// var streamStatus: Stream.Status {
// return .closed
// }
//
// var streamError: Error? {
// return nil
// }
//}
// MARK: - MultipartBodyStream -
class MultipartBodyStream: InputStream, StreamDelegate {
var numberOfBytesInPacket: Int
var delay: TimeInterval?
var inputStream: InputStream?
var contentLength: UInt64 {
var length: UInt64 = 0
for bodyPart in httpBodyParts {
length += bodyPart.contentLength
}
return length
}
// use isEmpty() func instead
// var empty: Bool = true
private var stringEncoding: String.Encoding
private var httpBodyParts: [HTTPBodyPart]
private var httpBodyPartEnumerator: EnumeratedSequence<[HTTPBodyPart]>.Iterator?
private var currentHTTPBodyPart: HTTPBodyPart?
private var outputStream: OutputStream?
private var buffer: Data?
private var streamStatusCopy: Stream.Status = .notOpen
private var streamErrorCopy: Error?
private weak var streamDelegate: StreamDelegate?
override var streamStatus: Stream.Status {
streamStatusCopy
}
override var streamError: Error? {
streamErrorCopy
}
override var delegate: StreamDelegate? {
get {
streamDelegate
}
set {
streamDelegate = newValue ?? self
}
}
init(endoding: String.Encoding) {
self.stringEncoding = endoding
self.httpBodyParts = [HTTPBodyPart]()
self.numberOfBytesInPacket = Int.max
super.init(data: Data())
// TODO: how to use delegate ??
// super.delegate = self
}
func setInitialAndFinalBoundaries() {
if httpBodyParts.count > 0 {
for bodyPart in httpBodyParts {
bodyPart.hasInitialBoundary = false
bodyPart.hasFinalBoundary = false
}
httpBodyParts.first?.hasInitialBoundary = true
httpBodyParts.last?.hasFinalBoundary = true
}
}
func appendHTTPBodyPart(bodyPart: HTTPBodyPart) {
httpBodyParts.append(bodyPart)
}
func isEmptry() -> Bool {
return self.httpBodyParts.count == 0
}
// MARK: - InputStream
override func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength len: Int) -> Int {
if self.streamStatusCopy == .closed {
return 0
}
var totalNumberOfBytesRead: Int = 0
while Int(totalNumberOfBytesRead) < min(len, self.numberOfBytesInPacket) {
if let currentHTTPBodyPart = currentHTTPBodyPart, currentHTTPBodyPart.hasBytesAvailable() {
let maxLength: Int = min(len, self.numberOfBytesInPacket) - totalNumberOfBytesRead
let numberOfBytesRead = currentHTTPBodyPart.read(buffer: &buffer[totalNumberOfBytesRead],
maxLength: maxLength)
if -1 == numberOfBytesRead {
self.streamErrorCopy = currentHTTPBodyPart.inputStream?.streamError
break
} else {
totalNumberOfBytesRead += numberOfBytesRead
if let delay = delay, delay > 0.0 {
Thread.sleep(forTimeInterval: delay)
}
}
} else {
self.currentHTTPBodyPart = httpBodyPartEnumerator?.next()?.element
guard let _ = self.currentHTTPBodyPart else {
break
}
}
}
return totalNumberOfBytesRead
}
override func getBuffer(_ buffer: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>, length len: UnsafeMutablePointer<Int>) -> Bool {
return false
}
override var hasBytesAvailable: Bool {
return self.streamStatusCopy == Stream.Status.open
}
// MARK: - Stream
override func open() {
if self.streamStatusCopy == .open {
return
}
self.streamStatusCopy = .open
self.setInitialAndFinalBoundaries()
self.httpBodyPartEnumerator = self.httpBodyParts.enumerated().makeIterator()
}
override func close() {
self.streamStatusCopy = .closed
}
override func property(forKey key: Stream.PropertyKey) -> Any? {
return nil
}
override func setProperty(_ property: Any?, forKey key: Stream.PropertyKey) -> Bool {
return false
}
override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
// empty
}
override func remove(from aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
// empty
}
// MARK: Undocumented CFReadStream Bridged Methods
func _scheduleInCFRunLoop(runLoop: RunLoop, forMode mode: String) {
print("call _scheduleInCFRunLoop")
}
func _unscheduleFromCFRunLoop(runLoop: CFRunLoop, forMode aMode:CFString) {
}
func _setCFClientFlags(inFlags: CFOptionFlags, callback: CFReadStreamClientCallBack!,
context: UnsafeMutablePointer<CFStreamClientContext>) -> Bool {
return false
}
}
// MARK: - HTTPBodyPart -
class HTTPBodyPart {
static let MultipartFormCRLF = "\r\n";
@inline(__always) static func OTelJSBridgeCreateMultipartFormBoundary() -> String{
return "Boundary+\(String(format: "%08X", arc4random()))\(String(format: "%08X", arc4random()))"
}
@inline(__always) static func OTelJSBridgeMultipartFormInitialBoundary(boundary: String) -> String {
return "--\(boundary)\(HTTPBodyPart.MultipartFormCRLF)"
}
@inline(__always) static func OTelJSBridgeMultipartFormEncapsulationBoundary(boundary: String) -> String {
return "\(HTTPBodyPart.MultipartFormCRLF)--\(boundary)\(HTTPBodyPart.MultipartFormCRLF)"
}
@inline(__always) static func OTelJSBridgeMultipartFormFinalBoundary(boundary: String) -> String {
return "\(HTTPBodyPart.MultipartFormCRLF)--\(boundary)--\(HTTPBodyPart.MultipartFormCRLF)"
}
@inline(__always) static func OTelJSBridgeContentTypeForPathExtension(ext: String) -> String {
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as NSString, nil) else {
return "application/octet-stream"
}
guard let mimetype = UTTypeCopyPreferredTagWithClass(uti.takeRetainedValue(), kUTTagClassMIMEType) else {
return "application/octet-stream"
}
return mimetype.takeRetainedValue() as String
}
var stringEncoding: String.Encoding = .utf8
var headers: [String: String]?
var boundary : String?
var body: Any?
var bodyContentLength: UInt64 = 0
var _inputStream: InputStream?
var inputStream: InputStream? {
if let _ = _inputStream {
return _inputStream
}
if let b = body as? Data {
_inputStream = InputStream.init(data: b)
} else if let b = body as? URL {
_inputStream = InputStream.init(url: b)
} else if let b = body as? InputStream {
_inputStream = b
} else {
_inputStream = InputStream.init(data: Data())
}
return _inputStream
}
var hasInitialBoundary: Bool = false
var hasFinalBoundary: Bool = false
var bytesAvailable: Bool {
return self.hasBytesAvailable()
}
var contentLength: UInt64 {
var length: UInt64 = 0;
if let boundary = boundary {
let encapsulationBoundaryData = (self.hasInitialBoundary ? HTTPBodyPart.OTelJSBridgeMultipartFormInitialBoundary(boundary: boundary): HTTPBodyPart.OTelJSBridgeMultipartFormEncapsulationBoundary(boundary: boundary)).data(using: stringEncoding)
if let _ = encapsulationBoundaryData {
length += UInt64(encapsulationBoundaryData!.count)
}
}
if let headersData = self.stringForHeaders()?.data(using: stringEncoding) {
length += UInt64(headersData.count);
}
length += bodyContentLength
if self.hasInitialBoundary, let boundary = self.boundary, let closingBoundaryData = HTTPBodyPart.OTelJSBridgeMultipartFormFinalBoundary(boundary: boundary).data(using: self.stringEncoding) {
length += UInt64(closingBoundaryData.count)
}
return length;
}
// extension
var phase: HTTPBodyPartReadPhase?
// _inputStream
var phaseReadOffset: UInt64 = 0
init() {
let _ = self.transitionToNextPhase()
}
deinit {
guard let _ = _inputStream else {
return
}
_inputStream?.close()
_inputStream = nil
}
func stringForHeaders() -> String? {
guard let headers = headers else {
return nil
}
var headerString = String()
for field in headers.keys {
headerString.append("\(field): \(headers[field] ?? "")\(HTTPBodyPart.MultipartFormCRLF)")
}
headerString.append(HTTPBodyPart.MultipartFormCRLF)
return headerString
}
func hasBytesAvailable() -> Bool {
// Allows `read:maxLength:` to be called again if `OTelJSBridgeMultipartFormFinalBoundary` doesn't fit into the available buffer
if phase == .FinalBoundaryPhase {
return true
}
guard let inputStream = self.inputStream else {
return false
}
switch inputStream.streamStatus {
case Stream.Status.notOpen:
fallthrough
case Stream.Status.opening:
fallthrough
case Stream.Status.open:
fallthrough
case Stream.Status.reading:
fallthrough
case Stream.Status.writing:
return true
case Stream.Status.atEnd:
fallthrough
case Stream.Status.closed:
fallthrough
case Stream.Status.error:
fallthrough
default:
return false
}
}
func read(buffer: UnsafeMutablePointer<UInt8>, maxLength: Int) -> Int {
var totalNumberOfBytesRead: Int = 0;
if (phase == .EncapsulationBoundaryPhase) {
if let boundary = boundary {
if let encapsulationBoundaryData = (self.hasInitialBoundary ? HTTPBodyPart.OTelJSBridgeMultipartFormInitialBoundary(boundary: boundary) : HTTPBodyPart.OTelJSBridgeMultipartFormEncapsulationBoundary(boundary: boundary)).data(using: self.stringEncoding) {
totalNumberOfBytesRead += self.readData(data: encapsulationBoundaryData,
intoBuffer: &buffer[totalNumberOfBytesRead],
maxLength: (maxLength - Int(totalNumberOfBytesRead)))
}
}
}
if (phase == .HeaderPhase) {
if let headersData = self.stringForHeaders()?.data(using: self.stringEncoding) {
totalNumberOfBytesRead += self.readData(data: headersData,
intoBuffer: &buffer[totalNumberOfBytesRead],
maxLength: (maxLength - Int(totalNumberOfBytesRead)))
}
}
if (phase == .BodyPhase) {
if let inputStream: InputStream = self.inputStream {
let numberOfBytesRead = inputStream.read(&buffer[totalNumberOfBytesRead],
maxLength: (maxLength - Int(totalNumberOfBytesRead)))
if -1 == numberOfBytesRead {
return -1
} else {
totalNumberOfBytesRead += numberOfBytesRead
if inputStream.streamStatus == .atEnd || inputStream.streamStatus == .closed || inputStream.streamStatus == .error {
let _ = self.transitionToNextPhase()
}
}
}
}
if (phase == .FinalBoundaryPhase) {
if let boundary = boundary {
if let closingBoundaryData = self.hasInitialBoundary ? HTTPBodyPart.OTelJSBridgeMultipartFormFinalBoundary(boundary: boundary).data(using: self.stringEncoding) : Data() {
totalNumberOfBytesRead += self.readData(data: closingBoundaryData,
intoBuffer: &buffer[totalNumberOfBytesRead],
maxLength: (maxLength - Int(totalNumberOfBytesRead)))
}
}
}
return totalNumberOfBytesRead;
}
func readData(data: Data, intoBuffer: UnsafeMutablePointer<UInt8>, maxLength: Int) -> Int {
let range: Range = Range.init(NSMakeRange(Int(self.phaseReadOffset), min(data.count - Int(phaseReadOffset), maxLength)))!
data.copyBytes(to: intoBuffer, from: range)
self.phaseReadOffset += UInt64(range.count);
if Int(phaseReadOffset) >= data.count {
let _ = self.transitionToNextPhase()
}
return range.count;
}
func transitionToNextPhase() -> Bool {
if !Thread.current.isMainThread {
let _ = DispatchQueue.main.sync {
self.transitionToNextPhase()
}
return true
}
switch phase {
case .EncapsulationBoundaryPhase:
phase = .HeaderPhase
case .HeaderPhase:
self.inputStream?.schedule(in: RunLoop.current, forMode: RunLoop.Mode.common)
self.inputStream?.open()
phase = .BodyPhase
case .BodyPhase:
self.inputStream?.close()
phase = .FinalBoundaryPhase
case .FinalBoundaryPhase:
fallthrough
default:
phase = .EncapsulationBoundaryPhase
}
self.phaseReadOffset = 0
return true
}
// #pragma mark - NSCopying
//
// - (instancetype)copyWithZone:(NSZone *)zone {
// OTelJSBridgeHTTPBodyPart *bodyPart = [[[self class] allocWithZone:zone] init];
//
// bodyPart.stringEncoding = self.stringEncoding;
// bodyPart.headers = self.headers;
// bodyPart.bodyContentLength = self.bodyContentLength;
// bodyPart.body = self.body;
// bodyPart.boundary = self.boundary;
//
// return bodyPart;
// }
//
// @end
}
// MARK: - HTTPBodyPartReadPhase
extension HTTPBodyPart {
enum HTTPBodyPartReadPhase : Int {
case EncapsulationBoundaryPhase = 1
case HeaderPhase = 2
case BodyPhase = 3
case FinalBoundaryPhase = 4
}
}
// MARK: - compare
extension HTTPBodyPart : Equatable {
static func == (lhs: HTTPBodyPart, rhs: HTTPBodyPart) -> Bool {
return lhs.stringEncoding == rhs.stringEncoding &&
lhs.headers == rhs.headers &&
lhs.boundary == rhs.boundary &&
lhs.bodyContentLength == rhs.bodyContentLength &&
lhs.inputStream == rhs.inputStream &&
lhs.hasInitialBoundary == rhs.hasInitialBoundary &&
lhs.hasFinalBoundary == rhs.hasFinalBoundary &&
lhs.bytesAvailable == rhs.bytesAvailable &&
lhs.contentLength == rhs.contentLength &&
lhs.phase == rhs.phase &&
lhs.phaseReadOffset == rhs.phaseReadOffset
}
}