Skip to content
This repository has been archived by the owner on Apr 5, 2020. It is now read-only.

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinM authored and JustinM committed Sep 20, 2017
1 parent b1611ae commit ad98f95
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 95 deletions.
88 changes: 53 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@

[Vapor](https://vapor.codes/) Provider for [S3SignerAWS](https://github.com/JustinM1/S3SignerAWS)

Generates authorization headers and pre-signed URLs for authenticating AWS S3 REST API requests
* Supports `GET, PUT, DELETE`
Generates V4 authorization headers and pre-signed URLs for authenticating AWS S3 REST API requests
* Supports `DELETE/GET/HEAD/POST/PUT`

### Installation (SPM)
- Vapor Version 1
```ruby
.Package(url: "https://github.com/JustinM1/VaporS3Signer.git", majorVersion: 1)
```
- Vapor Version 2
```ruby
.Package(url: "https://github.com/JustinM1/VaporS3Signer.git", majorVersion: 2)
.Package(url: "https://github.com/JustinM1/VaporS3Signer.git", majorVersion: 3)
```

### Config File
Expand Down Expand Up @@ -60,30 +55,43 @@ Here are the names for each region:
VaporS3Signer makes it extremely easy to generate V4 auth headers and pre-signed URLs by adding an extension to `Droplet`.

##### V4 Auth Headers
- All required headers for the request are created automatically, with the option to add more for individual use cases.
- All required headers for the request are created automatically, with the option to add more for individual use cases.

###### Get
```ruby
let drop = Droplet()
try drop.addProvider(VaporS3Signer.Provider.self)
drop.get("getS3TestImage") { req in
let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)")
guard let headers = try drop.s3Signer?.authHeaderV4(httpMethod: .get, urlString: urlString, headers: [:], payload: .none) else { throw Abort.serverError }
var vaporHeaders: [HeaderKey: String] = [:]
headers.forEach { vaporHeaders.updateValue($0.value, forKey: HeaderKey($0.key)) }
let resp = try self.drop.client.get(urlString, headers: vaporHeaders, query: [:])
guard let headers = try drop.s3Signer?.authHeaderV4(
httpMethod: .get,
urlString: urlString,
headers: [:],
payload: .none) else { throw Abort.serverError }
let vaporHeaders = headers.vaporHeaders
let resp = try self.drop.client.get(urlString, headers: vaporHeaders, query: [:])
}
```

###### PUT
```ruby
drop.post("users/image") { req in
let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)")
guard let payload = req.body.bytes, let headers = try self.drop.s3Signer?.authHeaderV4(httpMethod: .put, urlString: urlString, headers: [:], payload: Payload.bytes(payload)) else { throw Abort.serverError }
var vaporHeaders: [HeaderKey: String] = [:]
headers.forEach { vaporHeaders.updateValue($0.value, forKey: HeaderKey($0.key)) }
let resp = try self.drop.client.put(urlString, headers: vaporHeaders, query: [:], body: Body(payload))
guard let payload = req.body.bytes,
let headers = try self.drop.s3Signer?.authHeaderV4(
httpMethod: .put,
urlString: urlString,
headers: [:],
payload: Payload.bytes(payload)) else { throw Abort.serverError }
let vaporHeaders = headers.vaporHeaders
let resp = try self.drop.client.put(
urlString, headers:
vaporHeaders, query: [:],
body: Body(payload))
}
```

Expand All @@ -92,23 +100,33 @@ drop.post("users/image") { req in
###### Get
```ruby
let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)")
guard let presignedURL = try drop.s3Signer?.presignedURLV4(httpMethod: .get, urlString: urlString,
expiration: TimeFromNow.oneHour, headers: [:]), let url = URL(string: presignedURL.urlString) else { throw Abort.serverError }
let resp = try self.drop.client.get(preSignedURL.urlString, headers: [:], query: [:])
guard let presignedURLString = try drop.s3Signer?.presignedURLV4(
httpMethod: .get,
urlString: urlString,
expiration: TimeFromNow.oneHour,
headers: [:]) else { throw Abort.serverError }
let resp = try self.drop.client.get(
preSignedURLString,
headers: [:],
query: [:])
```
###### PUT
```ruby
drop.post("user/images") { req in
guard let payload = req.body.bytes, let preSignedURL = try self.drop.s3Signer?.presignedURLV4(httpMethod: .put, urlString: urlString, expiration: .thirtyMinutes, headers: [:]) else { throw Abort.badReqest }
let resp = try self.drop.client.put(preSignedURL.urlString, headers: [:], query: [:], body: Body(payload))
}
let urlString = "https://" + Region.usEast1_Virginia.host.appending("S3bucketname/users/\(someUserId)")
guard let payload = req.body.bytes,
let preSignedURLString = try self.drop.s3Signer?.presignedURLV4(
httpMethod: .put,
urlString: urlString,
expiration: .thirtyMinutes,
headers: [:]) else { throw Abort.badReqest }
let resp = try self.drop.client.put(
preSignedURLString,
headers: [:],
query: [:],
body: Body(payload))
```
* `TimeFromNow` has three default lengths, `30 minutes, 1 hour, and 3 hours`. There is also a custom option which takes `Seconds`: `typealias for Int`.

### Motivation

Found it quite painful to satisfy AWS S3 auth requirements, hoping to save others from some of that pain and suffering. Enjoy!

### Acknowledgements

Thanks [Tanner Nelson](https://github.com/tannernelson), [Logan Wright](https://github.com/LoganWright) and everyone in the [Vapor Slack Channel](http://vapor.team/)
11 changes: 11 additions & 0 deletions Sources/Dictionary+vaporHeaders.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Vapor
import HTTP

extension Dictionary where Key == String, Value == String {

var vaporHeaders: [HeaderKey: String] {
var newHeaders: [HeaderKey: String] = [:]
self.forEach { newHeaders.updateValue($0.value, forKey: HeaderKey($0.key)) }
return newHeaders
}
}
10 changes: 2 additions & 8 deletions Sources/DropletExtension.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
//
// DropletExtension.swift
// VaporS3Signer
//
// Created by Justin M. on 10/25/16.
//
//

import Foundation
import S3SignerAWS
import Vapor

extension Droplet {

/// Access currently stored S3Signer.
public var s3Signer: S3SignerAWS? {
get {
return self.storage["s3Signer"] as? S3SignerAWS
Expand Down
99 changes: 47 additions & 52 deletions Sources/Provider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,55 @@ import Vapor
import S3SignerAWS
import HTTP


public final class Provider: Vapor.Provider {

public static let repositoryName: String = "vapor-S3Signer"
private static let configFileName: String = "vapor-S3Signer"

public var s3Signer: S3SignerAWS

public init(accessKey: String, secretKey: String, region: Region, securityToken: String? = nil) {
self.s3Signer = S3SignerAWS(accessKey: accessKey, secretKey: secretKey, region: region, securityToken: securityToken)
}



public convenience init(config: Config) throws {

guard config[Provider.configFileName] != nil else {
throw S3ProviderError.config("no vapor-S3Signer.json config file")
}

guard let accessKey = config[Provider.configFileName, "accessKey"]?.string else {
throw S3ProviderError.config("No 'accessKey' key in vapor-S3Signer.json config file.")
}

guard let secretKey = config[Provider.configFileName, "secretKey"]?.string else {
throw S3ProviderError.config("No 'secretKey' key in vapor-S3Signer.json config file.")
}

guard let region = config[Provider.configFileName, "region"]?.string else {
throw S3ProviderError.config("No 'region' key in vapor-S3Signer.json config file.")
}

guard let regionEnum = Region(rawValue: region) else {
throw S3ProviderError.config("region name does not conform to any Region raw values. Check Region.swift for proper names.")
}

let token = config[Provider.configFileName, "securityToken"]?.string

self.init(accessKey: accessKey, secretKey: secretKey, region: regionEnum, securityToken: token)

}


public func boot(_ config: Config) throws { }

public func boot(_ droplet: Droplet) throws {
droplet.storage["s3Signer"] = self.s3Signer
}

public func beforeRun(_ droplet: Droplet) throws { }

public enum S3ProviderError: Swift.Error {
case config(String)
}
private static let configFileName: String = "vapor-S3Signer"
public static let repositoryName: String = "vapor-S3Signer"

public var s3Signer: S3SignerAWS

public init(accessKey: String, secretKey: String, region: Region, securityToken: String? = nil) {
self.s3Signer = S3SignerAWS(accessKey: accessKey, secretKey: secretKey, region: region, securityToken: securityToken)
}

public convenience init(config: Config) throws {

guard config[Provider.configFileName] != nil else {
throw S3ProviderError.config("no vapor-S3Signer.json config file")
}

guard let accessKey = config[Provider.configFileName, "accessKey"]?.string else {
throw S3ProviderError.config("No 'accessKey' key in vapor-S3Signer.json config file.")
}

guard let secretKey = config[Provider.configFileName, "secretKey"]?.string else {
throw S3ProviderError.config("No 'secretKey' key in vapor-S3Signer.json config file.")
}

guard let region = config[Provider.configFileName, "region"]?.string else {
throw S3ProviderError.config("No 'region' key in vapor-S3Signer.json config file.")
}

guard let regionEnum = Region(rawValue: region) else {
throw S3ProviderError.config("region name does not conform to any Region raw values. Check Region.swift for proper names.")
}

let token = config[Provider.configFileName, "securityToken"]?.string

self.init(accessKey: accessKey, secretKey: secretKey, region: regionEnum, securityToken: token)
}

public func beforeRun(_ droplet: Droplet) throws { }

public func boot(_ config: Config) throws { }

public func boot(_ droplet: Droplet) throws {
droplet.storage["s3Signer"] = self.s3Signer
}

public enum S3ProviderError: Swift.Error {
case config(String)
}
}


Expand Down
15 changes: 15 additions & 0 deletions Tests/VaporS3SignerTests/VaporS3SignerTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import HTTP
import Vapor
import XCTest
@testable import VaporS3Signer
Expand Down Expand Up @@ -33,4 +34,18 @@ class VaporS3SignerTests: XCTestCase {
XCTFail("Error caught during test: \(error)")
}
}

func testVaporHeaders() {
let headers: [String: String] = [
"headerKey1": "headerValue1",
"headerKey2": "headerValue2"
]

let expectedHeaders: [HeaderKey: String] = [
HeaderKey("headerKey1"): "headerValue1",
HeaderKey("headerKey2"): "headerValue2"
]

XCTAssertEqual(headers.vaporHeaders, expectedHeaders)
}
}

0 comments on commit ad98f95

Please sign in to comment.