Skip to content

Commit 15d9d69

Browse files
authored
Use HTTPClient for all LCP networking requests (readium#35)
1 parent af5b98e commit 15d9d69

19 files changed

+316
-244
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ All notable changes to this project will be documented in this file. Take a look
1414
* A new `Publication.conforms(to:)` API to identify the profile of a publication.
1515
* Support for the [`conformsTo` RWPM metadata](https://github.com/readium/webpub-manifest/issues/65), to identify the profile of a `Publication`.
1616
* Support for right-to-left PDF documents by extracting the reading progression from the `ViewerPreferences/Direction` metadata.
17+
* HTTP client:
18+
* A new `HTTPClient.download()` API to download HTTP resources to a temporary location.
19+
* `HTTPRequest` and `DefaultHTTPClient` take an optional `userAgent` property to customize the user agent.
1720

1821
#### Navigator
1922

@@ -28,12 +31,22 @@ All notable changes to this project will be documented in this file. Take a look
2831
* `Publication.format` is now deprecated in favor of the new `Publication.conforms(to:)` API which is more accurate.
2932
* For example, replace `publication.format == .epub` with `publication.conforms(to: .epub)` before opening a publication with the `EPUBNavigatorViewController`.
3033

34+
### Changed
35+
36+
#### LCP
37+
38+
* The `LCPService` now uses a provided `HTTPClient` instance for all HTTP requests.
39+
3140
### Fixed
3241

3342
#### Navigator
3443

3544
* [#14](https://github.com/readium/swift-toolkit/issues/14) Backward compatibility (iOS 10+) of JavaScript files is now handled with Babel.
3645

46+
#### LCP
47+
48+
* Fixed the notification of acquisition progress.
49+
3750

3851
## 2.2.0
3952

Sources/LCP/LCPAcquisition.swift

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import R2Shared
1010
/// Represents an on-going LCP acquisition task.
1111
///
1212
/// You can cancel the on-going download with `acquisition.cancel()`.
13-
public final class LCPAcquisition: Loggable {
13+
public final class LCPAcquisition: Loggable, Cancellable {
1414

1515
/// Informations about an acquired publication protected with LCP.
1616
public struct Publication {
@@ -20,10 +20,6 @@ public final class LCPAcquisition: Loggable {
2020

2121
/// Filename that should be used for the publication when importing it in the user library.
2222
public let suggestedFilename: String
23-
24-
/// Download task used to fetch the publication.
25-
@available(*, unavailable, message: "R2Shared.DownloadSession is deprecated")
26-
public let downloadTask: URLSessionDownloadTask?
2723
}
2824

2925
/// Percent-based progress of the acquisition.
@@ -36,25 +32,19 @@ public final class LCPAcquisition: Loggable {
3632

3733
/// Cancels the acquisition.
3834
public func cancel() {
39-
guard !isCancelled else {
40-
return
41-
}
42-
isCancelled = true
43-
downloadTask?.cancel()
35+
cancellable.cancel()
4436
didComplete(with: .cancelled)
4537
}
4638

47-
let progress = MutableObservable<Progress>(.indefinite)
48-
49-
private(set) var isCancelled = false
39+
let onProgress: (Progress) -> Void
40+
var cancellable = MediatorCancellable()
41+
5042
private var isCompleted = false
5143
private let completion: (CancellableResult<Publication, LCPError>) -> Void
5244

53-
var downloadTask: URLSessionDownloadTask?
54-
5545
init(onProgress: @escaping (Progress) -> Void, completion: @escaping (CancellableResult<Publication, LCPError>) -> Void) {
46+
self.onProgress = onProgress
5647
self.completion = completion
57-
self.progress.observe(onProgress)
5848
}
5949

6050
func didComplete(with result: CancellableResult<Publication, LCPError>) -> Void {

Sources/LCP/LCPService.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public final class LCPService: Loggable {
2121
private let licenses: LicensesService
2222
private let passphrases: PassphrasesRepository
2323

24-
public init(client: LCPClient) {
24+
public init(client: LCPClient, httpClient: HTTPClient = DefaultHTTPClient()) {
2525
// Determine whether the embedded liblcp.a is in production mode, by attempting to open a production license.
2626
let isProduction: Bool = {
2727
guard
@@ -35,15 +35,14 @@ public final class LCPService: Loggable {
3535
}()
3636

3737
let db = Database.shared
38-
let network = NetworkService()
3938
passphrases = db.transactions
4039
licenses = LicensesService(
4140
isProduction: isProduction,
4241
client: client,
4342
licenses: db.licenses,
44-
crl: CRLService(network: network),
45-
device: DeviceService(repository: db.licenses, network: network),
46-
network: network,
43+
crl: CRLService(httpClient: httpClient),
44+
device: DeviceService(repository: db.licenses, httpClient: httpClient),
45+
httpClient: httpClient,
4746
passphrases: PassphrasesService(client: client, repository: passphrases)
4847
)
4948
}

Sources/LCP/License/LCPError+wrap.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111

1212
import Foundation
13+
import R2Shared
1314

1415
extension LCPError {
1516

@@ -30,6 +31,10 @@ extension LCPError {
3031
return .parsing(error)
3132
}
3233

34+
if let error = error as? HTTPError {
35+
return .network(error)
36+
}
37+
3338
let nsError = error as NSError
3439
switch nsError.domain {
3540
case "R2LCPClient.LCPClientError":

Sources/LCP/License/License.swift

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ final class License: Loggable {
2424
private let validation: LicenseValidation
2525
private let licenses: LicensesRepository
2626
private let device: DeviceService
27-
private let network: NetworkService
27+
private let httpClient: HTTPClient
2828

29-
init(documents: ValidatedDocuments, client: LCPClient, validation: LicenseValidation, licenses: LicensesRepository, device: DeviceService, network: NetworkService) {
29+
init(documents: ValidatedDocuments, client: LCPClient, validation: LicenseValidation, licenses: LicensesRepository, device: DeviceService, httpClient: HTTPClient) {
3030
self.documents = documents
3131
self.client = client
3232
self.validation = validation
3333
self.licenses = licenses
3434
self.device = device
35-
self.network = network
35+
self.httpClient = httpClient
3636

3737
validation.observe { [weak self] result in
3838
if case .success(let documents) = result {
@@ -212,13 +212,9 @@ extension License: LCPLicense {
212212
.flatMap {
213213
// We fetch the Status Document again after the HTML interaction is done, in case it changed the
214214
// License.
215-
self.network.fetch(statusURL)
216-
.tryMap { status, data in
217-
guard 100..<400 ~= status else {
218-
throw LCPError.network(nil)
219-
}
220-
return data
221-
}
215+
self.httpClient.fetch(statusURL)
216+
.map { $0.body ?? Data() }
217+
.eraseToAnyError()
222218
}
223219
}
224220

@@ -246,19 +242,20 @@ extension License: LCPLicense {
246242

247243
return preferredEndDate()
248244
.tryMap(makeRenewURL(from:))
249-
.flatMap { self.network.fetch($0, method: .put) }
250-
.tryMap { status, data -> Data in
251-
switch status {
252-
case 100..<400:
253-
break
254-
case 400:
255-
throw RenewError.renewFailed
256-
case 403:
257-
throw RenewError.invalidRenewalPeriod(maxRenewDate: self.maxRenewDate)
258-
default:
259-
throw RenewError.unexpectedServerError
260-
}
261-
return data
245+
.flatMap {
246+
self.httpClient.fetch(HTTPRequest(url: $0, method: .put))
247+
.map { $0.body ?? Data() }
248+
.mapError { error -> RenewError in
249+
switch error.kind {
250+
case .badRequest:
251+
return .renewFailed
252+
case .forbidden:
253+
return .invalidRenewalPeriod(maxRenewDate: self.maxRenewDate)
254+
default:
255+
return .unexpectedServerError
256+
}
257+
}
258+
.eraseToAnyError()
262259
}
263260
}
264261

@@ -285,20 +282,18 @@ extension License: LCPLicense {
285282
return
286283
}
287284

288-
network.fetch(url, method: .put)
289-
.tryMap { status, data in
290-
switch status {
291-
case 100..<400:
292-
break
293-
case 400:
294-
throw ReturnError.returnFailed
295-
case 403:
296-
throw ReturnError.alreadyReturnedOrExpired
285+
httpClient.fetch(HTTPRequest(url: url, method: .put))
286+
.mapError { error -> ReturnError in
287+
switch error.kind {
288+
case .badRequest:
289+
return .returnFailed
290+
case .forbidden:
291+
return .alreadyReturnedOrExpired
297292
default:
298-
throw ReturnError.unexpectedServerError
293+
return .unexpectedServerError
299294
}
300-
return data
301295
}
296+
.map { $0.body ?? Data() }
302297
.flatMap(validateStatusDocument)
303298
.mapError(LCPError.wrap)
304299
.resolveWithError(completion)

Sources/LCP/License/LicenseValidation.swift

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ final class LicenseValidation: Loggable {
6161
fileprivate let sender: Any?
6262
fileprivate let crl: CRLService
6363
fileprivate let device: DeviceService
64-
fileprivate let network: NetworkService
64+
fileprivate let httpClient: HTTPClient
6565
fileprivate let passphrases: PassphrasesService
6666

6767
// List of observers notified when the Documents are validated, or if an error occurred.
@@ -85,7 +85,7 @@ final class LicenseValidation: Loggable {
8585
client: LCPClient,
8686
crl: CRLService,
8787
device: DeviceService,
88-
network: NetworkService,
88+
httpClient: HTTPClient,
8989
passphrases: PassphrasesService,
9090
onLicenseValidated: @escaping (LicenseDocument) throws -> Void
9191
) {
@@ -96,7 +96,7 @@ final class LicenseValidation: Loggable {
9696
self.client = client
9797
self.crl = crl
9898
self.device = device
99-
self.network = network
99+
self.httpClient = httpClient
100100
self.passphrases = passphrases
101101
self.onLicenseValidated = onLicenseValidated
102102
}
@@ -304,14 +304,9 @@ extension LicenseValidation {
304304
private func fetchStatus(of license: LicenseDocument) throws {
305305
let url = try license.url(for: .status, preferredType: .lcpStatusDocument)
306306
// Short timeout to avoid blocking the License, since the LSD is optional.
307-
network.fetch(url, timeout: 5)
308-
.tryMap { status, data -> Event in
309-
guard 100..<400 ~= status else {
310-
throw LCPError.network(nil)
311-
}
312-
313-
return .retrievedStatusData(data)
314-
}
307+
httpClient.fetch(HTTPRequest(url: url, timeoutInterval: 5))
308+
.map { .retrievedStatusData($0.body ?? Data()) }
309+
.eraseToAnyError()
315310
.resolve(raise)
316311
}
317312

@@ -323,13 +318,9 @@ extension LicenseValidation {
323318
private func fetchLicense(from status: StatusDocument) throws {
324319
let url = try status.url(for: .license, preferredType: .lcpLicenseDocument)
325320
// Short timeout to avoid blocking the License, since it can be updated next time.
326-
network.fetch(url, timeout: 5)
327-
.tryMap { status, data -> Event in
328-
guard 100..<400 ~= status else {
329-
throw LCPError.network(nil)
330-
}
331-
return .retrievedLicenseData(data)
332-
}
321+
httpClient.fetch(HTTPRequest(url: url, timeoutInterval: 5))
322+
.map { .retrievedLicenseData($0.body ?? Data()) }
323+
.eraseToAnyError()
333324
.resolve(raise)
334325
}
335326

Sources/LCP/Services/CRLService.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ final class CRLService {
2121
private static let crlKey = "org.readium.r2-lcp-swift.CRL"
2222
private static let dateKey = "org.readium.r2-lcp-swift.CRLDate"
2323

24-
private let network: NetworkService
24+
private let httpClient: HTTPClient
2525

26-
init(network: NetworkService) {
27-
self.network = network
26+
init(httpClient: HTTPClient) {
27+
self.httpClient = httpClient
2828
}
2929

3030
/// Retrieves the CRL either from the cache, or from EDRLab if the cache is outdated.
@@ -52,12 +52,13 @@ final class CRLService {
5252
private func fetch(timeout: TimeInterval? = nil) -> Deferred<String, Error> {
5353
let url = URL(string: "http://crl.edrlab.telesec.de/rl/EDRLab_CA.crl")!
5454

55-
return network.fetch(url, timeout: timeout)
56-
.tryMap { status, data in
57-
guard 100..<400 ~= status else {
55+
return httpClient.fetch(HTTPRequest(url: url, timeoutInterval: timeout))
56+
.mapError { _ in LCPError.crlFetching }
57+
.tryMap {
58+
guard let body = $0.body?.base64EncodedString() else {
5859
throw LCPError.crlFetching
5960
}
60-
return "-----BEGIN X509 CRL-----\(data.base64EncodedString())-----END X509 CRL-----";
61+
return "-----BEGIN X509 CRL-----\(body)-----END X509 CRL-----";
6162
}
6263

6364
}

Sources/LCP/Services/DeviceService.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ import R2Shared
1616
final class DeviceService {
1717

1818
private let repository: DeviceRepository
19-
private let network: NetworkService
19+
private let httpClient: HTTPClient
2020

21-
init(repository: DeviceRepository, network: NetworkService) {
21+
init(repository: DeviceRepository, httpClient: HTTPClient) {
2222
self.repository = repository
23-
self.network = network
23+
self.httpClient = httpClient
2424
}
2525

2626
/// Returns the device ID, creates it if needed.
@@ -60,14 +60,14 @@ final class DeviceService {
6060
throw LCPError.licenseInteractionNotAvailable
6161
}
6262

63-
return self.network.fetch(url, method: .post)
64-
.tryMap { status, data in
65-
guard 100..<400 ~= status else {
63+
return self.httpClient.fetch(HTTPRequest(url: url, method: .post))
64+
.tryMap { response in
65+
guard 100..<400 ~= response.statusCode else {
6666
return nil
6767
}
6868

6969
try self.repository.registerDevice(for: license)
70-
return data
70+
return response.body
7171
}
7272
}
7373
}

0 commit comments

Comments
 (0)