Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

Commit

Permalink
Merge pull request #62 from readium/fixes/lcp
Browse files Browse the repository at this point in the history
Various LCP fixes
  • Loading branch information
aferditamuriqi authored Nov 18, 2019
2 parents 697c818 + e9124bf commit 352a568
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ before_install:
- carthage update --verbose --no-use-binaries --platform iOS --cache-builds

script:
- xcodebuild clean build -project r2-lcp-swift.xcodeproj -scheme readium-lcp-swift -destination "platform=iOS Simulator,name=iPhone 8,OS=11.3" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -quiet
- xcodebuild clean build -project r2-lcp-swift.xcodeproj -scheme readium-lcp-swift -quiet
6 changes: 3 additions & 3 deletions Cartfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github "readium/r2-shared-swift" == 1.4.0
github "readium/r2-shared-swift" == 1.4.1
github "stephencelis/SQLite.swift" == 0.12.2
github "krzyzanowskim/CryptoSwift" == 0.15.0
github "weichsel/ZIPFoundation" == 0.9.8
github "krzyzanowskim/CryptoSwift" == 1.1.3
github "weichsel/ZIPFoundation" == 0.9.9
6 changes: 3 additions & 3 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github "krzyzanowskim/CryptoSwift" "0.15.0"
github "readium/r2-shared-swift" "1.4.0"
github "krzyzanowskim/CryptoSwift" "1.1.3"
github "readium/r2-shared-swift" "1.4.1"
github "stephencelis/SQLite.swift" "0.12.2"
github "weichsel/ZIPFoundation" "0.9.8"
github "weichsel/ZIPFoundation" "0.9.9"
4 changes: 2 additions & 2 deletions ReadiumLCP.podspec
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Pod::Spec.new do |s|

s.name = "ReadiumLCP"
s.version = "1.2.0"
s.version = "1.2.1"
s.license = "BSD 3-Clause License"
s.summary = "Readium LCP"
s.homepage = "http://readium.github.io"
s.author = { "Aferdita Muriqi" => "aferdita.muriqi@gmail.com" }
s.source = { :git => "https://github.com/readium/r2-lcp-swift.git", :branch => "develop" }
s.source = { :git => "https://github.com/readium/r2-lcp-swift.git", :tag => "1.2.1" }
s.exclude_files = ["**/Info*.plist"]
s.requires_arc = true
s.resources = ['readium-lcp-swift/Resources/**']
Expand Down
2 changes: 2 additions & 0 deletions r2-lcp-swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@
PRODUCT_NAME = ReadiumLCP;
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
};
name = Debug;
Expand Down Expand Up @@ -600,6 +601,7 @@
PRODUCT_NAME = ReadiumLCP;
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
};
name = Release;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class LCPLLicenseContainer: LicenseContainer {

func read() throws -> Data {
guard let data = try? Data(contentsOf: lcpl) else {
throw LCPError.licenseContainer
throw LCPError.licenseContainer(.readFailed(path: "."))
}
return data
}
Expand All @@ -31,7 +31,7 @@ final class LCPLLicenseContainer: LicenseContainer {
do {
try license.data.write(to: lcpl, options: .atomic)
} catch {
throw LCPError.licenseContainer
throw LCPError.licenseContainer(.writeFailed(path: "."))
}
}

Expand Down
9 changes: 4 additions & 5 deletions readium-lcp-swift/License/Container/LicenseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ protocol LicenseContainer {

}

func makeLicenseContainer(for publication: URL, mimetype: String? = nil) throws -> LicenseContainer {
switch Publication.Format(file: publication, mimetype: mimetype) {
func makeLicenseContainer(for publication: URL, mimetypes: [String] = []) throws -> LicenseContainer {
switch Publication.Format(file: publication, mimetypes: mimetypes) {
case .pdf:
return LCPDFLicenseContainer(lcpdf: publication)
case .epub:
return EPUBLicenseContainer(epub: publication)
default:
throw LCPError.licenseContainer
// If we can't determine the format, we assume that the publication is an EPUB as this is the most common use case.
return EPUBLicenseContainer(epub: publication)
}
}
10 changes: 5 additions & 5 deletions readium-lcp-swift/License/Container/ZIPLicenseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ class ZIPLicenseContainer: LicenseContainer {

func read() throws -> Data {
guard let archive = Archive(url: zip, accessMode: .read) else {
throw LCPError.licenseContainer
throw LCPError.licenseContainer(.openFailed)
}
guard let entry = archive[pathInZIP] else {
throw LCPError.licenseContainer
throw LCPError.licenseContainer(.fileNotFound(pathInZIP))
}

var data = Data()
Expand All @@ -38,15 +38,15 @@ class ZIPLicenseContainer: LicenseContainer {
data.append(part)
}
} catch {
throw LCPError.licenseContainer
throw LCPError.licenseContainer(.readFailed(path: pathInZIP))
}

return data
}

func write(_ license: LicenseDocument) throws {
guard let archive = Archive(url: zip, accessMode: .update) else {
throw LCPError.licenseContainer
throw LCPError.licenseContainer(.openFailed)
}

do {
Expand All @@ -61,7 +61,7 @@ class ZIPLicenseContainer: LicenseContainer {
return data[position..<size]
})
} catch {
throw LCPError.licenseContainer
throw LCPError.licenseContainer(.writeFailed(path: pathInZIP))
}
}

Expand Down
28 changes: 19 additions & 9 deletions readium-lcp-swift/License/License.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ extension License: LCPLicense {
return (charactersToCopyLeft ?? 1) > 0
}

func copy(_ text: String) -> String? {
func copy(_ text: String, consumes: Bool) -> String? {
guard var charactersLeft = charactersToCopyLeft else {
return text
}
Expand All @@ -93,11 +93,13 @@ extension License: LCPLicense {
text = String(text[..<endIndex])
}

do {
charactersLeft = max(0, charactersLeft - text.count)
try licenses.setCopiesLeft(charactersLeft, for: license.id)
} catch {
log(.error, error)
if consumes {
do {
charactersLeft = max(0, charactersLeft - text.count)
try licenses.setCopiesLeft(charactersLeft, for: license.id)
} catch {
log(.error, error)
}
}

return text
Expand Down Expand Up @@ -243,18 +245,26 @@ extension License {
func fetchPublication(completion: @escaping ((URL, URLSessionDownloadTask?)?, Error?) -> Void) -> Observable<DownloadProgress> {
do {
let license = self.documents.license
let title = license.link(for: .publication)?.title
let link = license.link(for: .publication)
let url = try license.url(for: .publication)

return self.network.download(url, title: title) { result, error in
return self.network.download(url, title: link?.title) { result, error in
guard let (downloadedFile, task) = result else {
completion(nil, error)
return
}

do {
var mimetypes: [String] = []
if let responseMimetype = task?.response?.mimeType {
mimetypes.append(responseMimetype)
}
if let linkType = link?.type {
mimetypes.append(linkType)
}

// Saves the License Document into the downloaded publication
let container = try makeLicenseContainer(for: downloadedFile, mimetype: task?.response?.mimeType)
let container = try makeLicenseContainer(for: downloadedFile, mimetypes: mimetypes)
try container.write(license)
completion((downloadedFile, task), nil)

Expand Down
29 changes: 17 additions & 12 deletions readium-lcp-swift/License/LicenseValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ extension LicenseValidation {
case fetchStatus(LicenseDocument)
case validateStatus(LicenseDocument, Data)
case fetchLicense(LicenseDocument, StatusDocument)
case checkLicenseStatus(LicenseDocument, StatusDocument?)
case checkLicenseStatus(LicenseDocument, StatusDocument?, statusDocumentTakesPrecedence: Bool)
case requestPassphrase(LicenseDocument, StatusDocument?)
case validateIntegrity(LicenseDocument, StatusDocument?, passphrase: String)
case registerDevice(ValidatedDocuments, Link)
Expand All @@ -156,7 +156,7 @@ extension LicenseValidation {
case let (.validateLicense(_, status), .validatedLicense(license)):
// Skips the status fetch if we already have one, to avoid any infinite loop
if let status = status {
self = .checkLicenseStatus(license, status)
self = .checkLicenseStatus(license, status, statusDocumentTakesPrecedence: false)
} else {
self = .fetchStatus(license)
}
Expand All @@ -168,29 +168,30 @@ extension LicenseValidation {
self = .validateStatus(license, data)
case let (.fetchStatus(license), .failed(_)):
// We ignore any error while fetching the Status Document, as it is optional
self = .checkLicenseStatus(license, nil)
self = .checkLicenseStatus(license, nil, statusDocumentTakesPrecedence: false)

// 2.2. Validate the structure of the status document
case let (.validateStatus(license, _), .validatedStatus(status)):
// Fetches the License Document if it was updated
if license.updated < status.licenseUpdated {
self = .fetchLicense(license, status)
} else {
self = .checkLicenseStatus(license, status)
self = .checkLicenseStatus(license, status, statusDocumentTakesPrecedence: false)
}
case let (.validateStatus(license, _), .failed(_)):
// We ignore any error while validating the Status Document, as it is optional
self = .checkLicenseStatus(license, nil)
self = .checkLicenseStatus(license, nil, statusDocumentTakesPrecedence: false)

// 3. Get an updated license if needed
case let (.fetchLicense(_, status), .retrievedLicenseData(data)):
self = .validateLicense(data, status)
case let (.fetchLicense(license, status), .failed(_)):
// We ignore any error while fetching the updated License Document
self = .checkLicenseStatus(license, status)
// Note: since we failed to get the updated License, then the Status Document will take precedence over the License when checking the status.
self = .checkLicenseStatus(license, status, statusDocumentTakesPrecedence: true)

// 4. Check the dates and license status
case let (.checkLicenseStatus(license, status), .checkedLicenseStatus(error)):
case let (.checkLicenseStatus(license, status, _), .checkedLicenseStatus(error)):
if let error = error {
self = .valid(ValidatedDocuments(license, .right(error), status))
} else {
Expand Down Expand Up @@ -325,14 +326,18 @@ extension LicenseValidation {
.resolve(raise)
}

private func checkLicenseStatus(of license: LicenseDocument, status: StatusDocument?) throws {
private func checkLicenseStatus(of license: LicenseDocument, status: StatusDocument?, statusDocumentTakesPrecedence: Bool) throws {
var error: StatusError? = nil

let now = Date()
let start = license.rights.start ?? now
let end = license.rights.end ?? now
// We only check the Status Document's status if the License rights are not valid, to get a proper status error message.
if start > now || now > end {
let isLicenseExpired = (start > now || now > end)

let isStatusValid = [.ready, .active].contains(status?.status ?? .ready)

// We only check the Status Document's status if the License itself is expired, to get a proper status error message. But in the case where the Status Document takes precedence (eg. after a failed License update), then we also check the status validity.
if isLicenseExpired || (statusDocumentTakesPrecedence && !isStatusValid) {
if let status = status {
let date = status.updated
switch status.status {
Expand Down Expand Up @@ -404,8 +409,8 @@ extension LicenseValidation {
try validateStatus(data: data)
case let .fetchLicense(_, status):
try fetchLicense(from: status)
case let .checkLicenseStatus(license, status):
try checkLicenseStatus(of: license, status: status)
case let .checkLicenseStatus(license, status, statusDocumentTakesPrecedence):
try checkLicenseStatus(of: license, status: status, statusDocumentTakesPrecedence: statusDocumentTakesPrecedence)
case let .requestPassphrase(license, _):
requestPassphrase(for: license)
case let .validateIntegrity(license, _, passphrase):
Expand Down
17 changes: 15 additions & 2 deletions readium-lcp-swift/Public/LCPError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public enum LCPError: LocalizedError {
// The status of the License is not valid, it can't be used to decrypt the publication.
case licenseStatus(StatusError)
// Can't read or write the License Document from its container.
case licenseContainer
case licenseContainer(ContainerError)
// The interaction is not available with this License.
case licenseInteractionNotAvailable
// This License's profile is not supported by liblcp.
Expand Down Expand Up @@ -74,7 +74,7 @@ public enum LCPError: LocalizedError {
return R2LCPLocalizedString("LCPError.licenseIntegrity", description)
case .licenseStatus(let error):
return error.localizedDescription
case .licenseContainer:
case .licenseContainer(_):
return R2LCPLocalizedString("LCPError.licenseContainer")
case .licenseInteractionNotAvailable:
return R2LCPLocalizedString("LCPError.licenseInteractionNotAvailable")
Expand Down Expand Up @@ -198,3 +198,16 @@ public enum ParsingError: Error {
// Invalid URL for link with rel %@.
case url(rel: String)
}


/// Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.)
public enum ContainerError: Error {
// Can't access the container, it's format is wrong.
case openFailed
// The file at given relative path is not found in the Container.
case fileNotFound(String)
// Can't read the file at given relative path in the Container.
case readFailed(path: String)
// Can't write the file at given relative path in the Container.
case writeFailed(path: String)
}
2 changes: 1 addition & 1 deletion readium-lcp-swift/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"ReadiumLCP.StatusError.returned" = "This license has been returned on %@.";
"ReadiumLCP.StatusError.expired.start" = "This license starts on %@.";
"ReadiumLCP.StatusError.expired.end" = "This license expired on %@.";
"ReadiumLCP.StatusError.revoked" = "This license has been revoked by its provider on %1$@.\nThe license was registered by %1$d device(s)";
"ReadiumLCP.StatusError.revoked" = "This license has been revoked by its provider on %1$@.\nThe license was registered by %2$d device(s)";

/* RenewError: Errors while renewing a loan. */
"ReadiumLCP.RenewError.renewFailed" = "Your publication could not be renewed properly.";
Expand Down
32 changes: 30 additions & 2 deletions readium-lcp-swift/Services/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,24 @@
//

import Foundation
import UIKit
import R2Shared


final class NetworkService: Loggable {

enum Method: String {
case get = "GET"
case post = "POST"
case put = "PUT"
}

func fetch(_ url: URL, method: Method = .get, timeout: TimeInterval? = nil) -> Deferred<(status: Int, data: Data)> {
return Deferred { success, failure in
self.log(.info, "\(method.rawValue) \(url)")

var request = URLRequest(url: url)
request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
request.httpMethod = method.rawValue
if let timeout = timeout {
request.timeoutInterval = timeout
Expand Down Expand Up @@ -59,4 +61,30 @@ final class NetworkService: Loggable {
}
}

/// Builds a more meaningful User-Agent for the LCP network requests.
/// See. https://github.com/readium/r2-testapp-swift/issues/291
private lazy var userAgent: String = {
var sysinfo = utsname()
uname(&sysinfo)

let darwinVersion = String(bytes: Data(bytes: &sysinfo.release, count: Int(_SYS_NAMELEN)), encoding: .ascii)?
.trimmingCharacters(in: .controlCharacters)
?? "0"

let deviceName = String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)?
.trimmingCharacters(in: .controlCharacters)
?? "0"

let cfNetworkVersion = Bundle(identifier: "com.apple.CFNetwork")?
.infoDictionary?["CFBundleShortVersionString"] as? String
?? "0"

let appInfo = Bundle.main.infoDictionary
let appName = appInfo?["CFBundleName"] as? String ?? "Unknown App"
let appVersion = appInfo?["CFBundleShortVersionString"] as? String ?? "0"
let device = UIDevice.current

return "\(appName)/\(appVersion) \(deviceName) \(device.systemName)/\(device.systemVersion) CFNetwork/\(cfNetworkVersion) Darwin/\(darwinVersion)"
}()

}

0 comments on commit 352a568

Please sign in to comment.