├── .gitignore ├── .swift-version ├── License.md ├── Package.resolved ├── Package.swift ├── Package@swift-4.swift ├── README.md ├── Sources ├── Dictionary+vaporHeaders.swift ├── DropletExtension.swift └── Provider.swift └── Tests └── VaporS3SignerTests └── VaporS3SignerTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build 3 | Packages 4 | Package.pins 5 | *.xcodeproj 6 | *.xcodeproj/xcshareddata/xcbaselines/4724F8141DA88A530003BAC6.xcbaseline 7 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 JustinM1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "BCrypt", 6 | "repositoryURL": "https://github.com/vapor/bcrypt.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "3ee4aca16ba6ebfb1ad48cc5fd4dfb163c6d6be8", 10 | "version": "1.1.0" 11 | } 12 | }, 13 | { 14 | "package": "Bits", 15 | "repositoryURL": "https://github.com/vapor/bits.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c32f5e6ae2007dccd21a92b7e33eba842dd80d2f", 19 | "version": "1.1.0" 20 | } 21 | }, 22 | { 23 | "package": "CTLS", 24 | "repositoryURL": "https://github.com/vapor/ctls.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "fddec6a4643d6e85b6bb6dc54b1b5cdbabd395d2", 28 | "version": "1.1.2" 29 | } 30 | }, 31 | { 32 | "package": "Console", 33 | "repositoryURL": "https://github.com/vapor/console.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "11c0694857d1be6c7b8b30d8db8b1162b73f2a2b", 37 | "version": "2.2.0" 38 | } 39 | }, 40 | { 41 | "package": "Core", 42 | "repositoryURL": "https://github.com/vapor/core.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "b8330808f4f6b69941961afe8ad6b015562f7b7c", 46 | "version": "2.1.2" 47 | } 48 | }, 49 | { 50 | "package": "Crypto", 51 | "repositoryURL": "https://github.com/vapor/crypto.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "bf4470b9da79024aab79c85de80374f6c29e3864", 55 | "version": "2.1.1" 56 | } 57 | }, 58 | { 59 | "package": "Debugging", 60 | "repositoryURL": "https://github.com/vapor/debugging.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "49c5e8f0a7cb5456a8f7c72c6cd9f1553e5885a8", 64 | "version": "1.1.0" 65 | } 66 | }, 67 | { 68 | "package": "Engine", 69 | "repositoryURL": "https://github.com/vapor/engine.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "decf702d774ac630dfe0441ff76b4bb68257b77a", 73 | "version": "2.2.1" 74 | } 75 | }, 76 | { 77 | "package": "JSON", 78 | "repositoryURL": "https://github.com/vapor/json.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "735800d8f2e75ebe3be25559eb6a781f4666dcfc", 82 | "version": "2.2.1" 83 | } 84 | }, 85 | { 86 | "package": "Multipart", 87 | "repositoryURL": "https://github.com/vapor/multipart.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "8e541b2e6fc64a3741eca2aa48ee2c3f23cbe17c", 91 | "version": "2.1.1" 92 | } 93 | }, 94 | { 95 | "package": "Node", 96 | "repositoryURL": "https://github.com/vapor/node.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "642f357d08ec5aa335ae2e3c4633c72da7b5a0c4", 100 | "version": "2.1.1" 101 | } 102 | }, 103 | { 104 | "package": "Random", 105 | "repositoryURL": "https://github.com/vapor/random.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "d7c4397d125caba795d14d956efacfe2a27a63d0", 109 | "version": "1.2.0" 110 | } 111 | }, 112 | { 113 | "package": "Routing", 114 | "repositoryURL": "https://github.com/vapor/routing.git", 115 | "state": { 116 | "branch": null, 117 | "revision": "cb9d78aca2540c1a6b45b0ab43e5b0c50f29d216", 118 | "version": "2.2.0" 119 | } 120 | }, 121 | { 122 | "package": "S3SignerAWS", 123 | "repositoryURL": "https://github.com/JustinM1/S3SignerAWS.git", 124 | "state": { 125 | "branch": null, 126 | "revision": "571994c7f9807fa85c5e8b9cdedc9c4af0da05b5", 127 | "version": "3.0.2" 128 | } 129 | }, 130 | { 131 | "package": "Sockets", 132 | "repositoryURL": "https://github.com/vapor/sockets.git", 133 | "state": { 134 | "branch": null, 135 | "revision": "70d14c0e223257176f5ef69a595f7cad5de7a88b", 136 | "version": "2.2.1" 137 | } 138 | }, 139 | { 140 | "package": "TLS", 141 | "repositoryURL": "https://github.com/vapor/tls.git", 142 | "state": { 143 | "branch": null, 144 | "revision": "6c6eedb6761cddc6b6c87142a27eec13fa1701ec", 145 | "version": "2.1.1" 146 | } 147 | }, 148 | { 149 | "package": "Vapor", 150 | "repositoryURL": "https://github.com/vapor/vapor.git", 151 | "state": { 152 | "branch": null, 153 | "revision": "0747ab4819f90c9be451847f80a153184526ad79", 154 | "version": "2.3.0" 155 | } 156 | } 157 | ] 158 | }, 159 | "version": 1 160 | } 161 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "VaporS3Signer", 5 | targets: [], 6 | dependencies: [ 7 | .Package(url: "https://github.com/JustinM1/S3SignerAWS.git", 8 | majorVersion: 3), 9 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2) 10 | ] 11 | ) 12 | -------------------------------------------------------------------------------- /Package@swift-4.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "VaporS3Signer", 6 | products: [ 7 | .library(name: "VaporS3Signer", targets: ["VaporS3Signer"]) 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/JustinM1/S3SignerAWS.git", .upToNextMajor(from: "3.0.0")), 11 | .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.0.0")), 12 | ], 13 | targets: [ 14 | .target(name: "VaporS3Signer", dependencies: ["S3SignerAWS", "Vapor"], path: "Sources"), 15 | .testTarget(name: "VaporS3SignerTests", dependencies: ["VaporS3Signer"]), 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VaporS3Signer 2 | 3 | [Vapor](https://vapor.codes/) Provider for [S3SignerAWS](https://github.com/JustinM1/S3SignerAWS) 4 | 5 | Generates V4 authorization headers and pre-signed URLs for authenticating AWS S3 REST API requests 6 | * Supports `DELETE/GET/HEAD/POST/PUT` 7 | 8 | ### Installation (SPM) 9 | ```ruby 10 | .Package(url: "https://github.com/JustinM1/VaporS3Signer.git", majorVersion: 3) 11 | ``` 12 | 13 | ### Config File 14 | 15 | - Add `vapor-S3Signer.json` file to your Config/secrets folder. 16 | 17 | The `vapor-S3Signer.json` file should contain your access key, secret key, and the region of your bucket. You can also include a temporary token for EC2 instance which is optional. 18 | 19 | Ex. 20 | ```ruby 21 | { 22 | "accessKey": "someKey", 23 | "secretKey": "someSecretKey", 24 | "region": "someRegionName", 25 | "securityToken": "someTempToken" 26 | } 27 | ``` 28 | Here are the names for each region: 29 | ##### CA 30 | * Canada (Central) = `"ca-central-1"` 31 | 32 | ##### US 33 | * US East 1 Virginia = `"us-east-1"` 34 | * US East 2 Ohio = `"us-east-2"` 35 | * US West 1 = `"us-west-1"` 36 | * US West 2 = `"us-west-2"` 37 | 38 | ##### EU 39 | 40 | * EU West 1 = `"eu-west-1"` 41 | * EU West 2 = `"eu-west-2"` 42 | * EU Central 1 = `"eu-central-1"` 43 | 44 | ##### AP 45 | 46 | * AP South 1 = `"ap-south-1"` 47 | * AP Southeast 1 = `"ap-southeast-1"` 48 | * AP Southeast 2 = `"ap-southeast-2"` 49 | * AP Northeast 1 = `"ap-northeast-1"` 50 | * AP Northeast 2 = `"ap-northeast-2"` 51 | 52 | ##### SA 53 | 54 | * SA East 1 = `"sa-east-1"` 55 | 56 | ### Usage 57 | **Note:** Check [S3SignerAWS-README.md](https://github.com/JustinM1/S3SignerAWS/blob/master/README.md) for a detailed explanation on usage and capabilities. 58 | 59 | VaporS3Signer makes it extremely easy to generate V4 auth headers and pre-signed URLs by adding an extension to `Droplet`. 60 | 61 | ##### V4 Auth Headers 62 | - All required headers for the request are created automatically, with the option to add more for individual use cases. 63 | 64 | ###### Get 65 | ```ruby 66 | drop.get("getS3TestImage") { req in 67 | let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)") 68 | 69 | guard let headers = try drop.s3Signer?.authHeaderV4( 70 | httpMethod: .get, 71 | urlString: urlString, 72 | headers: [:], 73 | payload: .none) else { throw Abort.serverError } 74 | 75 | let vaporHeaders = headers.vaporHeaders 76 | 77 | let resp = try self.drop.client.get(urlString, headers: vaporHeaders, query: [:]) 78 | } 79 | ``` 80 | 81 | ###### PUT 82 | ```ruby 83 | drop.post("users/image") { req in 84 | let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)") 85 | 86 | guard let payload = req.body.bytes, 87 | let headers = try self.drop.s3Signer?.authHeaderV4( 88 | httpMethod: .put, 89 | urlString: urlString, 90 | headers: [:], 91 | payload: Payload.bytes(payload)) else { throw Abort.serverError } 92 | 93 | let vaporHeaders = headers.vaporHeaders 94 | 95 | let resp = try self.drop.client.put( 96 | urlString, headers: 97 | vaporHeaders, query: [:], 98 | body: Body(payload)) 99 | } 100 | ``` 101 | 102 | ##### V4 Pre-Signed URL 103 | 104 | ###### Get 105 | ```ruby 106 | let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)") 107 | 108 | guard let presignedURLString = try drop.s3Signer?.presignedURLV4( 109 | httpMethod: .get, 110 | urlString: urlString, 111 | expiration: TimeFromNow.oneHour, 112 | headers: [:]) else { throw Abort.serverError } 113 | 114 | let resp = try self.drop.client.get( 115 | preSignedURLString, 116 | headers: [:], 117 | query: [:]) 118 | ``` 119 | ###### PUT 120 | ```ruby 121 | let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)") 122 | 123 | guard let payload = req.body.bytes, 124 | let preSignedURLString = try self.drop.s3Signer?.presignedURLV4( 125 | httpMethod: .put, 126 | urlString: urlString, 127 | expiration: .thirtyMinutes, 128 | headers: [:]) else { throw Abort.badReqest } 129 | 130 | let resp = try self.drop.client.put( 131 | preSignedURLString, 132 | headers: [:], 133 | query: [:], 134 | body: Body(payload)) 135 | ``` 136 | * `TimeFromNow` has three default lengths, `30 minutes, 1 hour, and 3 hours`. There is also a custom option which takes `Seconds`: `typealias for Int`. 137 | -------------------------------------------------------------------------------- /Sources/Dictionary+vaporHeaders.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import HTTP 3 | 4 | extension Dictionary where Key == String, Value == String { 5 | 6 | public var vaporHeaders: [HeaderKey: String] { 7 | var newHeaders: [HeaderKey: String] = [:] 8 | self.forEach { newHeaders.updateValue($0.value, forKey: HeaderKey($0.key)) } 9 | return newHeaders 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/DropletExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import S3SignerAWS 3 | import Vapor 4 | 5 | extension Droplet { 6 | 7 | /// Access currently stored S3Signer. 8 | public var s3Signer: S3SignerAWS? { 9 | get { 10 | return self.storage["s3Signer"] as? S3SignerAWS 11 | } 12 | set { 13 | if let val = newValue { 14 | self.storage["s3Signer"] = val 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Provider.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import S3SignerAWS 3 | import HTTP 4 | 5 | public final class Provider: Vapor.Provider { 6 | 7 | private static let configFileName: String = "vapor-S3Signer" 8 | public static let repositoryName: String = "vapor-S3Signer" 9 | 10 | public var s3Signer: S3SignerAWS 11 | 12 | public init(accessKey: String, secretKey: String, region: Region, securityToken: String? = nil) { 13 | self.s3Signer = S3SignerAWS(accessKey: accessKey, secretKey: secretKey, region: region, securityToken: securityToken) 14 | } 15 | 16 | public convenience init(config: Config) throws { 17 | 18 | guard config[Provider.configFileName] != nil else { 19 | throw S3ProviderError.config("no vapor-S3Signer.json config file") 20 | } 21 | 22 | guard let accessKey = config[Provider.configFileName, "accessKey"]?.string else { 23 | throw S3ProviderError.config("No 'accessKey' key in vapor-S3Signer.json config file.") 24 | } 25 | 26 | guard let secretKey = config[Provider.configFileName, "secretKey"]?.string else { 27 | throw S3ProviderError.config("No 'secretKey' key in vapor-S3Signer.json config file.") 28 | } 29 | 30 | guard let region = config[Provider.configFileName, "region"]?.string else { 31 | throw S3ProviderError.config("No 'region' key in vapor-S3Signer.json config file.") 32 | } 33 | 34 | guard let regionEnum = Region(rawValue: region) else { 35 | throw S3ProviderError.config("region name does not conform to any Region raw values. Check Region.swift for proper names.") 36 | } 37 | 38 | let token = config[Provider.configFileName, "securityToken"]?.string 39 | 40 | self.init(accessKey: accessKey, secretKey: secretKey, region: regionEnum, securityToken: token) 41 | } 42 | 43 | public func beforeRun(_ droplet: Droplet) throws { } 44 | 45 | public func boot(_ config: Config) throws { } 46 | 47 | public func boot(_ droplet: Droplet) throws { 48 | droplet.storage["s3Signer"] = self.s3Signer 49 | } 50 | 51 | public enum S3ProviderError: Swift.Error { 52 | case config(String) 53 | } 54 | } 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Tests/VaporS3SignerTests/VaporS3SignerTests.swift: -------------------------------------------------------------------------------- 1 | import HTTP 2 | import Vapor 3 | import XCTest 4 | @testable import VaporS3Signer 5 | 6 | class VaporS3SignerTests: XCTestCase { 7 | 8 | static var allTests : [(String, (VaporS3SignerTests) -> () throws -> Void)] { 9 | return [ 10 | ("testDropletExtension", testDropletExtension) 11 | ] 12 | } 13 | 14 | func testDropletExtension() { 15 | do { 16 | let config = try Config() 17 | 18 | try config.addProvider(VaporS3Signer.Provider( 19 | accessKey: "access", 20 | secretKey: "secret", 21 | region: .usWest1, 22 | securityToken: "token")) 23 | 24 | let drop = try Droplet(config) 25 | 26 | guard let signer = drop.s3Signer else { 27 | XCTFail("Droplet s3Signer should not be nil.") 28 | return 29 | } 30 | 31 | XCTAssertEqual(signer.region, .usWest1) 32 | 33 | } catch { 34 | XCTFail("Error caught during test: \(error)") 35 | } 36 | } 37 | 38 | func testVaporHeaders() { 39 | let headers: [String: String] = [ 40 | "headerKey1": "headerValue1", 41 | "headerKey2": "headerValue2" 42 | ] 43 | 44 | let expectedHeaders: [HeaderKey: String] = [ 45 | HeaderKey("headerKey1"): "headerValue1", 46 | HeaderKey("headerKey2"): "headerValue2" 47 | ] 48 | 49 | XCTAssertEqual(headers.vaporHeaders, expectedHeaders) 50 | } 51 | } 52 | --------------------------------------------------------------------------------