Skip to content

Improved presignedURL #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions Sources/S3Signer/S3Signer+PresignedURL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation
import HTTP

/// Private interface
extension S3Signer {

private struct PresignedURLAuthQuery {

var algorithm: String
var credentials: String
var date: String
var expires: String
var signedHeaders: String

enum Keys: String {
case algorithm = "X-Amz-Algorithm"
case credentials = "X-Amz-Credential"
case date = "X-Amz-Date"
case expires = "X-Amz-Expires"
case signedHeaders = "X-Amz-SignedHeaders"
}

func queryItems() -> [URLQueryItem] {
return [
URLQueryItem(name: Keys.algorithm.rawValue, value: algorithm),
URLQueryItem(name: Keys.credentials.rawValue, value: credentials),
URLQueryItem(name: Keys.date.rawValue, value: date),
URLQueryItem(name: Keys.expires.rawValue, value: expires),
URLQueryItem(name: Keys.signedHeaders.rawValue, value: signedHeaders)
]
}

}

func presignedURL(for httpMethod: HTTPMethod, url: URL, expiration: Expiration, region: Region? = nil, headers: [String: String] = [:], dates: Dates) throws -> URL? {
var updatedHeaders = headers

let region = region ?? config.region

updatedHeaders["host"] = url.host ?? region.host

var (canonRequest, urlComponents) = try presignedURLCanonRequest(httpMethod, dates: dates, expiration: expiration, url: url, region: region, headers: updatedHeaders)

let stringToSign = try createStringToSign(canonRequest, dates: dates, region: region)
let signature = try createSignature(stringToSign, timeStampShort: dates.short, region: region)
urlComponents.queryItems?.insert(URLQueryItem(name: "X-Amz-Signature", value: signature), at: 0)
return urlComponents.url
}

private func presignedURLCanonRequest(_ httpMethod: HTTPMethod, dates: Dates, expiration: Expiration, url: URL, region: Region, headers: [String: String]) throws -> (String, URLComponents) {
guard let credScope = credentialScope(dates.short, region: region).encode(type: .queryAllowed),
let signHeaders = signed(headers: headers).encode(type: .queryAllowed) else {
throw Error.invalidEncoding
}

guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
throw Error.badURL(url.absoluteString)
}

var urlQueryItems = urlComponents.queryItems ?? []

let authQuery = PresignedURLAuthQuery(algorithm: "AWS4-HMAC-SHA256",
credentials: config.accessKey,
date: credScope,
expires: "\(expiration.value)",
signedHeaders: signHeaders)

urlQueryItems.insert(contentsOf: authQuery.queryItems(), at: 0)
urlComponents.queryItems = urlQueryItems

return (
[
httpMethod.string,
formattedPath(urlComponents.path),
formattedQueryItems(urlQueryItems),
canonicalHeaders(headers),
signed(headers: headers),
"UNSIGNED-PAYLOAD"
].joined(separator: "\n"),
urlComponents
)
}

}
68 changes: 15 additions & 53 deletions Sources/S3Signer/S3Signer+Private.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ extension S3Signer {
}

func createCanonicalRequest(_ httpMethod: HTTPMethod, url: URL, headers: [String: String], bodyDigest: String) throws -> String {
let query = try self.query(url) ?? ""
return [
httpMethod.description,
path(url),
query,
httpMethod.string,
formattedPath(url.path),
formattedQueryString(url),
canonicalHeaders(headers),
signed(headers: headers),
bodyDigest
Expand Down Expand Up @@ -62,44 +61,22 @@ extension S3Signer {
func getDates(_ date: Date) -> Dates {
return Dates(date)
}

func path(_ url: URL) -> String {
return !url.path.isEmpty ? url.path.encode(type: .pathAllowed) ?? "/" : "/"
}

func presignedURLCanonRequest(_ httpMethod: HTTPMethod, dates: Dates, expiration: Expiration, url: URL, region: Region, headers: [String: String]) throws -> (String, URL) {
guard let credScope = credentialScope(dates.short, region: region).encode(type: .queryAllowed),
let signHeaders = signed(headers: headers).encode(type: .queryAllowed) else {
throw Error.invalidEncoding
}
let fullURL = "\(url.absoluteString)?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=\(config.accessKey)%2F\(credScope)&X-Amz-Date=\(dates.long)&X-Amz-Expires=\(expiration.value)&X-Amz-SignedHeaders=\(signHeaders)"

// This should never throw.
guard let url = URL(string: fullURL) else {
throw Error.badURL(fullURL)
}

let query = try self.query(url) ?? ""
return (
[
httpMethod.description,
path(url),
query,
canonicalHeaders(headers),
signed(headers: headers),
"UNSIGNED-PAYLOAD"
].joined(separator: "\n"),
url
)
func formattedPath(_ path: String) -> String {
return !path.isEmpty ? path.encode(type: .pathAllowed) ?? "/" : "/"
}
func query(_ url: URL) throws -> String? {

func formattedQueryString(_ url: URL) -> String {
if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems {
let items = queryItems.map({ ($0.name.encode(type: .queryAllowed) ?? "", $0.value?.encode(type: .queryAllowed) ?? "") })
let encodedItems = items.map({ "\($0.0)=\($0.1)" })
return encodedItems.sorted().joined(separator: "&")
return formattedQueryItems(queryItems)
}
return nil
return ""
}

func formattedQueryItems(_ queryItems: [URLQueryItem]) -> String {
let items = queryItems.map({ ($0.name.encode(type: .queryAllowed) ?? "", $0.value?.encode(type: .queryAllowed) ?? "") })
let encodedItems = items.map({ "\($0.0)=\($0.1)" })
return encodedItems.sorted().joined(separator: "&")
}

func signed(headers: [String: String]) -> String {
Expand All @@ -122,21 +99,6 @@ extension S3Signer {
return updatedHeaders
}

func presignedURL(for httpMethod: HTTPMethod, url: URL, expiration: Expiration, region: Region? = nil, headers: [String: String] = [:], dates: Dates) throws -> URL? {
var updatedHeaders = headers

let region = region ?? config.region

updatedHeaders["host"] = url.host ?? region.host

let (canonRequest, fullURL) = try presignedURLCanonRequest(httpMethod, dates: dates, expiration: expiration, url: url, region: region, headers: updatedHeaders)

let stringToSign = try createStringToSign(canonRequest, dates: dates, region: region)
let signature = try createSignature(stringToSign, timeStampShort: dates.short, region: region)
let presignedURL = URL(string: fullURL.absoluteString.appending("&X-Amz-Signature=\(signature)"))
return presignedURL
}

func headers(for httpMethod: HTTPMethod, urlString: URLRepresentable, region: Region? = nil, headers: [String: String] = [:], payload: Payload, dates: Dates) throws -> HTTPHeaders {
guard let url = urlString.convertToURL() else {
throw Error.badURL("\(urlString)")
Expand Down