diff --git a/.swift-version b/.swift-version index 5186d07..7d5c902 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.0 +4.1 diff --git a/LICENSE b/LICENSE index fcb439c..61638f8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 JustinM1 +Copyright (c) 2018 LiveUI Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.resolved b/Package.resolved index 5d16f6d..80cc269 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/vapor/core.git", "state": { "branch": null, - "revision": "ce64e70a48adf54835d040ba4c4dab431e2cd020", - "version": "3.1.1" + "revision": "c61a29ef08e7c8b3e836954269678c5a8fa31f3f", + "version": "3.1.2" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/vapor/engine.git", "state": { "branch": null, - "revision": "2419e37d689b78c9197b2f38cd8f2901cd7dcf9e", - "version": "3.0.0-rc.2.3.1" + "revision": "bcc330e964a0a294afb9c829b6e6b4626cdea427", + "version": "3.0.0-rc.2.4" } }, { @@ -118,6 +118,15 @@ "version": "1.0.0" } }, + { + "package": "URLEncodedForm", + "repositoryURL": "https://github.com/vapor/url-encoded-form.git", + "state": { + "branch": null, + "revision": "aca8efc7176f3ea2352dcbcd32526f82af5647c3", + "version": "1.0.1" + } + }, { "package": "Validation", "repositoryURL": "https://github.com/vapor/validation.git", @@ -132,8 +141,8 @@ "repositoryURL": "https://github.com/vapor/vapor.git", "state": { "branch": null, - "revision": "26b5c4032f236cc78e6fd3a51ac6d8aceb5a3a4f", - "version": "3.0.0-rc.2.4.1" + "revision": "752e6a6c392f18e4986e2251c5db76fd7f79d4e9", + "version": "3.0.0-rc.2.5" } }, { @@ -141,7 +150,7 @@ "repositoryURL": "https://github.com/LiveUI/VaporTestTools.git", "state": { "branch": "master", - "revision": "0791548a30e6a805bf9291b0ad6707c4085cc3ee", + "revision": "2b8aa260f6f7faf001d52cae1189d33274886498", "version": null } } diff --git a/Sources/S3/Dates.swift b/Sources/S3/Dates.swift deleted file mode 100644 index 6550f03..0000000 --- a/Sources/S3/Dates.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - - -class Bbbbb { - -} diff --git a/Sources/S3/Extensions/Container+S3.swift b/Sources/S3/Extensions/Container+S3.swift new file mode 100644 index 0000000..bdfd0d2 --- /dev/null +++ b/Sources/S3/Extensions/Container+S3.swift @@ -0,0 +1,18 @@ +// +// Container+S3.swift +// S3 +// +// Created by Ondrej Rafaj on 19/04/2018. +// + +import Foundation +import Vapor + + +extension Container { + + public func makeS3Client() throws -> S3Client { + return try make() + } + +} diff --git a/Sources/S3/Extensions/Region+Tools.swift b/Sources/S3/Extensions/Region+Tools.swift new file mode 100644 index 0000000..2c3efb8 --- /dev/null +++ b/Sources/S3/Extensions/Region+Tools.swift @@ -0,0 +1,22 @@ +// +// Region+Tools.swift +// S3Signer +// +// Created by Ondrej Rafaj on 19/04/2018. +// + +import Foundation +@_exported import S3Signer + + +extension Region { + + public func urlString(bucket: String) -> String { + return host + bucket + } + + public func url(bucket: String) -> URL? { + return URL(string: urlString(bucket: bucket)) + } + +} diff --git a/Sources/S3/Extensions/S3+Private.swift b/Sources/S3/Extensions/S3+Private.swift new file mode 100644 index 0000000..32bfcda --- /dev/null +++ b/Sources/S3/Extensions/S3+Private.swift @@ -0,0 +1,27 @@ +// +// S3+Private.swift +// S3 +// +// Created by Ondrej Rafaj on 19/04/2018. +// + +import Foundation +import Vapor +import HTTP + + +extension S3 { + + func make(request url: URL, method: HTTPMethod, headers: HTTPHeaders, data: Data? = nil, on req: Request) throws -> Future { + let client = try req.make(Client.self) + let request = Request(using: req.privateContainer) + request.http.method = method + request.http.headers = headers + if let data = data { + request.http.body = HTTPBody(data: data) + } + request.http.url = url + return try client.respond(to: request) + } + +} diff --git a/Sources/S3/Extensions/Service+S3.swift b/Sources/S3/Extensions/Service+S3.swift new file mode 100644 index 0000000..f580d56 --- /dev/null +++ b/Sources/S3/Extensions/Service+S3.swift @@ -0,0 +1,20 @@ +// +// Service+S3.swift +// S3Signer +// +// Created by Ondrej Rafaj on 19/04/2018. +// + +import Foundation +import Service +@_exported import S3Signer + + +extension Services { + + /// Convenience method to register both S3Signer and S3Client + public mutating func register(s3 config: S3Signer.Config, defaultBucket: String) throws { + try S3.init(defaultBucket: defaultBucket, config: config, services: &self) + } + +} diff --git a/Sources/S3/Protocols/FileInfo.swift b/Sources/S3/Protocols/FileInfo.swift new file mode 100644 index 0000000..1ed596c --- /dev/null +++ b/Sources/S3/Protocols/FileInfo.swift @@ -0,0 +1,17 @@ +// +// FileInfo.swift +// S3 +// +// Created by Ondrej Rafaj on 19/04/2018. +// + +import Foundation + + +public protocol FileInfo { + /// Override target bucket + var bucket: String? { get } + + /// S3 file path + var path: String { get } +} diff --git a/Sources/S3/Protocols/S3Client.swift b/Sources/S3/Protocols/S3Client.swift new file mode 100644 index 0000000..f5123aa --- /dev/null +++ b/Sources/S3/Protocols/S3Client.swift @@ -0,0 +1,19 @@ +// +// S3Signer.swift +// S3 +// +// Created by Ondrej Rafaj on 18/04/2018. +// + +import Foundation +import Vapor + + +public protocol S3Client: Service { + func put(file: S3.File.Upload, headers: [String: String], on req: Request) throws -> EventLoopFuture + func put(file url: URL, destination: String, bucket: String?, access: S3.AccessControlList, on req: Request) throws -> Future + func put(file path: String, destination: String, bucket: String?, access: S3.AccessControlList, on req: Request) throws -> Future + func put(string: String, mime: MediaType, destination: String, bucket: String?, access: S3.AccessControlList, on req: Request) throws -> Future + func get(file: S3.File.Location, headers: [String: String], on req: Request) throws -> Future + func delete(file: S3.File.Location, headers: [String: String], on req: Request) throws -> Future +} diff --git a/Sources/S3/S3+Files.swift b/Sources/S3/S3+Files.swift new file mode 100755 index 0000000..9b1c30c --- /dev/null +++ b/Sources/S3/S3+Files.swift @@ -0,0 +1,29 @@ +// +// S3+Files.swift +// S3 +// +// Created by Ondrej Rafaj on 01/12/2016. +// Copyright © 2016 manGoweb UK Ltd. All rights reserved. +// + +import Foundation +import Vapor + + +// Helper S3 extension for uploading files by their URL/path +public extension S3 { + + /// Upload file by it's URL to S3, full set + public func put(file url: URL, destination: String, bucket: String? = nil, access: AccessControlList = .privateAccess, on req: Request) throws -> Future { + let data: Data = try Data(contentsOf: url) + let file = File.Upload(data: data, bucket: bucket, destination: destination, access: access, mime: mimeType(forFileAtUrl: url)) + return try put(file: file, on: req) + } + + /// Upload file by it's path to S3, full set + public func put(file path: String, destination: String, bucket: String? = nil, access: AccessControlList = .privateAccess, on req: Request) throws -> Future { + let url: URL = URL(fileURLWithPath: path) + return try put(file: url, destination: destination, bucket: bucket, access: access, on: req) + } + +} diff --git a/Sources/S3/S3+Strings.swift b/Sources/S3/S3+Strings.swift new file mode 100755 index 0000000..c77636c --- /dev/null +++ b/Sources/S3/S3+Strings.swift @@ -0,0 +1,24 @@ +// +// S3+Strings.swift +// S3 +// +// Created by Ondrej Rafaj on 01/12/2016. +// Copyright © 2016 manGoweb UK Ltd. All rights reserved. +// + +import Foundation +import Vapor + + +public extension S3 { + + /// Upload file content to S3, full set + public func put(string: String, mime: MediaType = .plainText, destination: String, bucket: String? = nil, access: AccessControlList = .privateAccess, on req: Request) throws -> Future { + guard let data: Data = string.data(using: String.Encoding.utf8) else { + throw Error.badStringData + } + let file = File.Upload(data: data, bucket: bucket, destination: destination, access: access, mime: mime) + return try put(file: file, on: req) + } + +} diff --git a/Sources/S3/S3.swift b/Sources/S3/S3.swift new file mode 100755 index 0000000..ebe23db --- /dev/null +++ b/Sources/S3/S3.swift @@ -0,0 +1,245 @@ +// +// S3.swift +// S3 +// +// Created by Ondrej Rafaj on 01/12/2016. +// Copyright © 2016 manGoweb UK Ltd. All rights reserved. +// + +import Foundation +import Vapor +@_exported import S3Signer +import HTTP + + +/// Main S3 class +public class S3: S3Client { + + /// Available access control list values for "x-amz-acl" header as specified in AWS documentation + public enum AccessControlList: String { + case privateAccess = "private" + case publicRead = "public-read" + case publicReadWrite = "public-read-write" + case awsExecRead = "aws-exec-read" + case authenticatedRead = "authenticated-read" + case bucketOwnerRead = "bucket-owner-read" + case bucketOwnerFullControl = "bucket-owner-full-control" + } + + + public struct File { + + /// File to be uploaded (PUT) + public struct Upload: FileInfo { + + /// Data + public internal(set) var data: Data + + /// Override target bucket + public internal(set) var bucket: String? + + /// S3 file path + public internal(set) var path: String + + /// Desired access control for file + public internal(set) var access: AccessControlList = .privateAccess + + /// Desired file type (mime) for the uploaded file + public internal(set) var mime: MediaType = .plainText + + // MARK: Initialization + + /// File data to be uploaded + public init(data: Data, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess, mime: MediaType = .plainText) { + self.data = data + self.bucket = bucket + self.path = destination + self.access = access + self.mime = mime + } + + /// File to be uploaded + public init(file: URL, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess) throws { + self.data = try Data(contentsOf: file) + self.bucket = bucket + self.path = destination + self.access = access + self.mime = mimeType(forFileAtUrl: file) + } + + /// File to be uploaded + public init(file: String, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess) throws { + guard let url = URL(string: file) else { + throw Error.invalidUrl + } + try self.init(file: url, bucket: bucket, destination: destination, access: access) + } + + } + + /// File to be located + public struct Location: FileInfo { + + /// Override target bucket + public internal(set) var bucket: String? + + /// S3 file path + public internal(set) var path: String + + } + + /// File response comming back from S3 + public struct Response { + + /// Data + public internal(set) var data: Data + + /// Override target bucket + public internal(set) var bucket: String? + + /// S3 file path + public internal(set) var path: String + + /// Access control for file + public internal(set) var access: AccessControlList? + + /// File type (mime) + public internal(set) var mime: MediaType + + } + } + + /// Error messages + public enum Error: Swift.Error { + case missingCredentials(String) + case invalidUrl + case badResponse(Response) + case badStringData + case missingData + case notFound + + case uploadFailed(Response) + } + + /// If set, this bucket name value will be used globally unless overriden by a specific call + public internal(set) var defaultBucket: String + + + // MARK: Initialization + + /// Basic initialization method, also registers S3Signer and self with services + @discardableResult public convenience init(defaultBucket: String, config: S3Signer.Config, services: inout Services) throws { + try self.init(defaultBucket: defaultBucket) + + try services.register(S3Signer(config)) + services.register(self) + } + + /// Basic initialization method + public init(defaultBucket: String) throws { + self.defaultBucket = defaultBucket + } + + // MARK: Managing objects + + /// Upload file to S3 + public func put(file: File.Upload, headers: [String: String] = [:], on req: Request) throws -> EventLoopFuture { + guard let url = try buildUrl(file: file, on: req) else { + throw Error.invalidUrl + } + + var awsHeaders: [String: String] = headers + awsHeaders["Content-Type"] = file.mime.description + awsHeaders["x-amz-acl"] = file.access.rawValue + let signer = try req.make(S3Signer.self) + let headers = try signer.headers(for: .PUT, urlString: url.absoluteString, headers: awsHeaders, payload: .bytes(file.data)) + + return try make(request: url, method: .PUT, headers: headers, data: file.data, on: req).map(to: File.Response.self) { response in + if response.http.status == .ok { + let res = File.Response(data: file.data, bucket: file.bucket ?? self.defaultBucket, path: file.path, access: file.access, mime: file.mime) + return res + } else { + throw Error.uploadFailed(response) + } + } + } + + + + /// Retrieve file data from S3 + public func get(file: File.Location, headers: [String: String] = [:], on req: Request) throws -> Future { + guard let url = try buildUrl(file: file, on: req) else { + throw Error.invalidUrl + } + + let awsHeaders: [String: String] = headers + let signer = try req.make(S3Signer.self) + let headers = try signer.headers(for: .GET, urlString: url.absoluteString, headers: awsHeaders, payload: .none) + + return try make(request: url, method: .GET, headers: headers, on: req).map(to: File.Response.self) { response in + if response.http.status == .notFound { + throw Error.notFound + } + guard response.http.status == .ok else { + throw Error.badResponse(response) + } + guard let data = response.http.body.data else { + throw Error.missingData + } + + let res = File.Response(data: data, bucket: file.bucket ?? self.defaultBucket, path: file.path, access: nil, mime: self.mimeType(forFileAtUrl: url)) + return res + } + } + + + /// Delete file from S3 + public func delete(file: File.Location, headers: [String: String] = [:], on req: Request) throws -> Future { + guard let url = try buildUrl(file: file, on: req) else { + throw Error.invalidUrl + } + + let awsHeaders: [String: String] = headers + let signer = try req.make(S3Signer.self) + let headers = try signer.headers(for: .GET, urlString: url.absoluteString, headers: awsHeaders, payload: .none) + + return try make(request: url, method: .DELETE, headers: headers, on: req).map(to: Void.self) { response in + if response.http.status == .notFound { + throw Error.notFound + } + guard response.http.status == .ok || response.http.status == .noContent else { + throw Error.badResponse(response) + } + return Void() + } + } + +} + +// MARK: - Helper methods + +extension S3 { + + static func mimeType(forFileAtUrl url: URL) -> MediaType { + guard let mediaType = MediaType.fileExtension(url.pathExtension) else { + return MediaType(type: "application", subType: "octet-stream") + } + return mediaType + } + + func mimeType(forFileAtUrl url: URL) -> MediaType { + return S3.mimeType(forFileAtUrl: url) + } + + func buildUrl(file: FileInfo, on req: Request) throws -> URL? { + let config = try req.makeS3Signer().config + + guard var url: URL = URL(string: config.region.host) else { + throw Error.invalidUrl + } + url.appendPathComponent(file.bucket ?? defaultBucket) + url.appendPathComponent(file.path) + return url + } + +} diff --git a/Sources/S3Signer/Dates.swift b/Sources/S3Signer/Dates.swift index aa98d47..a9a6dc6 100644 --- a/Sources/S3Signer/Dates.swift +++ b/Sources/S3Signer/Dates.swift @@ -1,45 +1,46 @@ import Foundation -internal struct Dates { - - /// The ISO8601 basic format timestamp of signature creation. YYYYMMDD'T'HHMMSS'Z'. - internal let long: String - - /// The short timestamp of signature creation. YYYYMMDD. - internal let short: String -} -extension Dates { - - internal init(date: Date) { - short = date.timestampShort - long = date.timestampLong - } +struct Dates { + + /// The ISO8601 basic format timestamp of signature creation. YYYYMMDD'T'HHMMSS'Z'. + let long: String + + /// The short timestamp of signature creation. YYYYMMDD. + let short: String + + init(_ date: Date) { + self.short = date.timestampShort + self.long = date.timestampLong + } + } + extension Date { - - private static let shortdateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyyMMdd" - formatter.timeZone = TimeZone(abbreviation: "UTC") - formatter.locale = Locale(identifier: "en_US_POSIX") - return formatter - }() - - private static let longdateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" - formatter.timeZone = TimeZone(abbreviation: "UTC") - formatter.locale = Locale(identifier: "en_US_POSIX") - return formatter - }() - - internal var timestampShort: String { - return Date.shortdateFormatter.string(from: self) - } - - internal var timestampLong: String { - return Date.longdateFormatter.string(from: self) - } + + private static let shortDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd" + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + + private static let longDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + + var timestampShort: String { + return Date.shortDateFormatter.string(from: self) + } + + var timestampLong: String { + return Date.longDateFormatter.string(from: self) + } + } diff --git a/Sources/S3Signer/Derived_from_LICENSE b/Sources/S3Signer/Derived_from_LICENSE new file mode 100644 index 0000000..84d5723 --- /dev/null +++ b/Sources/S3Signer/Derived_from_LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 JustinM1 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Sources/S3Signer/Expiration.swift b/Sources/S3Signer/Expiration.swift index 17453bf..0e7c559 100644 --- a/Sources/S3Signer/Expiration.swift +++ b/Sources/S3Signer/Expiration.swift @@ -1,14 +1,16 @@ import Foundation -public typealias Seconds = Int -/// Pre-Sign URL Expiration time +/// Pre-sign URL expiration time public enum Expiration { + + public typealias Seconds = Int + /// 30 minutes case thirtyMinutes /// 60 minutes - case oneHour + case hour /// 180 minutes case threeHours @@ -18,12 +20,13 @@ public enum Expiration { } extension Expiration { + /// Expiration Value - internal var value: Seconds { + var value: Seconds { switch self { case .thirtyMinutes: return 60 * 30 - case .oneHour: + case .hour: return 60 * 60 case .threeHours: return 60 * 60 * 3 @@ -31,4 +34,5 @@ extension Expiration { return exp } } + } diff --git a/Sources/S3Signer/Extensions/Container+S3Signer.swift b/Sources/S3Signer/Extensions/Container+S3Signer.swift new file mode 100644 index 0000000..1a91b71 --- /dev/null +++ b/Sources/S3Signer/Extensions/Container+S3Signer.swift @@ -0,0 +1,19 @@ +// +// Container+S3Signer.swift +// S3Signer +// +// Created by Ondrej Rafaj on 19/04/2018. +// + +import Foundation +import Vapor + + +extension Container { + + /// Returns S3 signer + public func makeS3Signer() throws -> S3Signer { + return try make() + } + +} diff --git a/Sources/S3Signer/PercentEncoder.swift b/Sources/S3Signer/Extensions/String+Encoding.swift similarity index 56% rename from Sources/S3Signer/PercentEncoder.swift rename to Sources/S3Signer/Extensions/String+Encoding.swift index e6a027a..76dc011 100644 --- a/Sources/S3Signer/PercentEncoder.swift +++ b/Sources/S3Signer/Extensions/String+Encoding.swift @@ -1,10 +1,13 @@ -import Core +import Foundation +import Vapor + struct AWSEncoding { - internal static let QueryAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~=&" - internal static let PathAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~/" + static let QueryAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~=&" + static let PathAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~/" } + extension String { func awsStringEncoding(_ type: String) -> String? { diff --git a/Sources/S3Signer/HTTPMethod+Description.swift b/Sources/S3Signer/HTTPMethod+Description.swift new file mode 100644 index 0000000..206b373 --- /dev/null +++ b/Sources/S3Signer/HTTPMethod+Description.swift @@ -0,0 +1,45 @@ +import Foundation +import Vapor + +extension HTTPMethod { + + var description: String { + switch self { + case .GET: return "GET" + case .PUT: return "PUT" + case .ACL: return "ACL" + case .HEAD: return "HEAD" + case .POST: return "POST" + case .COPY: return "COPY" + case .LOCK: return "LOCK" + case .MOVE: return "MOVE" + case .BIND: return "BIND" + case .LINK: return "LINK" + case .PATCH: return "PATCH" + case .TRACE: return "TRACE" + case .MKCOL: return "MKCOL" + case .MERGE: return "MERGE" + case .PURGE: return "PURGE" + case .NOTIFY: return "NOTIFY" + case .SEARCH: return "SEARCH" + case .UNLOCK: return "UNLOCK" + case .REBIND: return "REBIND" + case .UNBIND: return "UNBIND" + case .REPORT: return "REPORT" + case .DELETE: return "DELETE" + case .UNLINK: return "UNLINK" + case .CONNECT: return "CONNECT" + case .MSEARCH: return "MSEARCH" + case .OPTIONS: return "OPTIONS" + case .PROPFIND: return "PROPFIND" + case .CHECKOUT: return "CHECKOUT" + case .PROPPATCH: return "PROPPATCH" + case .SUBSCRIBE: return "SUBSCRIBE" + case .MKCALENDAR: return "MKCALENDAR" + case .MKACTIVITY: return "MKACTIVITY" + case .UNSUBSCRIBE: return "UNSUBSCRIBE" + case .RAW(let value): return value + } + } + +} diff --git a/Sources/S3Signer/HTTPMethod.swift b/Sources/S3Signer/HTTPMethod.swift deleted file mode 100644 index 176cd64..0000000 --- a/Sources/S3Signer/HTTPMethod.swift +++ /dev/null @@ -1,16 +0,0 @@ -/// HTTP Method -/// -/// - delete: DELETE -/// - get: GET -/// - head: HEAD -/// - post: POST -/// - The POST operation adds an object to a specified bucket using HTML forms. POST is an alternate form of PUT that enables browser-based uploads as a way of putting objects in buckets. -/// - put: PUT -/// See https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectOps.html for more information. -public enum HTTPMethod: String { - case delete = "DELETE" - case get = "GET" - case head = "HEAD" - case post = "POST" - case put = "PUT" -} diff --git a/Sources/S3Signer/Payload.swift b/Sources/S3Signer/Payload.swift index b265695..c15962d 100644 --- a/Sources/S3Signer/Payload.swift +++ b/Sources/S3Signer/Payload.swift @@ -8,39 +8,49 @@ public enum Payload { } extension Payload { - internal var bytes: Data { + + var bytes: Data { switch self { - case .bytes(let bytes): return bytes - default: return "".convertToData() + case .bytes(let bytes): + return bytes + default: + return "".convertToData() } } - internal func hashed() throws -> String { + func hashed() throws -> String { switch self { - case .bytes(let bytes): return try SHA256.hash(bytes).hexEncodedString() - case .none: return try SHA256.hash("".convertToData()).hexEncodedString() - case .unsigned: return "UNSIGNED-PAYLOAD" + case .bytes(let bytes): + return try SHA256.hash(bytes).hexEncodedString() + case .none: + return try SHA256.hash("".convertToData()).hexEncodedString() + case .unsigned: + return "UNSIGNED-PAYLOAD" } } - internal var isBytes: Bool { + var isBytes: Bool { switch self { case .bytes( _), .none: return true default: return false } } - internal func size() -> String { + func size() -> String { switch self { - case .bytes, .none: return self.bytes.count.description - case .unsigned: return "UNSIGNED-PAYLOAD" + case .bytes, .none: + return self.bytes.count.description + case .unsigned: + return "UNSIGNED-PAYLOAD" } } - internal var isUnsigned: Bool { + var isUnsigned: Bool { switch self { - case .unsigned: return true - default: return false + case .unsigned: + return true + default: + return false } } } diff --git a/Sources/S3Signer/Region.swift b/Sources/S3Signer/Region.swift index f675075..b6e45c1 100644 --- a/Sources/S3Signer/Region.swift +++ b/Sources/S3Signer/Region.swift @@ -1,37 +1,62 @@ -/// The region the bucket is located. +import Foundation + +/// AWS Region public enum Region: String { - - case apNortheast1 = "ap-northeast-1" - case apNortheast2 = "ap-northeast-2" - case apSouth1 = "ap-south-1" - case apSoutheast1 = "ap-southeast-1" - case apSoutheast2 = "ap-southeast-2" - case caCentral1 = "ca-central-1" - case euCentral1 = "eu-central-1" - case euWest1 = "eu-west-1" - case euWest2 = "eu-west-2" - case saEast1 = "sa-east-1" - case usEast1_Virginia = "us-east-1" - case usEast2_Ohio = "us-east-2" - case usWest1 = "us-west-1" - case usWest2 = "us-west-2" - - public var host: String { - switch self { - case .apNortheast1: return "s3-ap-northeast-1.amazonaws.com" - case .apNortheast2: return "s3.ap-northeast-2.amazonaws.com" - case .apSouth1: return "s3.ap-south-1.amazonaws.com" - case .apSoutheast1: return "s3-ap-southeast-1.amazonaws.com" - case .apSoutheast2: return "s3-ap-southeast-2.amazonaws.com" - case .caCentral1: return "s3.ca-central-1.amazonaws.com" - case .euCentral1: return "s3.eu-central-1.amazonaws.com" - case .euWest1: return "s3-eu-west-1.amazonaws.com" - case .euWest2: return "s3.eu-west-2.amazonaws.com" - case .saEast1: return "s3-sa-east-1.amazonaws.com" - case .usEast1_Virginia: return "s3.amazonaws.com" - case .usEast2_Ohio: return "s3.us-east-2.amazonaws.com" - case .usWest1: return "s3-us-west-1.amazonaws.com" - case .usWest2: return "s3-us-west-2.amazonaws.com" - } - } + /// US East (N. Virginia) + case usEast1 = "us-east-1" + + /// US East (Ohio) + case usEast2 = "us-east-2" + + /// US West (N. California) + case usWest1 = "us-west-1" + + /// US West (Oregon) + case usWest2 = "us-west-2" + + /// Canada (Central) + case caCentral1 = "ca-central-1" + + /// EU (Frankfurt) + case euCentral1 = "eu-central-1" + + /// EU (Ireland) + case euWest1 = "eu-west-1" + + /// EU (London) + case euWest2 = "eu-west-2" + + /// EU (Paris) + case euWest3 = "eu-west-3" + + /// Asia Pacific (Tokyo) + case apNortheast1 = "ap-northeast-1" + + /// Asia Pacific (Seoul) + case apNortheast2 = "ap-northeast-2" + + /// Asia Pacific (Osaka-Local) + case apNortheast3 = "ap-northeast-3" + + /// Asia Pacific (Singapore) + case apSoutheast1 = "ap-southeast-1" + + /// Asia Pacific (Sydney) + case apSoutheast2 = "ap-southeast-2" + + /// Asia Pacific (Mumbai) + case apSouth1 = "ap-south-1" + + /// South America (São Paulo) + case saEast1 = "sa-east-1" +} + + +extension Region { + + /// Generate base URL + public var host: String { + return "https://s3.\(self.rawValue).amazonaws.com".finished(with: "/") + } + } diff --git a/Sources/S3Signer/S3Signer.swift b/Sources/S3Signer/S3Signer.swift new file mode 100644 index 0000000..b2bc7df --- /dev/null +++ b/Sources/S3Signer/S3Signer.swift @@ -0,0 +1,193 @@ +import Foundation +import Vapor +import Crypto + + +/// S3 Client: All network calls to and from AWS' S3 servers +public final class S3Signer: Service { + + /// Errors + public enum Error: Swift.Error { + case badURL + case invalidEncoding + } + + /// S3 Configuration + public struct Config: Service { + + /// AWS Access Key + let accessKey: String + + /// AWS Secret Key + let secretKey: String + + /// The region where S3 bucket is located. + public let region: Region + + /// AWS Security Token. Used to validate temporary credentials, such as those from an EC2 Instance's IAM role + let securityToken : String? + + /// AWS Service type + let service: String = "s3" + + /// Initalizer + public init(accessKey: String, secretKey: String, region: Region, securityToken: String? = nil) { + self.accessKey = accessKey + self.secretKey = secretKey + self.region = region + self.securityToken = securityToken + } + + } + + /// Configuration + public private(set) var config: Config + + /// Initializer + public init(_ config: Config) throws { + self.config = config + } + +} + +extension S3Signer { + + public func putFile(_ data: Data, to path: String, in bucket: String, on req: Request) throws -> Future { + let client = try req.make(Client.self) + let url = config.region.host + bucket.finished(with: "/") + path + let headers = try self.headers(for: .PUT, urlString: url, payload: Payload.bytes(data)) + + let request = Request(using: req.privateContainer) + request.http.method = .PUT + request.http.headers = headers + request.http.body = HTTPBody(data: data) + request.http.url = URL(string: url)! + return try client.respond(to: request) + } + + public func headers(for httpMethod: HTTPMethod, urlString: String, headers: [String: String] = [:], payload: Payload) throws -> HTTPHeaders { + guard let url = URL(string: urlString) else { throw S3Signer.Error.badURL } + let dates = getDates(Date()) + let bodyDigest = try payload.hashed() + var updatedHeaders = updateHeaders(headers, url: url, longDate: dates.long, bodyDigest: bodyDigest) + + if httpMethod == .PUT && payload.isBytes { + // TODO: Figure out why S3 would fail with this + updatedHeaders["Content-MD5"] = try MD5.hash(payload.bytes).hexEncodedString() + } + + updatedHeaders["Authorization"] = try self.headers(httpMethod, url: url, headers: updatedHeaders, bodyDigest: bodyDigest, dates: dates) + + if httpMethod == .PUT { + updatedHeaders["Content-Length"] = payload.size() + if url.pathExtension != "" { + updatedHeaders["Content-Type"] = url.pathExtension + } + } + + if payload.isUnsigned { + updatedHeaders["x-amz-content-sha256"] = bodyDigest + } + + var headers = HTTPHeaders() + for (key, value) in updatedHeaders { + headers.add(name: key, value: value) + } + + return headers + } +} + +extension S3Signer { + + private func canonicalHeaders(_ headers: [String: String]) -> String { + let headerList = Array(headers.keys) + .map { "\($0.lowercased()):\(headers[$0]!)" } + .filter { $0 != "authorization" } + .sorted(by: { $0.localizedCompare($1) == ComparisonResult.orderedAscending }) + .joined(separator: "\n") + .appending("\n") + return headerList + } + + private func createCanonicalRequest(_ httpMethod: HTTPMethod, url: URL, headers: [String: String], bodyDigest: String) throws -> String { + return try [httpMethod.description, path(url), query(url), canonicalHeaders(headers),signedHeaders(headers), bodyDigest].joined(separator: "\n") + } + + private func createSignature(_ stringToSign: String, timeStampShort: String) throws -> String { + let dateKey = try HMAC.SHA256.authenticate(timeStampShort.convertToData(), key: "AWS4\(config.secretKey)".convertToData()) + let dateRegionKey = try HMAC.SHA256.authenticate(config.region.rawValue.convertToData(), key: dateKey) + let dateRegionServiceKey = try HMAC.SHA256.authenticate(config.service.convertToData(), key: dateRegionKey) + let signingKey = try HMAC.SHA256.authenticate("aws4_request".convertToData(), key: dateRegionServiceKey) + let signature = try HMAC.SHA256.authenticate(stringToSign.convertToData(), key: signingKey) + return signature.hexEncodedString() + } + + private func createStringToSign(_ canonicalRequest: String, dates: Dates) throws -> String { + let canonRequestHash = try SHA256.hash(canonicalRequest.convertToData()).hexEncodedString() + return ["AWS4-HMAC-SHA256", dates.long, credentialScope(dates.short), canonRequestHash].joined(separator: "\n") + } + + private func credentialScope(_ timeStampShort: String) -> String { + return [timeStampShort, config.region.rawValue, config.service, "aws4_request"].joined(separator: "/") + } + + private func headers(_ httpMethod: HTTPMethod, url: URL, headers: [String: String], bodyDigest: String, dates: Dates) throws -> String { + let canonicalRequestHex = try createCanonicalRequest(httpMethod, url: url, headers: headers, bodyDigest: bodyDigest) + let stringToSign = try createStringToSign(canonicalRequestHex, dates: dates) + let signature = try createSignature(stringToSign, timeStampShort: dates.short) + let authHeader = "AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(credentialScope(dates.short)), SignedHeaders=\(signedHeaders(headers)), Signature=\(signature)" + return authHeader + } + + private func getDates(_ date: Date) -> Dates { + return Dates(date) + } + + private func path(_ url: URL) -> String { + return !url.path.isEmpty ? url.path.awsStringEncoding(AWSEncoding.PathAllowed) ?? "/" : "/" + } + + private func presignedURLCanonRequest(_ httpMethod: HTTPMethod, dates: Dates, expiration: Expiration, url: URL, headers: [String: String]) throws -> (String, URL) { + guard let credScope = credentialScope(dates.short).awsStringEncoding(AWSEncoding.QueryAllowed), + let signHeaders = signedHeaders(headers).awsStringEncoding(AWSEncoding.QueryAllowed) else { throw S3Signer.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 S3Signer.Error.badURL + } + + return try ([httpMethod.description, path(url), query(url), canonicalHeaders(headers), signedHeaders(headers), "UNSIGNED-PAYLOAD"].joined(separator: "\n"), url) + } + + private func query(_ url: URL) throws -> String { + if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems { + let items = queryItems.map({ ($0.name.awsStringEncoding(AWSEncoding.QueryAllowed) ?? "", $0.value?.awsStringEncoding(AWSEncoding.QueryAllowed) ?? "") }) + let encodedItems = items.map({ "\($0.0)=\($0.1)" }) + return encodedItems.sorted().joined(separator: "&") + } + return "" + } + + private func signedHeaders(_ headers: [String: String]) -> String { + let headerList = Array(headers.keys).map { $0.lowercased() }.filter { $0 != "authorization" }.sorted().joined(separator: ";") + return headerList + } + + private func updateHeaders(_ headers: [String: String], url: URL, longDate: String, bodyDigest: String) -> [String: String] { + var updatedHeaders = headers + updatedHeaders["X-Amz-Date"] = longDate + updatedHeaders["Host"] = url.host ?? config.region.host + + if bodyDigest != "UNSIGNED-PAYLOAD" && config.service == "s3" { + updatedHeaders["x-amz-content-sha256"] = bodyDigest + } + // According to http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS + if let token = config.securityToken { + updatedHeaders["X-Amz-Security-Token"] = token + } + return updatedHeaders + } + +} diff --git a/Sources/S3Signer/S3SignerAWS.swift b/Sources/S3Signer/S3SignerAWS.swift deleted file mode 100644 index f0f113f..0000000 --- a/Sources/S3Signer/S3SignerAWS.swift +++ /dev/null @@ -1,350 +0,0 @@ -import Foundation -import Crypto -import Core -import Bits - - -public enum S3Error: Error { - case badURL - case invalidEncoding -} - - - -public class S3SignerAWS { - - // the salt for this hash - public let config: BCryptConfig - - /// AWS Access Key - private let accessKey: String - - /// The region where S3 bucket is located. - public let region: Region - - /// AWS Secret Key - private let secretKey: String - - /// AWS Security Token. Used to validate temporary credentials, such as those from an EC2 Instance's IAM role - private let securityToken : String? // - - /// The service used in calculating the signature. Currently limited to s3, possible expansion to other services after testing. - internal var service: String { - return "s3" - } - - /// Initializes a signer which works for either permanent credentials or temporary secrets - /// - /// - Parameters: - /// - accessKey: AWS Access Key - /// - secretKey: AWS Secret Key - /// - region: Which AWS region to sign against - /// - securityToken: Optional token used only with temporary credentials - public init(accessKey: String, - secretKey: String, - region: Region, - securityToken: String? = nil) - { - self.accessKey = accessKey - self.secretKey = secretKey - self.region = region - self.securityToken = securityToken - } - - /// Generate a V4 auth header for aws Requests. - /// - /// - Parameters: - /// - httpMethod: HTTP Method (GET, HEAD, PUT, POST, DELETE) - /// - urlString: Full URL String. Left for ability to customize whether a virtual hosted-style request i.e. "https://exampleBucket.s3.amazonaws.com" vs path-style request i.e. "https://s3.amazonaws.com/exampleBucket". Make sure to include url scheme i.e. https:// or signature will not be calculated properly. - /// - headers: Any additional headers you want incuded in the signature. All the required headers are created automatically. - /// - payload: The payload being sent with request - /// - Returns: The required headers that need to be sent with request. Host, X-Amz-Date, Authorization - /// - If PUT request, Content-Length - /// - if PUT and pathExtension is available, Content-Type - /// - if PUT and not unsigned, Content-md5 - /// - Throws: S3SignerError - public func authHeaderV4( - httpMethod: HTTPMethod, - urlString: String, - headers: [String: String] = [:], - payload: Payload) - throws -> [String:String] - { - guard let url = URL(string: urlString) else { - throw S3SignerError.badURL - } - - let dates = getDates(date: Date()) - - let bodyDigest = try payload.hashed() - - var updatedHeaders = updateHeaders( - headers: headers, - url: url, - longDate: dates.long, - bodyDigest: bodyDigest) - - if httpMethod == .put && payload.isBytes { - // WARNING: S3 might be failing with this - updatedHeaders["Content-MD5"] = try MD5.hash(payload.bytes).hexEncodedString() - } - - updatedHeaders["Authorization"] = try generateAuthHeader( - httpMethod: httpMethod, - url: url, - headers: updatedHeaders, - bodyDigest: bodyDigest, - dates: dates) - - if httpMethod == .put { - updatedHeaders["Content-Length"] = payload.size() - - if url.pathExtension != "" { - updatedHeaders["Content-Type"] = url.pathExtension - } - } - - if payload.isUnsigned { - updatedHeaders["x-amz-content-sha256"] = bodyDigest - } - - return updatedHeaders - } - - /// Generate a V4 pre-signed URL - /// - /// - Parameters: - /// - httpMethod: The method of request. - /// - urlString: Full URL String. Left for ability to customize whether a virtual hosted-style request i.e. "https://exampleBucket.s3.amazonaws.com" vs path-style request i.e. "https://s3.amazonaws.com/exampleBucket". Make sure to include url scheme i.e. https:// or signature will not be calculated properly. - /// - expiration: How long the URL is valid. - /// - headers: Any additional headers to be included with signature calculation. - /// - Returns: Pre-signed URL string. - /// - Throws: S3SignerError - public func presignedURLV4( - httpMethod: HTTPMethod, - urlString: String, - expiration: TimeFromNow, - headers: [String:String]) - throws -> String - { - guard let url = URL(string: urlString) else { - throw S3SignerError.badURL - } - - let dates = getDates(date: Date()) - - var updatedHeaders = headers - - updatedHeaders["Host"] = url.host ?? region.host - - let (canonRequest, fullURL) = try presignedURLCanonRequest(httpMethod: httpMethod, dates: dates, expiration: expiration, url: url, headers: updatedHeaders) - - let stringToSign = try createStringToSign(canonicalRequest: canonRequest, dates: dates) - - let signature = try createSignature(stringToSign, timeStampShort: dates.short) - - let presignedURL = fullURL.absoluteString.appending("&X-Amz-Signature=\(signature)") - - return presignedURL - } - - internal func canonicalHeaders( - headers: [String: String]) - -> String - { - #if swift(>=4) - - let headerList = Array(headers.keys) - .map { "\($0.lowercased()):\(headers[$0]!)" } - .filter { $0 != "authorization" } - .sorted(by: { $0.localizedCompare($1) == ComparisonResult.orderedAscending }) - .joined(separator: "\n") - .appending("\n") - - return headerList - - #else - - let headerList = Array(headers.keys) - .map { "\($0.lowercased()):\(headers[$0]!)" } - .filter { $0 != "authorization" } - .sorted { $0.0.localizedCompare($0.1) == ComparisonResult.orderedAscending } - .joined(separator: "\n") - .appending("\n") - - return headerList - - #endif - } - - internal func createCanonicalRequest(_ - httpMethod: HTTPMethod, - url: URL, - headers: [String: String], - bodyDigest: String) - throws -> String - { - return try [ - httpMethod.rawValue, - path(url: url), - query(url), - canonicalHeaders(headers: headers), - signedHeaders(headers: headers), - bodyDigest - ].joined(separator: "\n") - } - - /// Create signature - /// - /// - Parameters: - /// - stringToSign: String to sign. - /// - timeStampShort: Short timestamp. - /// - Returns: Signature. - /// - Throws: HMAC error. - private func createSignature(_ stringToSign: String, timeStampShort: String) throws -> String { - let dateKey = try HMAC.SHA256.authenticate(timeStampShort.convertToData(), key: "AWS4\(self.config.secretKey)".convertToData()) - let dateRegionKey = try HMAC.SHA256.authenticate(self.config.region.rawValue.convertToData(), key: dateKey) - let dateRegionServiceKey = try HMAC.SHA256.authenticate(self.config.service.convertToData(), key: dateRegionKey) - let signingKey = try HMAC.SHA256.authenticate("aws4_request".convertToData(), key: dateRegionServiceKey) - let signature = try HMAC.SHA256.authenticate(stringToSign.convertToData(), key: signingKey) - return signature.hexEncodedString() - } - - /// Create the String To Sign portion of signature. - /// - /// - Parameters: - /// - canonicalRequest: The canonical request used. - /// - dates: The dates object containing short and long timestamps of request. - /// - Returns: String to sign. - /// - Throws: If hashing canonical request fails. - internal func createStringToSign( - canonicalRequest: String, - dates: Dates) - throws -> String - { - let canonRequestHash = try SHA256.hash(canonicalRequest.convertToData()).hexEncodedString() - return ["AWS4-HMAC-SHA256", - dates.long, - credentialScope(timeStampShort: dates.short), - canonRequestHash] - .joined(separator: "\n") - } - - /// Credential scope - /// - /// - Parameter timeStampShort: Short timestamp. - /// - Returns: Credential Scope. - private func credentialScope(_ timeStampShort: String) -> String { - return [timeStampShort, self.config.region.rawValue, self.config.service, "aws4_request"].joined(separator: "/") - } - - /// Generate Auth Header for V4 Authorization Header request. - /// - /// - Parameters: - /// - httpMethod: The HTTPMethod of request. - /// - url: The URL of the request. - /// - headers: All headers used in signature calcuation. - /// - bodyDigest: The hashed payload of request. - /// - dates: The short and long timestamps of time of request. - /// - Returns: Authorization header value. - /// - Throws: S3SignerError - private func generateAuthHeader(_ httpMethod: HTTPMethod, url: URL, headers: [String: String], bodyDigest: String, dates: Dates) throws -> String { - let canonicalRequestHex = try self.createCanonicalRequest(httpMethod, url: url, headers: headers, bodyDigest: bodyDigest) - let stringToSign = try self.createStringToSign(canonicalRequestHex, dates: dates) - let signature = try self.createSignature(stringToSign, timeStampShort: dates.short) - let authHeader = "AWS4-HMAC-SHA256 Credential=\(self.config.accessKey)/\(self.credentialScope(dates.short)), SignedHeaders=\(self.signedHeaders(headers)), Signature=\(signature)" - return authHeader - } - - /// Instantiate Dates object containing the required date formats needed for signature calculation. - /// - /// - Parameter date: The date of request. - /// - Returns: Dates object. - internal func getDates(date: Date) -> Dates { - return Dates(date: date) - } - - /// The percent encoded path of request URL. - /// - /// - Parameter url: The URL of request. - /// - Returns: Percent encoded path if not empty, or "/". - /// - Throws: Encoding error. - private func path(url: URL) throws -> String { - return try !url.path.isEmpty ? url.path.percentEncode(allowing: Byte.awsPathAllowed) : "/" - } - - /// The canonical request for Presigned URL requests. - /// - /// - Parameters: - /// - httpMethod: HTTPMethod of request. - /// - dates: Dates formatted for request. - /// - expiration: The period of time before URL expires. - /// - url: The URL of the request. - /// - headers: Headers used to sign and add to presigned URL. - /// - Returns: Canonical request for pre-signed URL. - /// - Throws: S3SignerError - private func presignedURLCanonRequest(_ httpMethod: HTTPMethod, dates: Dates, expiration: Expiration, url: URL, headers: [String: String]) throws -> (String, URL) { - guard let credScope = self.credentialScope(dates.short).awsStringEncoding(AWSEncoding.QueryAllowed), - let signHeaders = self.signedHeaders(headers).awsStringEncoding(AWSEncoding.QueryAllowed) else { throw S3Error.invalidEncoding } - let fullURL = "\(url.absoluteString)?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=\(self.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 S3Error.badURL - } - - return try ([httpMethod.description, self.path(url), self.query(url), self.canonicalHeaders(headers), self.signedHeaders(headers), "UNSIGNED-PAYLOAD"].joined(separator: "\n"), url) - } - - /// Encode and sort queryItems. - /// - /// - Parameter url: The URL for request containing the possible queryItems. - /// - Returns: Encoded and sorted(By Key) queryItem String. - /// - Throws: Encoding Error - private func query(_ url: URL) throws -> String { - if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems { - let items = queryItems.map({ ($0.name.awsStringEncoding(AWSEncoding.QueryAllowed) ?? "", $0.value?.awsStringEncoding(AWSEncoding.QueryAllowed) ?? "") }) - let encodedItems = items.map({ "\($0.0)=\($0.1)" }) - return encodedItems.sorted().joined(separator: "&") - } - return "" - } - - /// Signed headers - /// - /// - Parameter headers: Headers to sign. - /// - Returns: Signed headers. - private func signedHeaders(headers: [String: String]) -> String { - let headerList = Array(headers.keys).map { $0.lowercased() }.filter { $0 != "authorization" }.sorted().joined(separator: ";") - return headerList - } - - /// Add the required headers to a V4 authorization header request. - /// - /// - Parameters: - /// - headers: Original headers to add the additional required headers to. - /// - url: The URL of the request. - /// - longDate: The formatted ISO date. - /// - bodyDigest: The payload hash of request. - /// - Returns: Updated headers with additional required headers. - internal func updateHeaders( - headers: [String:String], - url: URL, - longDate: String, - bodyDigest: String) - -> [String:String] - { - var updatedHeaders = headers - updatedHeaders["X-Amz-Date"] = longDate - updatedHeaders["Host"] = url.host ?? region.host - - if bodyDigest != "UNSIGNED-PAYLOAD" && service == "s3" { - updatedHeaders["x-amz-content-sha256"] = bodyDigest - } - // According to http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS - if let token = securityToken { - updatedHeaders["X-Amz-Security-Token"] = token - } - return updatedHeaders - } -} diff --git a/Sources/S3Signer/S3SignerError.swift b/Sources/S3Signer/S3SignerError.swift deleted file mode 100644 index b9d61a6..0000000 --- a/Sources/S3Signer/S3SignerError.swift +++ /dev/null @@ -1,3 +0,0 @@ -public enum S3SignerError: Error { - case badURL -} diff --git a/Sources/S3Signer/TimeFromNow.swift b/Sources/S3Signer/TimeFromNow.swift deleted file mode 100644 index a1134d8..0000000 --- a/Sources/S3Signer/TimeFromNow.swift +++ /dev/null @@ -1,29 +0,0 @@ -public typealias Seconds = Int - -/// How long until the V4 Pre-signed URL expires. -/// -/// - thirtyMinutes: 30 minutes -/// - oneHour: 60 minutes -/// - threeHours: 180 minutes -/// - custom: Custom expiration time, in seconds. -public enum TimeFromNow { - case thirtyMinutes - case oneHour - case threeHours - case custom(Seconds) - - - /// V4 expiration. - internal var expiration: Seconds { - switch self { - case .thirtyMinutes: - return 60 * 30 - case .oneHour: - return 60 * 60 - case .threeHours: - return 60 * 60 * 3 - case .custom(let exp): - return exp - } - } -} diff --git a/Sources/S3Signer/scripts/update.sh b/Sources/S3Signer/scripts/update.sh new file mode 100755 index 0000000..c254080 --- /dev/null +++ b/Sources/S3Signer/scripts/update.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +rm -rf .build +vapor clean -y --verbose +vapor xcode -n --verbose diff --git a/Sources/S3Signer/scripts/upgrade.sh b/Sources/S3Signer/scripts/upgrade.sh new file mode 100755 index 0000000..6a98522 --- /dev/null +++ b/Sources/S3Signer/scripts/upgrade.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +rm -rf .build +vapor clean -y --verbose +rm Package.resolved +vapor xcode -n --verbose diff --git a/Sources/S3TestTools/Dates.swift b/Sources/S3TestTools/Dates.swift deleted file mode 100644 index d86622c..0000000 --- a/Sources/S3TestTools/Dates.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -class Aaaaa { - -} diff --git a/Sources/S3TestTools/Extensions/Services+S3Mock.swift b/Sources/S3TestTools/Extensions/Services+S3Mock.swift new file mode 100644 index 0000000..280a3ea --- /dev/null +++ b/Sources/S3TestTools/Extensions/Services+S3Mock.swift @@ -0,0 +1,20 @@ +// +// Services+S3Mock.swift +// S3TestTools +// +// Created by Ondrej Rafaj on 19/04/2018. +// + +import Foundation +import Service +import S3Signer +import S3 + + +extension Services { + + public mutating func registerS3Mock() throws { + try register(S3Mock(), as: S3Client.self) + } + +} diff --git a/Sources/S3TestTools/S3Mock.swift b/Sources/S3TestTools/S3Mock.swift new file mode 100644 index 0000000..79f47cf --- /dev/null +++ b/Sources/S3TestTools/S3Mock.swift @@ -0,0 +1,39 @@ +// +// S3.swift +// S3 +// +// Created by Ondrej Rafaj on 18/04/2018. +// + +import Foundation +@_exported import S3 +import Vapor + + +class S3Mock: S3Client { + + func put(file: S3.File.Upload, headers: [String: String], on req: Request) throws -> EventLoopFuture { + fatalError() + } + + func put(file url: URL, destination: String, bucket: String?, access: S3.AccessControlList, on req: Request) throws -> Future { + fatalError() + } + + func put(file path: String, destination: String, bucket: String?, access: S3.AccessControlList, on req: Request) throws -> Future { + fatalError() + } + + func put(string: String, mime: MediaType, destination: String, bucket: String?, access: S3.AccessControlList, on req: Request) throws -> Future { + fatalError() + } + + func get(file: S3.File.Location, headers: [String: String], on req: Request) throws -> Future { + fatalError() + } + + func delete(file: S3.File.Location, headers: [String: String], on req: Request) throws -> Future { + fatalError() + } + +} diff --git a/circle.yml b/circle.yml deleted file mode 100644 index c15e3fb..0000000 --- a/circle.yml +++ /dev/null @@ -1,10 +0,0 @@ -dependencies: - override: - - eval "$(curl -sL https://apt.vapor.sh)" - - sudo apt-get install vapor - - sudo chmod -R a+rx /usr/ -test: - override: - - swift build - - swift build -c release - - swift test diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index fdde0b1..0000000 --- a/codecov.yml +++ /dev/null @@ -1,12 +0,0 @@ -coverage: - precision: 4 - round: down - range: "60...100" - -comment: - layout: "header" - behavior: default - require_changes: no - -ignore: -- "Tests/.*" diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100755 index 0000000..c254080 --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +rm -rf .build +vapor clean -y --verbose +vapor xcode -n --verbose diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh new file mode 100755 index 0000000..6a98522 --- /dev/null +++ b/scripts/upgrade.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +rm -rf .build +vapor clean -y --verbose +rm Package.resolved +vapor xcode -n --verbose