in Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift [66:140]
func get(_ source: Model.CollectionSource, callback: @escaping (Result<Model.Collection, Error>) -> Void) {
guard case .json = source.type else {
return callback(.failure(InternalError("JSONPackageCollectionProvider can only be used for fetching 'json' package collections")))
}
if let errors = source.validate()?.errors() {
return callback(.failure(JSONPackageCollectionProviderError.invalidSource("\(errors)")))
}
// Source is a local file
if let absolutePath = source.absolutePath {
do {
let fileContents = try localFileSystem.readFileContents(absolutePath)
return fileContents.withData { data in
self.decodeAndRunSignatureCheck(source: source, data: data, certPolicyKeys: Self.defaultCertPolicyKeys, callback: callback)
}
} catch {
return callback(.failure(error))
}
}
// first do a head request to check content size compared to the maximumSizeInBytes constraint
let headOptions = self.makeRequestOptions(validResponseCodes: [200])
let headers = self.makeRequestHeaders()
self.httpClient.head(source.url, headers: headers, options: headOptions) { result in
switch result {
case .failure(HTTPClientError.badResponseStatusCode(let statusCode)):
if statusCode == 404 {
return callback(.failure(JSONPackageCollectionProviderError.collectionNotFound(source.url)))
} else {
return callback(.failure(JSONPackageCollectionProviderError.collectionUnavailable(source.url, statusCode)))
}
case .failure(let error):
return callback(.failure(error))
case .success(let response):
guard let contentLength = response.headers.get("Content-Length").first.flatMap(Int64.init) else {
return callback(.failure(JSONPackageCollectionProviderError.invalidResponse(source.url, "Missing Content-Length header")))
}
guard contentLength <= self.configuration.maximumSizeInBytes else {
return callback(.failure(JSONPackageCollectionProviderError.responseTooLarge(source.url, contentLength)))
}
// next do a get request to get the actual content
var getOptions = self.makeRequestOptions(validResponseCodes: [200])
getOptions.maximumResponseSizeInBytes = self.configuration.maximumSizeInBytes
self.httpClient.get(source.url, headers: headers, options: getOptions) { result in
switch result {
case .failure(HTTPClientError.badResponseStatusCode(let statusCode)):
if statusCode == 404 {
return callback(.failure(JSONPackageCollectionProviderError.collectionNotFound(source.url)))
} else {
return callback(.failure(JSONPackageCollectionProviderError.collectionUnavailable(source.url, statusCode)))
}
case .failure(let error):
return callback(.failure(error))
case .success(let response):
// check content length again so we can record this as a bad actor
// if not returning head and exceeding size
// TODO: store bad actors to prevent server DoS
guard let contentLength = response.headers.get("Content-Length").first.flatMap(Int64.init) else {
return callback(.failure(JSONPackageCollectionProviderError.invalidResponse(source.url, "Missing Content-Length header")))
}
guard contentLength < self.configuration.maximumSizeInBytes else {
return callback(.failure(JSONPackageCollectionProviderError.responseTooLarge(source.url, contentLength)))
}
guard let body = response.body else {
return callback(.failure(JSONPackageCollectionProviderError.invalidResponse(source.url, "Body is empty")))
}
let certPolicyKeys = self.sourceCertPolicy.certificatePolicyKeys(for: source) ?? Self.defaultCertPolicyKeys
self.decodeAndRunSignatureCheck(source: source, data: body, certPolicyKeys: certPolicyKeys, callback: callback)
}
}
}
}
}