├── .circleci └── config.yml ├── .codebeatignore ├── .codecov.yml ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── Package.swift ├── Package@swift-4.swift ├── README.md ├── Sources ├── AWS │ └── AWS.swift ├── AWSSignatureV4 │ ├── AWSSignatureV4.swift │ ├── ErrorParser │ │ ├── AWSError.swift │ │ ├── ErrorParser+Grammar.swift │ │ ├── ErrorParser.swift │ │ ├── Scanner.swift │ │ └── Trie.swift │ ├── Payload.swift │ ├── PercentEncoder.swift │ └── Region.swift ├── AutoScaling │ └── AutoScaling.swift ├── EC2 │ └── EC2.swift ├── S3 │ └── S3.swift └── VaporS3 │ ├── Provider.swift │ └── VaporS3Error.swift └── Tests ├── AWSTests ├── AWSTests.swift ├── AutoscalingTests.swift ├── ErrorParserTests.swift ├── SignatureTestSuite.swift └── Utilities │ └── SignerResult.swift └── LinuxMain.swift /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | MacOS: 4 | macos: 5 | xcode: "9.0" 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v1-spm-deps-{{ checksum "Package.swift" }} 11 | - run: 12 | name: Install CMySQL and CTLS 13 | command: | 14 | brew tap vapor/homebrew-tap 15 | brew install cmysql 16 | brew install ctls 17 | - run: 18 | name: Build and Run Tests 19 | no_output_timeout: 1800 20 | command: | 21 | swift package generate-xcodeproj --enable-code-coverage 22 | xcodebuild -scheme AWS-Package -enableCodeCoverage YES test | xcpretty 23 | - run: 24 | name: Report coverage to Codecov 25 | command: | 26 | bash <(curl -s https://codecov.io/bash) 27 | - save_cache: 28 | key: v1-spm-deps-{{ checksum "Package.swift" }} 29 | paths: 30 | - .build 31 | Linux: 32 | docker: 33 | - image: brettrtoomey/vapor-ci:0.0.1 34 | steps: 35 | - checkout 36 | - restore_cache: 37 | keys: 38 | - v2-spm-deps-{{ checksum "Package.swift" }} 39 | - run: 40 | name: Copy Package file 41 | command: cp Package.swift res 42 | - run: 43 | name: Build and Run Tests 44 | no_output_timeout: 1800 45 | command: | 46 | swift test -Xswiftc -DNOJSON 47 | - run: 48 | name: Restoring Package file 49 | command: mv res Package.swift 50 | - save_cache: 51 | key: v2-spm-deps-{{ checksum "Package.swift" }} 52 | paths: 53 | - .build 54 | workflows: 55 | version: 2 56 | build-and-test: 57 | jobs: 58 | - MacOS 59 | - Linux 60 | experimental: 61 | notify: 62 | branches: 63 | only: 64 | - master 65 | - develop 66 | -------------------------------------------------------------------------------- /.codebeatignore: -------------------------------------------------------------------------------- 1 | Public/** 2 | Resources/Assets/** 3 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "0...100" 3 | ignore: 4 | - "Tests" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .DS_Store 3 | *.xcodeproj 4 | Packages/ 5 | Package.pins 6 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | function_body_length: 4 | warning: 60 5 | variable_name: 6 | min_length: 7 | warning: 2 8 | line_length: 80 9 | disabled_rules: 10 | - opening_brace 11 | colon: 12 | flexible_right_spacing: true 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Nodes 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. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "AWS", 5 | targets: [ 6 | Target(name: "AWS", dependencies: ["AutoScaling", "EC2", "S3"]), 7 | Target(name: "AutoScaling", dependencies: ["AWSSignatureV4"]), 8 | Target(name: "EC2", dependencies: ["AWSSignatureV4"]), 9 | Target(name: "S3", dependencies: ["AWSSignatureV4"]), 10 | Target(name: "VaporS3", dependencies: ["S3"]), 11 | ], 12 | dependencies: [ 13 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2), 14 | .Package(url: "https://github.com/drmohundro/SWXMLHash", majorVersion: 3), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /Package@swift-4.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AWS", 7 | products: [ 8 | .library(name: "AWS", targets: ["AWS"]), 9 | .library(name: "VaporS3", targets: ["VaporS3"]), 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/vapor/vapor.git", from: "2.2.0"), 13 | .package(url: "https://github.com/drmohundro/SWXMLHash", from: "4.1.1"), 14 | ], 15 | targets: [ 16 | .target(name: "AWS", dependencies: ["AutoScaling", "EC2", "S3"]), 17 | .target(name: "AutoScaling", dependencies: ["AWSSignatureV4", "SWXMLHash"]), 18 | .target(name: "AWSSignatureV4", dependencies: ["Vapor"]), 19 | .target(name: "EC2", dependencies: ["AWSSignatureV4"]), 20 | .target(name: "S3", dependencies: ["AWSSignatureV4"]), 21 | .target(name: "VaporS3", dependencies: ["S3"]), 22 | .testTarget(name: "AWSTests", dependencies: ["AWS"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS 2 | [![Swift Version](https://img.shields.io/badge/Swift-3-brightgreen.svg)](http://swift.org) 3 | [![Vapor Version](https://img.shields.io/badge/Vapor-2-F6CBCA.svg)](http://vapor.codes) 4 | [![Circle CI](https://circleci.com/gh/nodes-vapor/aws/tree/master.svg?style=shield)](https://circleci.com/gh/nodes-vapor/aws) 5 | [![codebeat badge](https://codebeat.co/badges/255e7772-28ec-4695-bdd5-770cfd676d9c)](https://codebeat.co/projects/github-com-nodes-vapor-aws-master) 6 | [![codecov](https://codecov.io/gh/nodes-vapor/aws/branch/master/graph/badge.svg)](https://codecov.io/gh/nodes-vapor/aws) 7 | [![Readme Score](http://readme-score-api.herokuapp.com/score.svg?url=https://github.com/nodes-vapor/aws)](http://clayallsopp.github.io/readme-score?url=https://github.com/nodes-vapor/aws) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nodes-vapor/aws/master/LICENSE) 9 | 10 | 11 | This package makes it easy to use AWS resources from Swift. 12 | 13 | ## 📦 Installation 14 | 15 | Update your `Package.swift` file. 16 | ```swift 17 | .Package(url: "https://github.com/nodes-vapor/aws.git", majorVersion: 1) 18 | ``` 19 | 20 | 21 | ## Getting started 🚀 22 | 23 | Currently the following AWS Services are available: 24 | - EC2 25 | - S3 26 | 27 | If you need other resources you can use Raw call, to call the AWS API directly. 28 | 29 | ### EC2 30 | 31 | **Describe instances** 32 | 33 | ```swift 34 | do { 35 | let instances = try EC2( 36 | accessKey: "my-key", 37 | secretKey: "my-secret", 38 | region: "my-region" 39 | ).describeInstances() 40 | } catch { 41 | 42 | } 43 | ``` 44 | 45 | ### S3 46 | 47 | **Upload a file to S3** 48 | 49 | ```swift 50 | do { 51 | try S3( 52 | accessKey: "my-key", 53 | secretKey: "my-secret", 54 | region: "my-region", 55 | bucket: "my-s3-bucket" 56 | ).uploadFile("/path/to/local/file", "/folder/in/s3/bucket") 57 | } catch { 58 | 59 | } 60 | ``` 61 | 62 | ### Raw call 63 | 64 | If you need a resource not made in one of the functions, you can use the system to call the AWS API directly. 65 | 66 | **Describe instances example** 67 | 68 | ```swift 69 | do { 70 | return try CallAWS().call( 71 | method: "GET", 72 | service: "ec2", 73 | host: "ec2.amazonaws.com", 74 | region: "my-region", 75 | baseURL: "https://ec2.amazonaws.com", 76 | key: "my-key", 77 | secret: "my-secret", 78 | requestParam: "Action=DescribeInstances" 79 | ) 80 | } catch { 81 | 82 | } 83 | ``` 84 | 85 | ## 📃 Development 86 | 87 | If you want to improve this, you'll need to make sure you're making a copy of OpenSSL available to `swift build` and the toolchain. If you use Xcode, something like the following after `brew install openssl` will work: 88 | 89 | ``` 90 | swift package -Xswiftc -I/usr/local/Cellar/openssl/1.0.2j/include -Xlinker -L/usr/local/Cellar/openssl/1.0.2j/lib/ generate-xcodeproj 91 | ``` 92 | 93 | ## 🏆 Credits 94 | 95 | This package is developed and maintained by the Vapor team at [Nodes](https://www.nodesagency.com). 96 | The package owner for this project is [Brett](https://github.com/brettRToomey). 97 | 98 | 99 | ## 📄 License 100 | 101 | This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 102 | -------------------------------------------------------------------------------- /Sources/AWS/AWS.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/aws/5ce8efa3d789531ddb4513ef8109748cc18a0c0a/Sources/AWS/AWS.swift -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/AWSSignatureV4.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | import Crypto 3 | import HTTP 4 | import Foundation 5 | 6 | public enum AccessControlList: String { 7 | case privateAccess = "private" 8 | case publicRead = "public-read" 9 | case publicReadWrite = "public-read-write" 10 | case awsExecRead = "aws-exec-read" 11 | case authenticatedRead = "authenticated-read" 12 | case bucketOwnerRead = "bucket-owner-read" 13 | case bucketOwnerFullControl = "bucket-owner-full-control" 14 | } 15 | 16 | public struct AWSSignatureV4 { 17 | public enum Method: String { 18 | case delete = "DELETE" 19 | case get = "GET" 20 | case post = "POST" 21 | case put = "PUT" 22 | } 23 | 24 | let service: String 25 | let host: String 26 | let region: String 27 | let accessKey: String 28 | let secretKey: String 29 | let contentType = "application/x-www-form-urlencoded; charset=utf-8" 30 | 31 | internal var unitTestDate: Date? 32 | 33 | var amzDate: String { 34 | let dateFormatter = DateFormatter() 35 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 36 | dateFormatter.dateFormat = "YYYYMMdd'T'HHmmss'Z'" 37 | return dateFormatter.string(from: unitTestDate ?? Date()) 38 | } 39 | 40 | public init( 41 | service: String, 42 | host: String, 43 | region: Region, 44 | accessKey: String, 45 | secretKey: String 46 | ) { 47 | self.service = service 48 | self.host = host 49 | self.region = region.rawValue 50 | self.accessKey = accessKey 51 | self.secretKey = secretKey 52 | } 53 | 54 | func getStringToSign( 55 | algorithm: String, 56 | date: String, 57 | scope: String, 58 | canonicalHash: String 59 | ) -> String { 60 | return [ 61 | algorithm, 62 | date, 63 | scope, 64 | canonicalHash 65 | ].joined(separator: "\n") 66 | } 67 | 68 | func getSignature(_ stringToSign: String) throws -> String { 69 | let dateHMAC = try HMAC(.sha256, dateStamp()).authenticate(key: "AWS4\(secretKey)") 70 | let regionHMAC = try HMAC(.sha256, region).authenticate(key: dateHMAC) 71 | let serviceHMAC = try HMAC(.sha256, service).authenticate(key: regionHMAC) 72 | let signingHMAC = try HMAC(.sha256, "aws4_request").authenticate(key: serviceHMAC) 73 | 74 | let signature = try HMAC(.sha256, stringToSign).authenticate(key: signingHMAC) 75 | return signature.hexString 76 | } 77 | 78 | func getCredentialScope() -> String { 79 | return [ 80 | dateStamp(), 81 | region, 82 | service, 83 | "aws4_request" 84 | ].joined(separator: "/") 85 | } 86 | 87 | func getCanonicalRequest( 88 | payloadHash: String, 89 | method: Method, 90 | path: String, 91 | query: String, 92 | canonicalHeaders: String, 93 | signedHeaders: String 94 | ) throws -> String { 95 | let path = try path.percentEncode(allowing: Byte.awsPathAllowed) 96 | let query = try query.percentEncode(allowing: Byte.awsQueryAllowed) 97 | 98 | return [ 99 | method.rawValue, 100 | path, 101 | query, 102 | canonicalHeaders, 103 | "", 104 | signedHeaders, 105 | payloadHash 106 | ].joined(separator: "\n") 107 | } 108 | 109 | func dateStamp() -> String { 110 | let date = unitTestDate ?? Date() 111 | let dateFormatter = DateFormatter() 112 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 113 | dateFormatter.dateFormat = "YYYYMMdd" 114 | return dateFormatter.string(from: date) 115 | } 116 | } 117 | 118 | extension AWSSignatureV4 { 119 | func generateHeadersToSign( 120 | headers: inout [String: String], 121 | host: String, 122 | hash: String 123 | ) { 124 | headers["Host"] = host 125 | headers["X-Amz-Date"] = amzDate 126 | 127 | if hash != "UNSIGNED-PAYLOAD" { 128 | headers["x-amz-content-sha256"] = hash 129 | } 130 | } 131 | 132 | func alphabetize(_ dict: [String : String]) -> [(key: String, value: String)] { 133 | return dict.sorted(by: { $0.0.lowercased() < $1.0.lowercased() }) 134 | } 135 | 136 | func createCanonicalHeaders(_ headers: [(key: String, value: String)]) -> String { 137 | return headers.map { 138 | "\($0.key.lowercased()):\($0.value)" 139 | }.joined(separator: "\n") 140 | } 141 | 142 | func createAuthorizationHeader( 143 | algorithm: String, 144 | credentialScope: String, 145 | signature: String, 146 | signedHeaders: String 147 | ) -> String { 148 | return "\(algorithm) Credential=\(accessKey)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signature)" 149 | } 150 | } 151 | 152 | extension AWSSignatureV4 { 153 | /** 154 | Sign a request to be sent to an AWS API. 155 | 156 | - returns: 157 | A dictionary with headers to attach to a request 158 | 159 | - parameters: 160 | - payload: A hash of this data will be included in the headers 161 | - method: Type of HTTP request 162 | - path: API call being referenced 163 | - query: Additional querystring in key-value format ("?key=value&key2=value2") 164 | - headers: HTTP headers added to the request 165 | */ 166 | public func sign( 167 | payload: Payload = .none, 168 | method: Method = .get, 169 | path: String, 170 | query: String? = nil, 171 | headers: [String : String] = [:] 172 | ) throws -> [HeaderKey : String] { 173 | let algorithm = "AWS4-HMAC-SHA256" 174 | let credentialScope = getCredentialScope() 175 | let payloadHash = try payload.hashed() 176 | 177 | var headers = headers 178 | 179 | generateHeadersToSign(headers: &headers, host: host, hash: payloadHash) 180 | 181 | let sortedHeaders = alphabetize(headers) 182 | let signedHeaders = sortedHeaders.map { $0.key.lowercased() }.joined(separator: ";") 183 | let canonicalHeaders = createCanonicalHeaders(sortedHeaders) 184 | 185 | // Task 1 is the Canonical Request 186 | let canonicalRequest = try getCanonicalRequest( 187 | payloadHash: payloadHash, 188 | method: method, 189 | path: path, 190 | query: query ?? "", 191 | canonicalHeaders: canonicalHeaders, 192 | signedHeaders: signedHeaders 193 | ) 194 | 195 | let canonicalHash = try Hash.make(.sha256, canonicalRequest).hexString 196 | 197 | // Task 2 is the String to Sign 198 | let stringToSign = getStringToSign( 199 | algorithm: algorithm, 200 | date: amzDate, 201 | scope: credentialScope, 202 | canonicalHash: canonicalHash 203 | ) 204 | 205 | // Task 3 calculates Signature 206 | let signature = try getSignature(stringToSign) 207 | 208 | //Task 4 Add signing information to the request 209 | let authorizationHeader = createAuthorizationHeader( 210 | algorithm: algorithm, 211 | credentialScope: credentialScope, 212 | signature: signature, 213 | signedHeaders: signedHeaders 214 | ) 215 | 216 | var requestHeaders: [HeaderKey: String] = [ 217 | "X-Amz-Date": amzDate, 218 | "Content-Type": contentType, 219 | "x-amz-content-sha256": payloadHash, 220 | "Authorization": authorizationHeader, 221 | "Host": self.host 222 | ] 223 | 224 | headers.forEach { key, value in 225 | let headerKey = HeaderKey(stringLiteral: key) 226 | requestHeaders[headerKey] = value 227 | } 228 | 229 | return requestHeaders 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/ErrorParser/AWSError.swift: -------------------------------------------------------------------------------- 1 | public enum AWSError: String { 2 | case accessDenied = "AccessDenied" 3 | case accountProblem = "AccountProblem" 4 | case ambiguousGrantByEmailAddress = "AmbiguousGrantByEmailAddress" 5 | case authorizationHeaderMalformed = "AuthorizationHeaderMalformed" 6 | case badDigest = "BadDigest" 7 | case bucketAlreadyExists = "BucketAlreadyExists" 8 | case bucketAlreadyOwnedByYou = "BucketAlreadyOwnedByYou" 9 | case bucketNotEmpty = "BucketNotEmpty" 10 | case credentialsNotSupported = "CredentialsNotSupported" 11 | case crossLocationLoggingProhibited = "CrossLocationLoggingProhibited" 12 | case entityTooSmall = "EntityTooSmall" 13 | case entityTooLarge = "EntityTooLarge" 14 | case expiredToken = "ExpiredToken" 15 | case illegalVersioningConfigurationException = "IllegalVersioningConfigurationException" 16 | case incompleteBody = "IncompleteBody" 17 | case incorrectNumberOfFilesInPostRequest = "IncorrectNumberOfFilesInPostRequest" 18 | case inlineDataTooLarge = "InlineDataTooLarge" 19 | case internalError = "InternalError" 20 | case invalidAccessKeyId = "InvalidAccessKeyId" 21 | case invalidAddressingHeader = "InvalidAddressingHeader" 22 | case invalidArgument = "InvalidArgument" 23 | case invalidBucketName = "InvalidBucketName" 24 | case invalidDigest = "InvalidDigest" 25 | case invalidEncryptionAlgorithmError = "InvalidEncryptionAlgorithmError" 26 | case invalidLocationConstraint = "InvalidLocationConstraint" 27 | case invalidObjectState = "InvalidObjectState" 28 | case invalidPart = "InvalidPart" 29 | case invalidPartOrder = "InvalidPartOrder" 30 | case invalidPayer = "InvalidPayer" 31 | case invalidPolicyDocument = "InvalidPolicyDocument" 32 | case invalidRange = "InvalidRange" 33 | case invalidRequest = "InvalidRequest" 34 | case invalidSecurity = "InvalidSecurity" 35 | case invalidSOAPRequest = "InvalidSOAPRequest" 36 | case invalidStorageClass = "InvalidStorageClass" 37 | case invalidTargetBucketForLogging = "InvalidTargetBucketForLogging" 38 | case invalidToken = "InvalidToken" 39 | case invalidURI = "InvalidURI" 40 | case keyTooLong = "KeyTooLong" 41 | case malformedACLError = "MalformedACLError" 42 | case malformedPOSTRequest = "MalformedPOSTRequest" 43 | case malformedXML = "MalformedXML" 44 | case maxMessageLengthExceeded = "MaxMessageLengthExceeded" 45 | case maxPostPreDataLengthExceededError = "MaxPostPreDataLengthExceededError" 46 | case metadataTooLarge = "MetadataTooLarge" 47 | case methodNotAllowed = "MethodNotAllowed" 48 | case missingAttachment = "MissingAttachment" 49 | case missingContentLength = "MissingContentLength" 50 | case missingRequestBodyError = "MissingRequestBodyError" 51 | case missingSecurityElement = "MissingSecurityElement" 52 | case missingSecurityHeader = "MissingSecurityHeader" 53 | case noLoggingStatusForKey = "NoLoggingStatusForKey" 54 | case noSuchBucket = "NoSuchBucket" 55 | case noSuchKey = "NoSuchKey" 56 | case noSuchLifecycleConfiguration = "NoSuchLifecycleConfiguration" 57 | case noSuchUpload = "NoSuchUpload" 58 | case noSuchVersion = "NoSuchVersion" 59 | case notImplemented = "NotImplemented" 60 | case notSignedUp = "NotSignedUp" 61 | case noSuchBucketPolicy = "NoSuchBucketPolicy" 62 | case operationAborted = "OperationAborted" 63 | case peramentRedirect = "PeramentRedirect" 64 | case preconditionFailed = "PreconditionFailed" 65 | case redirect = "Redirect" 66 | case restoreAlreadyInProgress = "RestoreAlreadyInProgress" 67 | case requestIsNotMultiPartContent = "RequestIsNotMultiPartContent" 68 | case requestTimeout = "RequestTimeout" 69 | case requestTimeTooSkewed = "RequestTimeTooSkewed" 70 | case requestTorrentOfBucketError = "RequestTorrentOfBucketError" 71 | case signatureDoesNotMatch = "SignatureDoesNotMatch" 72 | case serviceUnavailable = "ServiceUnavailable" 73 | case slowDown = "SlowDown" 74 | case temporaryRedirect = "TemporaryRedirect" 75 | case tokenRefreshRequired = "TokenRefreshRequired" 76 | case tooManyBuckets = "TooManyBuckets" 77 | case unexpectedContent = "UnexpectedContent" 78 | case unresolvableGrantByEmailAddress = "UnresolvableGrantByEmailAddress" 79 | case userKeyMustBeSpecified = "UserKeyMustBeSpecified" 80 | } 81 | 82 | extension AWSError: Error { 83 | } 84 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/ErrorParser/ErrorParser+Grammar.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | 3 | extension ErrorParser { 4 | static let awsGrammar: Trie = { 5 | let trie = Trie() 6 | 7 | insert(into: trie, .accessDenied) 8 | insert(into: trie, .accountProblem) 9 | insert(into: trie, .ambiguousGrantByEmailAddress) 10 | insert(into: trie, .authorizationHeaderMalformed) 11 | insert(into: trie, .badDigest) 12 | insert(into: trie, .bucketAlreadyExists) 13 | insert(into: trie, .bucketAlreadyOwnedByYou) 14 | insert(into: trie, .bucketNotEmpty) 15 | insert(into: trie, .credentialsNotSupported) 16 | insert(into: trie, .crossLocationLoggingProhibited) 17 | insert(into: trie, .entityTooSmall) 18 | insert(into: trie, .entityTooLarge) 19 | insert(into: trie, .expiredToken) 20 | insert(into: trie, .illegalVersioningConfigurationException) 21 | insert(into: trie, .incompleteBody) 22 | insert(into: trie, .incorrectNumberOfFilesInPostRequest) 23 | insert(into: trie, .inlineDataTooLarge) 24 | insert(into: trie, .internalError) 25 | insert(into: trie, .invalidAccessKeyId) 26 | insert(into: trie, .invalidAddressingHeader) 27 | insert(into: trie, .invalidArgument) 28 | insert(into: trie, .invalidBucketName) 29 | insert(into: trie, .invalidDigest) 30 | insert(into: trie, .invalidEncryptionAlgorithmError) 31 | insert(into: trie, .invalidLocationConstraint) 32 | insert(into: trie, .invalidObjectState) 33 | insert(into: trie, .invalidPart) 34 | insert(into: trie, .invalidPartOrder) 35 | insert(into: trie, .invalidPayer) 36 | insert(into: trie, .invalidPolicyDocument) 37 | insert(into: trie, .invalidRange) 38 | insert(into: trie, .invalidRequest) 39 | insert(into: trie, .invalidSecurity) 40 | insert(into: trie, .invalidSOAPRequest) 41 | insert(into: trie, .invalidStorageClass) 42 | insert(into: trie, .invalidTargetBucketForLogging) 43 | insert(into: trie, .invalidToken) 44 | insert(into: trie, .invalidURI) 45 | insert(into: trie, .keyTooLong) 46 | insert(into: trie, .malformedACLError) 47 | insert(into: trie, .malformedPOSTRequest) 48 | insert(into: trie, .malformedXML) 49 | insert(into: trie, .maxMessageLengthExceeded) 50 | insert(into: trie, .maxPostPreDataLengthExceededError) 51 | insert(into: trie, .metadataTooLarge) 52 | insert(into: trie, .methodNotAllowed) 53 | insert(into: trie, .missingAttachment) 54 | insert(into: trie, .missingContentLength) 55 | insert(into: trie, .missingRequestBodyError) 56 | insert(into: trie, .missingSecurityElement) 57 | insert(into: trie, .missingSecurityHeader) 58 | insert(into: trie, .noLoggingStatusForKey) 59 | insert(into: trie, .noSuchBucket) 60 | insert(into: trie, .noSuchKey) 61 | insert(into: trie, .noSuchLifecycleConfiguration) 62 | insert(into: trie, .noSuchUpload) 63 | insert(into: trie, .noSuchVersion) 64 | insert(into: trie, .notImplemented) 65 | insert(into: trie, .notSignedUp) 66 | insert(into: trie, .noSuchBucketPolicy) 67 | insert(into: trie, .operationAborted) 68 | insert(into: trie, .peramentRedirect) 69 | insert(into: trie, .preconditionFailed) 70 | insert(into: trie, .redirect) 71 | insert(into: trie, .restoreAlreadyInProgress) 72 | insert(into: trie, .requestIsNotMultiPartContent) 73 | insert(into: trie, .requestTimeout) 74 | insert(into: trie, .requestTimeTooSkewed) 75 | insert(into: trie, .requestTorrentOfBucketError) 76 | insert(into: trie, .signatureDoesNotMatch) 77 | insert(into: trie, .serviceUnavailable) 78 | insert(into: trie, .slowDown) 79 | insert(into: trie, .temporaryRedirect) 80 | insert(into: trie, .tokenRefreshRequired) 81 | insert(into: trie, .tooManyBuckets) 82 | insert(into: trie, .unexpectedContent) 83 | insert(into: trie, .unresolvableGrantByEmailAddress) 84 | insert(into: trie, .userKeyMustBeSpecified) 85 | 86 | return trie 87 | }() 88 | 89 | static func insert(into trie: Trie, _ error: AWSError) { 90 | trie.insert(error, for: error.rawValue.makeBytes()) 91 | } 92 | } 93 | 94 | /* 95 | case accessDenied 96 | case accountProblem 97 | case ambiguousGrantByEmailAddress 98 | case badDigest 99 | case bucketAlreadyExists 100 | case bucketAlreadyOwnedByYou 101 | case bucketNotEmpty 102 | case credentialsNotSupported 103 | case crossLocationLoggingProhibited 104 | case entityTooSmall 105 | case entityTooLarge 106 | case expiredToken 107 | case illegalVersioningConfigurationException 108 | case incompleteBody 109 | case incorrectNumberOfFilesInPostRequest 110 | case inlineDataTooLarge 111 | case internalError 112 | case invalidAccessKeyId 113 | case invalidAddressingHeader 114 | case invalidArgument 115 | case invalidBucketName 116 | case invalidDigest 117 | case invalidEncryptionAlgorithmError 118 | case invalidLocationConstraint 119 | case invalidObjectState 120 | case invalidPart 121 | case invalidPartOrder 122 | case invalidPayer 123 | case invalidPolicyDocument 124 | case invalidRange 125 | case invalidRequest 126 | case invalidSecurity 127 | case invalidSOAPRequest 128 | case invalidStorageClass 129 | case invalidTargetBucketForLogging 130 | case invalidToken 131 | case invalidURI 132 | case keyTooLong 133 | case malformedACLError 134 | case malformedPOSTRequest 135 | case malformedXML 136 | case maxMessageLengthExceeded 137 | case maxPostPreDataLengthExceededError 138 | case metadataTooLarge 139 | case methodNotAllowed 140 | case missingAttachment 141 | case missingContentLength 142 | case missingRequestBodyError 143 | case missingSecurityElement 144 | case missingSecurityHeader 145 | case noLoggingStatusForKey 146 | case noSuchBucket 147 | case noSuchKey 148 | case noSuchLifecycleConfiguration 149 | case noSuchUpload 150 | case noSuchVersion 151 | case notImplemented 152 | case notSignedUp 153 | case noSuchBucketPolicy 154 | case operationAborted 155 | case peramentRedirect 156 | case preconditionFailed 157 | case redirect 158 | case restoreAlreadyInProgress 159 | case requestIsNotMultiPartContent 160 | case requestTimeout 161 | case requestTimeTooSkewed 162 | case requestTorrentOfBucketError 163 | case signatureDoesNotMatch 164 | case serviceUnavailable 165 | case slowDown 166 | case temporaryRedirect 167 | case tokenRefreshRequired 168 | case tooManyBuckets 169 | case unexpectedContent 170 | case unresolvableGrantByEmailAddress 171 | case userKeyMustBeSpecified 172 | */ 173 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/ErrorParser/ErrorParser.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | 3 | public struct ErrorParser { 4 | static let codeBytes: Bytes = [.C, .o, .d, .e, .greaterThan] 5 | enum Error: Swift.Error { 6 | case unknownError(String) 7 | case couldNotFindErrorTag 8 | } 9 | 10 | var scanner: Scanner 11 | 12 | init(scanner: Scanner) { 13 | self.scanner = scanner 14 | } 15 | } 16 | 17 | extension ErrorParser { 18 | public static func parse(_ bytes: Bytes) throws -> AWSError { 19 | var parser = ErrorParser(scanner: Scanner(bytes)) 20 | return try parser.extractError() 21 | } 22 | } 23 | 24 | extension ErrorParser { 25 | mutating func extractError() throws -> AWSError { 26 | while true { 27 | skip(until: .lessThan) 28 | 29 | guard scanner.peek() != nil else { 30 | throw Error.couldNotFindErrorTag 31 | } 32 | 33 | // check for `` 34 | guard checkForCodeTag() else { 35 | continue 36 | } 37 | 38 | let errorBytes = consume(until: .lessThan) 39 | 40 | guard let error = ErrorParser.awsGrammar.contains(errorBytes) else { 41 | throw Error.unknownError(errorBytes.makeString()) 42 | } 43 | 44 | return error 45 | } 46 | } 47 | 48 | mutating func checkForCodeTag() -> Bool { 49 | scanner.pop() 50 | 51 | for (index, byte) in ErrorParser.codeBytes.enumerated() { 52 | guard 53 | let preview = scanner.peek(aheadBy: index), 54 | preview == byte 55 | else { 56 | return false 57 | } 58 | } 59 | 60 | scanner.pop(ErrorParser.codeBytes.count) 61 | 62 | return true 63 | } 64 | } 65 | 66 | extension ErrorParser { 67 | mutating func skip(until terminator: Byte) { 68 | var count = 0 69 | 70 | while let byte = scanner.peek(aheadBy: count), byte != terminator { 71 | count += 1 72 | } 73 | 74 | scanner.pop(count) 75 | } 76 | 77 | mutating func consume(until terminator: Byte) -> Bytes { 78 | var bytes: [Byte] = [] 79 | 80 | while let byte = scanner.peek(), byte != terminator { 81 | scanner.pop() 82 | bytes.append(byte) 83 | } 84 | 85 | return bytes 86 | } 87 | } 88 | 89 | extension Byte { 90 | /// < 91 | static let lessThan: Byte = 0x3C 92 | 93 | /// > 94 | static let greaterThan: Byte = 0x3E 95 | 96 | /// lowercase `d` 97 | static let d: Byte = 0x64 98 | 99 | /// lowercase `e` 100 | static let e: Byte = 0x65 101 | 102 | /// lowercase `o` 103 | static let o: Byte = 0x6F 104 | } 105 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/ErrorParser/Scanner.swift: -------------------------------------------------------------------------------- 1 | struct Scanner { 2 | var pointer: UnsafePointer 3 | var elements: UnsafeBufferPointer 4 | // assuming you don't mutate no copy _should_ occur 5 | let elementsCopy: [Element] 6 | } 7 | 8 | extension Scanner { 9 | init(_ data: [Element]) { 10 | self.elementsCopy = data 11 | self.elements = elementsCopy.withUnsafeBufferPointer { $0 } 12 | 13 | self.pointer = elements.baseAddress! 14 | } 15 | } 16 | 17 | extension Scanner { 18 | func peek(aheadBy n: Int = 0) -> Element? { 19 | guard pointer.advanced(by: n) < elements.endAddress else { return nil } 20 | return pointer.advanced(by: n).pointee 21 | } 22 | 23 | /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have 24 | @discardableResult 25 | mutating func pop() -> Element { 26 | assert(pointer != elements.endAddress) 27 | defer { pointer = pointer.advanced(by: 1) } 28 | return pointer.pointee 29 | } 30 | 31 | /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have 32 | @discardableResult 33 | mutating func attemptPop() throws -> Element { 34 | guard pointer < elements.endAddress else { throw ScannerError.Reason.endOfStream } 35 | defer { pointer = pointer.advanced(by: 1) } 36 | return pointer.pointee 37 | } 38 | 39 | mutating func pop(_ n: Int) { 40 | for _ in 0.. { 63 | return baseAddress!.advanced(by: endIndex) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/ErrorParser/Trie.swift: -------------------------------------------------------------------------------- 1 | class Trie { 2 | var key: UInt8 3 | var value: ValueType? 4 | 5 | var children: [Trie] = [] 6 | 7 | var isLeaf: Bool { 8 | return children.count == 0 9 | } 10 | 11 | convenience init() { 12 | self.init(key: 0x00) 13 | } 14 | 15 | init(key: UInt8, value: ValueType? = nil) { 16 | self.key = key 17 | self.value = value 18 | } 19 | } 20 | 21 | extension Trie { 22 | subscript(_ key: UInt8) -> Trie? { 23 | get { return children.first(where: { $0.key == key }) } 24 | set { 25 | guard let index = children.index(where: { $0.key == key }) else { 26 | guard let newValue = newValue else { return } 27 | children.append(newValue) 28 | return 29 | } 30 | 31 | guard let newValue = newValue else { 32 | children.remove(at: index) 33 | return 34 | } 35 | 36 | let child = children[index] 37 | guard child.value == nil else { 38 | print("warning: inserted duplicate tokens into Trie.") 39 | return 40 | } 41 | 42 | child.value = newValue.value 43 | } 44 | } 45 | 46 | func insert(_ keypath: [UInt8], value: ValueType) { 47 | insert(value, for: keypath) 48 | } 49 | 50 | func insert(_ value: ValueType, for keypath: [UInt8]) { 51 | var current = self 52 | 53 | for (index, key) in keypath.enumerated() { 54 | guard let next = current[key] else { 55 | let next = Trie(key: key) 56 | current[key] = next 57 | current = next 58 | 59 | if index == keypath.endIndex - 1 { 60 | next.value = value 61 | } 62 | 63 | continue 64 | } 65 | 66 | if index == keypath.endIndex - 1 && next.value == nil { 67 | next.value = value 68 | } 69 | 70 | current = next 71 | } 72 | } 73 | 74 | func contains(_ keypath: [UInt8]) -> ValueType? { 75 | var current = self 76 | 77 | for key in keypath { 78 | guard let next = current[key] else { return nil } 79 | current = next 80 | } 81 | 82 | return current.value 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/Payload.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | import Crypto 3 | 4 | public enum Payload { 5 | case bytes(Bytes) 6 | case unsigned 7 | case none 8 | } 9 | 10 | extension Payload { 11 | func hashed() throws -> String { 12 | switch self { 13 | case .bytes(let bytes): 14 | return try Hash.make(.sha256, bytes).hexString 15 | 16 | case .unsigned: 17 | return "UNSIGNED-PAYLOAD" 18 | 19 | case .none: 20 | return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 21 | } 22 | } 23 | } 24 | 25 | extension Payload { 26 | var bytes: Bytes { 27 | switch self { 28 | case .bytes(let bytes): 29 | return bytes 30 | 31 | default: 32 | return [] 33 | } 34 | } 35 | } 36 | 37 | extension Payload: Equatable { 38 | public static func ==(lhs: Payload, rhs: Payload) -> Bool { 39 | switch (lhs, rhs) { 40 | case (.bytes, .bytes), (.unsigned, .unsigned), (.none, .none): 41 | return true 42 | 43 | default: 44 | return false 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/PercentEncoder.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | 3 | extension Byte { 4 | public static let awsQueryAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~=&".makeBytes() 5 | 6 | public static let awsPathAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~/".makeBytes() 7 | } 8 | 9 | extension String { 10 | public func percentEncode(allowing allowed: Bytes) throws -> String { 11 | let bytes = self.makeBytes() 12 | let encodedBytes = try percentEncodedUppercase(bytes, shouldEncode: { 13 | return !allowed.contains($0) 14 | }) 15 | return encodedBytes.makeString() 16 | } 17 | } 18 | 19 | func percentEncodedUppercase( 20 | _ input: [Byte], 21 | shouldEncode: (Byte) throws -> Bool = { _ in true } 22 | ) throws -> [Byte] { 23 | var group: [Byte] = [] 24 | try input.forEach { byte in 25 | if try shouldEncode(byte) { 26 | let hex = String(byte, radix: 16).uppercased().utf8 27 | group.append(.percent) 28 | if hex.count == 1 { 29 | group.append(.zero) 30 | } 31 | group.append(contentsOf: hex) 32 | } else { 33 | group.append(byte) 34 | } 35 | } 36 | return group 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AWSSignatureV4/Region.swift: -------------------------------------------------------------------------------- 1 | public enum Region: String { 2 | case usEast1 = "us-east-1" 3 | case usEast2 = "us-east-2" 4 | case usWest1 = "us-west-1" 5 | case usWest2 = "us-west-2" 6 | case euWest1 = "eu-west-1" 7 | case euCentral1 = "eu-central-1" 8 | case apSouth1 = "ap-south-1" 9 | case apSoutheast1 = "ap-southeast-1" 10 | case apSoutheast2 = "ap-southeast-2" 11 | case apNortheast1 = "ap-northeast-1" 12 | case apNortheast2 = "ap-northeast-2" 13 | case saEast1 = "sa-east-1" 14 | 15 | public var host: String { 16 | switch self { 17 | case .usEast1: return "s3.amazonaws.com" 18 | case .usEast2: return "s3.us-east-2.amazonaws.com" 19 | case .usWest1: return "s3-us-west-1.amazonaws.com" 20 | case .usWest2: return "s3-us-west-2.amazonaws.com" 21 | case .euWest1: return "s3-eu-west-1.amazonaws.com" 22 | case .euCentral1: return "s3.eu-central-1.amazonaws.com" 23 | case .apSouth1: return "s3.ap-south-1.amazonaws.com" 24 | case .apSoutheast1: return "s3-ap-southeast-1.amazonaws.com" 25 | case .apSoutheast2: return "s3-ap-southeast-2.amazonaws.com" 26 | case .apNortheast1: return "s3-ap-northeast-1.amazonaws.com" 27 | case .apNortheast2: return "s3.ap-northeast-2.amazonaws.com" 28 | case .saEast1: return "s3-sa-east-1.amazonaws.com" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/AutoScaling/AutoScaling.swift: -------------------------------------------------------------------------------- 1 | import HTTP 2 | import Vapor 3 | import Core 4 | import Transport 5 | import AWSSignatureV4 6 | import TLS 7 | import Node 8 | import SWXMLHash 9 | 10 | @_exported import enum AWSSignatureV4.AWSError 11 | @_exported import enum AWSSignatureV4.AccessControlList 12 | 13 | public enum LifeCycleState { 14 | case InService 15 | } 16 | 17 | public enum HealthStatus { 18 | case Healthy 19 | } 20 | 21 | public struct Instance { 22 | public let state: LifeCycleState 23 | public let instanceID: String 24 | public let status: HealthStatus 25 | public let protectedFromScaleIn: Bool 26 | public let availabilityZone: String 27 | } 28 | 29 | public struct AutoScaling { 30 | public enum Error: Swift.Error { 31 | case InvalidNextToken // The NextToken value is not valid. 32 | case ResourceContention // You already have a pending update to an Auto Scaling resource (for example, a group, instance, or load balancer). 33 | case invalidResponse(Status) 34 | } 35 | 36 | let accessKey: String 37 | let secretKey: String 38 | let region: Region 39 | let service: String 40 | let host: String 41 | let baseURL: String 42 | let signer: AWSSignatureV4 43 | 44 | public init(accessKey: String, secretKey: String, region: String) { 45 | self.accessKey = accessKey 46 | self.secretKey = secretKey 47 | self.region = Region(rawValue: region)! 48 | self.service = "autoscaling" 49 | self.host = "\(self.service).amazonaws.com" 50 | self.baseURL = "https://\(self.host)" 51 | self.signer = AWSSignatureV4( 52 | service: self.service, 53 | host: self.host, 54 | region: self.region, 55 | accessKey: accessKey, 56 | secretKey: secretKey 57 | ) 58 | } 59 | 60 | func generateQuery(for action: String, name: String) -> String { 61 | return "Action=\(action)&AutoScalingGroupNames.member.1=\(name)&Version=2011-01-01" 62 | } 63 | 64 | /* 65 | * http://docs.aws.amazon.com/AutoScaling/latest/APIReference/API_DescribeAutoScalingGroups.html 66 | */ 67 | public func describeAutoScalingGroups(name: String) throws -> [Instance] { 68 | let query = generateQuery(for: "DescribeAutoScalingGroups", name: name) 69 | 70 | let headers = try signer.sign(path: "/", query: query) 71 | 72 | let client = try EngineClientFactory.init().makeClient(hostname: host, port: 443, securityLayer: .tls(Context.init(.client)), proxy: nil) 73 | 74 | let version = HTTP.Version(major: 1, minor: 1) 75 | let request = HTTP.Request(method: Method.get, uri: "\(baseURL)/?\(query)", version: version, headers: headers, body: Body.data(Bytes([]))) 76 | let response = try client.respond(to: request) 77 | 78 | guard response.status == .ok else { 79 | print("Response error: \(response)") 80 | guard let bytes = response.body.bytes else { 81 | throw Error.invalidResponse(response.status) 82 | } 83 | 84 | throw try ErrorParser.parse(bytes) 85 | } 86 | 87 | guard let bytes = response.body.bytes else { 88 | throw Error.invalidResponse(.internalServerError) 89 | } 90 | 91 | let output = bytes.makeString() 92 | let xml = SWXMLHash.parse(output) 93 | let autoscalingGroupXML = xml["DescribeAutoScalingGroupsResponse"]["DescribeAutoScalingGroupsResult"]["AutoScalingGroups"]["member"] 94 | 95 | var autoscalingGroup = [Instance]() 96 | for member in autoscalingGroupXML["Instances"].children { 97 | if let instanceId = member["InstanceId"].element?.text, let availabilityZone = member["AvailabilityZone"].element?.text { 98 | autoscalingGroup.append(Instance(state: .InService, instanceID: instanceId, status: .Healthy, protectedFromScaleIn: false, availabilityZone: availabilityZone)) 99 | } 100 | } 101 | return autoscalingGroup 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/EC2/EC2.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class EC2 { 4 | let accessKey: String 5 | let secretKey: String 6 | let region: String 7 | let service: String 8 | let host: String 9 | let baseURL: String 10 | 11 | public init(accessKey: String, secretKey: String, region: String) { 12 | self.accessKey = accessKey 13 | self.secretKey = secretKey 14 | self.region = region 15 | self.service = "ec2" 16 | self.host = "\(self.service).amazonaws.com" 17 | self.baseURL = "https://\(self.host)" 18 | } 19 | 20 | public func describeInstances() throws -> String { 21 | //TODO(Brett): wrap this result in a model instead of a string type 22 | /*let response = try AWSDriver().call( 23 | method: .get, 24 | service: service, 25 | host: host, 26 | region: region, 27 | baseURL: baseURL, 28 | key: accessKey, 29 | secret: secretKey, 30 | requestParam: "Action=DescribeRegions&Version=2015-10-01" 31 | ) 32 | 33 | return response.description*/ 34 | return "" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/S3/S3.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | import HTTP 3 | import Transport 4 | import AWSSignatureV4 5 | import Vapor 6 | 7 | @_exported import enum AWSSignatureV4.AWSError 8 | @_exported import enum AWSSignatureV4.AccessControlList 9 | 10 | public struct S3 { 11 | public enum Error: Swift.Error { 12 | case unimplemented 13 | case invalidResponse(Status) 14 | } 15 | 16 | let signer: AWSSignatureV4 17 | public var host: String 18 | 19 | public init( 20 | host: String, 21 | accessKey: String, 22 | secretKey: String, 23 | region: Region 24 | ) { 25 | self.host = host 26 | signer = AWSSignatureV4( 27 | service: "s3", 28 | host: host, 29 | region: region, 30 | accessKey: accessKey, 31 | secretKey: secretKey 32 | ) 33 | } 34 | 35 | public func upload(bytes: Bytes, path: String, access: AccessControlList) throws { 36 | let url = generateURL(for: path) 37 | let headers = try signer.sign( 38 | payload: .bytes(bytes), 39 | method: .put, 40 | path: path, 41 | headers: ["x-amz-acl": access.rawValue] 42 | ) 43 | 44 | let response = try EngineClient.factory.put(url, headers, Body.data(bytes)) 45 | guard response.status == .ok else { 46 | guard let bytes = response.body.bytes else { 47 | throw Error.invalidResponse(response.status) 48 | } 49 | 50 | throw try ErrorParser.parse(bytes) 51 | } 52 | } 53 | 54 | public func get(path: String) throws -> Bytes { 55 | let url = generateURL(for: path) 56 | let headers = try signer.sign(path: path) 57 | 58 | let response = try EngineClient.factory.get(url, headers) 59 | guard response.status == .ok else { 60 | guard let bytes = response.body.bytes else { 61 | throw Error.invalidResponse(response.status) 62 | } 63 | 64 | throw try ErrorParser.parse(bytes) 65 | } 66 | 67 | guard let bytes = response.body.bytes else { 68 | throw Error.invalidResponse(.internalServerError) 69 | } 70 | 71 | return bytes 72 | } 73 | 74 | public func delete(file: String) throws { 75 | throw Error.unimplemented 76 | } 77 | } 78 | 79 | extension S3 { 80 | func generateURL(for path: String) -> String { 81 | //FIXME(Brett): 82 | return "https://\(host)\(path)" 83 | } 84 | } 85 | 86 | extension Dictionary where Key: CustomStringConvertible, Value: CustomStringConvertible { 87 | var vaporHeaders: [HeaderKey: String] { 88 | var result: [HeaderKey: String] = [:] 89 | self.forEach { 90 | result.updateValue($0.value.description, forKey: HeaderKey($0.key.description)) 91 | } 92 | 93 | return result 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/VaporS3/Provider.swift: -------------------------------------------------------------------------------- 1 | import AWSSignatureV4 2 | import Vapor 3 | import S3 4 | 5 | private let s3StorageKey = "s3-provider:s3" 6 | 7 | public final class Provider: Vapor.Provider { 8 | let s3: S3 9 | public static var repositoryName: String = "VaporS3" 10 | /// Initialize the provider with an s3 instance 11 | public init(_ s3: S3) { 12 | self.s3 = s3 13 | } 14 | 15 | /// Create an s3 instance with host, accessKey, secretKey, and region 16 | public convenience init(host: String, accessKey: String, secretKey: String, region: Region) { 17 | let s3 = S3(host: host, accessKey: accessKey, secretKey: secretKey, region: region) 18 | self.init(s3) 19 | } 20 | 21 | /// Initialize the s3 instance from config 22 | /// expects `s3.json` with following keys: 23 | /// host: String 24 | /// accessKey: String 25 | /// secretKey: String 26 | /// region: String -- matching official AWS Region list 27 | public convenience init(config: Config) throws { 28 | guard let s3Config = config["s3"] else { throw ConfigError.missingFile("s3") } 29 | guard let host = s3Config["host"]?.string else { 30 | throw ConfigError.missing(key: ["host"], file: "s3", desiredType: String.self) 31 | } 32 | guard let accessKey = s3Config["accessKey"]?.string else { 33 | throw ConfigError.missing(key: ["accessKey"], file: "s3", desiredType: String.self) 34 | } 35 | guard let secretKey = s3Config["secretKey"]?.string else { 36 | throw ConfigError.missing(key: ["secretKey"], file: "s3", desiredType: String.self) 37 | } 38 | guard let region = s3Config["region"]?.string.flatMap(Region.init) else { 39 | throw ConfigError.missing(key: ["region"], file: "s3", desiredType: Region.self) 40 | } 41 | self.init(host: host, accessKey: accessKey, secretKey: secretKey, region: region) 42 | } 43 | 44 | public func boot(_ drop: Droplet) throws { 45 | drop.storage[s3StorageKey] = s3 46 | } 47 | 48 | public func boot(_ config: Config) throws {} 49 | 50 | public func beforeRun(_ droplet: Droplet) throws {} 51 | } 52 | 53 | extension Droplet { 54 | /// Use this function to access the underlying 55 | /// s3 object. 56 | /// 57 | /// make sure that VaporS3 has been added properly 58 | /// before doing 59 | public func s3() throws -> S3 { 60 | guard let s3 = storage[s3StorageKey] as? S3 else { throw VaporS3Error.s3NotConfigured } 61 | return s3 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/VaporS3/VaporS3Error.swift: -------------------------------------------------------------------------------- 1 | import Debugging 2 | 3 | public enum VaporS3Error: Debuggable { 4 | case s3NotConfigured 5 | } 6 | 7 | extension VaporS3Error { 8 | public var identifier: String { 9 | return "s3NotConfigured" 10 | } 11 | 12 | public var reason: String { 13 | return "s3 didn't exist in droplet" 14 | } 15 | 16 | public var possibleCauses: [String] { 17 | return [ 18 | "you're accessing drop.s3() before the provider has been added and booted properly" 19 | ] 20 | } 21 | 22 | public var suggestedFixes: [String] { 23 | return [ 24 | "make sure you're adding the s3 provider with `drop.addProvider`", 25 | "do not call `drop.s3()` until AFTER the provider has been added" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/AWSTests/AWSTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import Core 4 | 5 | class AWSTests: XCTestCase { 6 | static var allTests = [ 7 | ("testExample", testExample) 8 | ] 9 | 10 | func testExample() { 11 | XCTAssertEqual(2+2, 4) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/AWSTests/AutoscalingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import HTTP 4 | import Foundation 5 | 6 | @testable import AutoScaling 7 | 8 | class AutoscalingTests: XCTestCase { 9 | static var allTests = [ 10 | ("testGenerateQuery", testGenerateQuery) 11 | ] 12 | 13 | func testGenerateQuery() { 14 | let autoscaling = AutoScaling(accessKey: "fake", secretKey: "secret", region: "us-east-1") 15 | let query = autoscaling.generateQuery(for: "Action", name: "autoscaling-name") 16 | XCTAssertEqual(query, "Action=Action&AutoScalingGroupNames.member.1=autoscaling-name&Version=2011-01-01") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/AWSTests/ErrorParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import AWSSignatureV4 4 | 5 | class ErrorParserTests: XCTestCase { 6 | static var allTests = [ 7 | ("testExample", testExample) 8 | ] 9 | 10 | func testExample() { 11 | XCTAssertEqual(2+2, 4) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/AWSTests/SignatureTestSuite.swift: -------------------------------------------------------------------------------- 1 | /** 2 | All tests are based off of Amazon's Signature Test Suite 3 | See: http://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html 4 | 5 | They also include the [`x-amz-content-sha256` header](http://docs.aws.amazon.com/AmazonS3/latest/API/bucket-policy-s3-sigv4-conditions.html). 6 | */ 7 | 8 | import XCTest 9 | 10 | import HTTP 11 | import Foundation 12 | 13 | @testable import AWSSignatureV4 14 | 15 | class SignatureTestSuite: XCTestCase { 16 | static var allTests = [ 17 | ("testGetUnreserved", testGetUnreserved), 18 | ("testGetUTF8", testGetUTF8), 19 | ("testGetVanilla", testGetVanilla), 20 | ("testGetVanillaQuery", testGetVanillaQuery), 21 | ("testGetVanillaEmptyQueryKey", testGetVanillaEmptyQueryKey), 22 | ("testGetVanillaQueryUnreserved", testGetVanillaQueryUnreserved), 23 | ("testGetVanillaQueryUTF8", testGetVanillaQueryUTF8), 24 | ("testPostVanilla", testPostVanilla), 25 | ("testPostVanillaQuery", testPostVanillaQuery), 26 | ("testPostVanillaQueryNonunreserved", testPostVanillaQueryNonunreserved) 27 | ] 28 | 29 | static let dateFormatter: DateFormatter = { 30 | let _dateFormatter = DateFormatter() 31 | _dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 32 | _dateFormatter.dateFormat = "YYYYMMdd'T'HHmmss'Z'" 33 | return _dateFormatter 34 | }() 35 | 36 | func testGetUnreserved() { 37 | let expectedCanonicalRequest = "GET\n/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 38 | 39 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 40 | 41 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 42 | "X-Amz-Date": "20150830T123600Z", 43 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=feae8f2b49f6807d4ca43941e2d6c7aacaca499df09935d14e97eed7647da5dc" 44 | ] 45 | 46 | let result = sign( 47 | method: .get, 48 | path: "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 49 | ) 50 | result.expect( 51 | canonicalRequest: expectedCanonicalRequest, 52 | credentialScope: expectedCredentialScope, 53 | canonicalHeaders: expectedCanonicalHeaders 54 | ) 55 | } 56 | 57 | func testGetUTF8() { 58 | let expectedCanonicalRequest = "GET\n/%E1%88%B4\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 59 | 60 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 61 | 62 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 63 | "X-Amz-Date": "20150830T123600Z", 64 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=29d69532444b4f32a4c1b19af2afc116589685058ece54d8e43f0be05aeff6c0" 65 | ] 66 | 67 | let result = sign(method: .get, path: "/ሴ") 68 | result.expect( 69 | canonicalRequest: expectedCanonicalRequest, 70 | credentialScope: expectedCredentialScope, 71 | canonicalHeaders: expectedCanonicalHeaders 72 | ) 73 | } 74 | 75 | func testGetVanilla() { 76 | let expectedCanonicalRequest = "GET\n/\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 77 | 78 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 79 | 80 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 81 | "X-Amz-Date": "20150830T123600Z", 82 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=726c5c4879a6b4ccbbd3b24edbd6b8826d34f87450fbbf4e85546fc7ba9c1642" 83 | ] 84 | 85 | let result = sign(method: .get, path: "/") 86 | result.expect( 87 | canonicalRequest: expectedCanonicalRequest, 88 | credentialScope: expectedCredentialScope, 89 | canonicalHeaders: expectedCanonicalHeaders 90 | ) 91 | } 92 | 93 | //duplicate as `testGetVanilla`, but is in Amazon Test Suite 94 | //will keep until I figure out why there's a duplicate test 95 | func testGetVanillaQuery() { 96 | let expectedCanonicalRequest = "GET\n/\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 97 | 98 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 99 | 100 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 101 | "X-Amz-Date": "20150830T123600Z", 102 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=726c5c4879a6b4ccbbd3b24edbd6b8826d34f87450fbbf4e85546fc7ba9c1642" 103 | ] 104 | 105 | let result = sign(method: .get, path: "/") 106 | result.expect( 107 | canonicalRequest: expectedCanonicalRequest, 108 | credentialScope: expectedCredentialScope, 109 | canonicalHeaders: expectedCanonicalHeaders 110 | ) 111 | } 112 | 113 | func testGetVanillaEmptyQueryKey() { 114 | let expectedCanonicalRequest = "GET\n/\nParam1=value1\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 115 | 116 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 117 | 118 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 119 | "X-Amz-Date": "20150830T123600Z", 120 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2287c0f96af21b7ccf3ee4a2905bcbb2d6f9a94c68d0849f3d1715ef003f2a05" 121 | ] 122 | 123 | let result = sign(method: .get, path: "/", query: "Param1=value1") 124 | result.expect( 125 | canonicalRequest: expectedCanonicalRequest, 126 | credentialScope: expectedCredentialScope, 127 | canonicalHeaders: expectedCanonicalHeaders 128 | ) 129 | } 130 | 131 | func testGetVanillaQueryUnreserved() { 132 | let expectedCanonicalRequest = "GET\n/\n-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 133 | 134 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 135 | 136 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 137 | "X-Amz-Date": "20150830T123600Z", 138 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=e86fe49a4c0dda9163bed3b1b40d530d872eb612e2c366de300bfefdf356fd6a" 139 | ] 140 | 141 | let result = sign( 142 | method: .get, 143 | path: "/", 144 | query:"-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 145 | ) 146 | result.expect( 147 | canonicalRequest: expectedCanonicalRequest, 148 | credentialScope: expectedCredentialScope, 149 | canonicalHeaders: expectedCanonicalHeaders 150 | ) 151 | } 152 | 153 | func testGetVanillaQueryUTF8() { 154 | let expectedCanonicalRequest = "GET\n/\n%E1%88%B4=bar\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 155 | 156 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 157 | 158 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 159 | "X-Amz-Date": "20150830T123600Z", 160 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=6753d65781ac8f6964cb6fb90445ee138d65d9663df21f28f478bd09add64fd8" 161 | ] 162 | 163 | let result = sign(method: .get, path: "/", query: "ሴ=bar") 164 | result.expect( 165 | canonicalRequest: expectedCanonicalRequest, 166 | credentialScope: expectedCredentialScope, 167 | canonicalHeaders: expectedCanonicalHeaders 168 | ) 169 | } 170 | 171 | func testPostVanilla() { 172 | let expectedCanonicalRequest = "POST\n/\n\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 173 | 174 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 175 | 176 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 177 | "X-Amz-Date": "20150830T123600Z", 178 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=3ad5e249949a59b862eedd9f1bf1ece4693c3042bf860ef5e3351b8925316f98" 179 | ] 180 | 181 | let result = sign(method: .post, path: "/") 182 | result.expect( 183 | canonicalRequest: expectedCanonicalRequest, 184 | credentialScope: expectedCredentialScope, 185 | canonicalHeaders: expectedCanonicalHeaders 186 | ) 187 | } 188 | 189 | func testPostVanillaQuery() { 190 | let expectedCanonicalRequest = "POST\n/\nParam1=value1\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 191 | 192 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 193 | 194 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 195 | "X-Amz-Date": "20150830T123600Z", 196 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=d43fd95e1dfefe02247ce8858649e1a063f9dd10f25f170f7ebda6ee3e9b6fbc" 197 | ] 198 | 199 | let result = sign(method: .post, path: "/", query: "Param1=value1") 200 | result.expect( 201 | canonicalRequest: expectedCanonicalRequest, 202 | credentialScope: expectedCredentialScope, 203 | canonicalHeaders: expectedCanonicalHeaders 204 | ) 205 | } 206 | 207 | /** 208 | This test isn't based on the test suite, but tracks handling of special characters. 209 | */ 210 | func testPostVanillaQueryNonunreserved() { 211 | let expectedCanonicalRequest = "POST\n/\n%40%23%24%25%5E&%2B=%2F%2C%3F%3E%3C%60%22%3B%3A%5C%7C%5D%5B%7B%7D\nhost:example.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20150830T123600Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 212 | 213 | let expectedCredentialScope = "20150830/us-east-1/service/aws4_request" 214 | 215 | let expectedCanonicalHeaders: [HeaderKey : String] = [ 216 | "X-Amz-Date": "20150830T123600Z", 217 | "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=3db24d76713a5ccb9afe4a26acb83ae4cfa3e67d9e10f165bdf99bda199c625d" 218 | ] 219 | 220 | let result = sign(method: .post, path: "/", query: "@#$%^&+=/,?><`\";:\\|][{}") 221 | result.expect( 222 | canonicalRequest: expectedCanonicalRequest, 223 | credentialScope: expectedCredentialScope, 224 | canonicalHeaders: expectedCanonicalHeaders 225 | ) 226 | 227 | } 228 | } 229 | 230 | extension SignatureTestSuite { 231 | var testDate: Date { 232 | return SignatureTestSuite.dateFormatter.date(from: "20150830T123600Z")! 233 | } 234 | 235 | 236 | /** 237 | Preparation of data to sign a canonical request. 238 | 239 | Intended to handle the preparation in the AWSSignatureV4's `sign` function 240 | 241 | - returns: 242 | Hash value and multiple versions of headers 243 | 244 | - parameters: 245 | - auth: Signature struct to use for calculations 246 | - host: Hostname to sign for 247 | */ 248 | func prepCanonicalRequest(auth: AWSSignatureV4, host: String) -> (String, String, String) { 249 | let payloadHash = try! Payload.none.hashed() 250 | var headers = [String:String]() 251 | auth.generateHeadersToSign(headers: &headers, host: host, hash: payloadHash) 252 | 253 | let sortedHeaders = auth.alphabetize(headers) 254 | let signedHeaders = sortedHeaders.map { $0.key.lowercased() }.joined(separator: ";") 255 | let canonicalHeaders = auth.createCanonicalHeaders(sortedHeaders) 256 | return (payloadHash, signedHeaders, canonicalHeaders) 257 | } 258 | 259 | func sign( 260 | method: AWSSignatureV4.Method, 261 | path: String, 262 | query: String = "" 263 | ) -> SignerResult { 264 | let host = "example.amazonaws.com" 265 | var auth = AWSSignatureV4( 266 | service: "service", 267 | host: host, 268 | region: .usEast1, 269 | accessKey: "AKIDEXAMPLE", 270 | secretKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" 271 | ) 272 | 273 | auth.unitTestDate = testDate 274 | let (payloadHash, signedHeaders, preppedCanonicalHeaders) = prepCanonicalRequest(auth: auth, host: host) 275 | let canonicalRequest = try! auth.getCanonicalRequest(payloadHash: payloadHash, method: method, path: path, query: query, canonicalHeaders: preppedCanonicalHeaders, signedHeaders: signedHeaders) 276 | 277 | 278 | let credentialScope = auth.getCredentialScope() 279 | 280 | //FIXME(Brett): handle throwing 281 | let canonicalHeaders = try! auth.sign( 282 | payload: .none, 283 | method: method, 284 | path: path, 285 | query: query 286 | ) 287 | 288 | return SignerResult( 289 | canonicalRequest: canonicalRequest, 290 | credentialScope: credentialScope, 291 | canonicalHeaders: canonicalHeaders 292 | ) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /Tests/AWSTests/Utilities/SignerResult.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import HTTP 4 | 5 | struct SignerResult { 6 | let canonicalRequest: String 7 | let credentialScope: String 8 | let canonicalHeaders: [HeaderKey: String] 9 | } 10 | 11 | extension SignerResult { 12 | func expect( 13 | canonicalRequest: String, 14 | credentialScope: String, 15 | canonicalHeaders: [HeaderKey: String], 16 | file: StaticString = #file, 17 | line: UInt = #line 18 | ) { 19 | XCTAssertEqual(self.canonicalRequest, canonicalRequest, file: file, line: line) 20 | XCTAssertEqual(self.credentialScope, credentialScope, file: file, line: line) 21 | 22 | canonicalHeaders.forEach { 23 | if $0.key == "Authorization" { 24 | for (givenLine, expectedLine) in zip(self.canonicalHeaders[$0.key]!.components(separatedBy: " "), $0.value.components(separatedBy: " ")) { 25 | XCTAssertEqual(givenLine, expectedLine) 26 | } 27 | } else { 28 | XCTAssertEqual(self.canonicalHeaders[$0.key], $0.value, file: file, line: line) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AWSTests 3 | 4 | XCTMain([ 5 | testCase(AWSTests.allTests), 6 | testCase(SignatureTestSuite.allTests), 7 | testCase(ErrorParserTests.allTests), 8 | testCase(AutoscalingTests.allTests), 9 | ]) 10 | --------------------------------------------------------------------------------