├── .gitignore
├── .swift-version
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ ├── calebkleveter.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ │ └── pro.xcuserdatad
│ │ ├── IDEFindNavigatorScopes.plist
│ │ └── UserInterfaceState.xcuserstate
│ ├── xcshareddata
│ └── xcschemes
│ │ ├── S3DemoRun.xcscheme
│ │ ├── S3Kit-Package.xcscheme
│ │ ├── S3Kit.xcscheme
│ │ └── S3Signer.xcscheme
│ └── xcuserdata
│ ├── calebkleveter.xcuserdatad
│ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── pro.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
├── .travis.yml
├── Dockerfile
├── Jenkinsfile
├── LICENSE
├── Other
└── Postman
│ └── Vapor S3.postman_collection.json
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── S3DemoRun
│ └── main.swift
├── S3Kit
│ ├── Exports.swift
│ ├── Extensions
│ │ ├── Error+S3.swift
│ │ ├── HTTPHeaders+Tools.swift
│ │ ├── Region+Tools.swift
│ │ ├── Response+XMLDecoding.swift
│ │ ├── S3+Bucket.swift
│ │ ├── S3+Copy.swift
│ │ ├── S3+Delete.swift
│ │ ├── S3+Get.swift
│ │ ├── S3+List.swift
│ │ ├── S3+Move.swift
│ │ ├── S3+ObjectInfo.swift
│ │ ├── S3+Put.swift
│ │ ├── S3+Request.swift
│ │ ├── S3+Service.swift
│ │ ├── S3+Strings.swift
│ │ └── String+Tools.swift
│ ├── Models
│ │ ├── AccessControlList.swift
│ │ ├── Bucket.swift
│ │ ├── BucketResults.swift
│ │ ├── BucketsInfo.swift
│ │ ├── ErrorMessage.swift
│ │ ├── File.swift
│ │ ├── Object.swift
│ │ └── Owner.swift
│ ├── Protocols
│ │ ├── LocationConvertible.swift
│ │ └── S3Client.swift
│ ├── S3.swift
│ └── URLBuilder
│ │ ├── S3URLBuilder.swift
│ │ └── URLBuilder.swift
├── S3Provider
│ ├── Exports.swift
│ └── Model
│ │ └── Models+Content.swift
└── S3Signer
│ ├── Dates.swift
│ ├── Derived_from_LICENSE
│ ├── Expiration.swift
│ ├── Exports.swift
│ ├── Extensions
│ ├── HMAC+Tools.swift
│ ├── HTTPMethod+Description.swift
│ ├── S3Signer+Private.swift
│ └── String+Encoding.swift
│ ├── Payload.swift
│ ├── Region.swift
│ └── S3Signer.swift
├── Tests
├── LinuxMain.swift
└── S3Tests
│ ├── AWSTestSuite.swift
│ ├── BaseTestCase.swift
│ ├── Info.plist
│ ├── S3SignerAWSTests.swift
│ ├── S3SignerV2Tests.swift
│ └── S3Tests.swift
└── scripts
├── test.sh
├── update.sh
└── upgrade.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .build
3 | Packages
4 | *.xcodeproj
5 | *.xcodeproj/xcshareddata/xcbaselines/4724F8141DA88A530003BAC6.xcbaseline
6 | Package.pins
7 |
8 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.1
2 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/calebkleveter.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LiveUI/S3/966f8e4cd12115b0e0a4ccc424eee3f52507d6be/.swiftpm/xcode/package.xcworkspace/xcuserdata/calebkleveter.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/pro.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/pro.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LiveUI/S3/966f8e4cd12115b0e0a4ccc424eee3f52507d6be/.swiftpm/xcode/package.xcworkspace/xcuserdata/pro.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/S3DemoRun.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
68 |
69 |
73 |
74 |
75 |
76 |
82 |
84 |
90 |
91 |
92 |
93 |
95 |
96 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/S3Kit-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
66 |
67 |
72 |
73 |
75 |
81 |
82 |
83 |
84 |
85 |
95 |
96 |
102 |
103 |
104 |
105 |
111 |
112 |
118 |
119 |
120 |
121 |
123 |
124 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/S3Kit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/S3Signer.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/calebkleveter.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | S3-Package.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | S3.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 1
16 |
17 | S3DemoRun.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 4
21 |
22 | S3Signer.xcscheme_^#shared#^_
23 |
24 | orderHint
25 | 2
26 |
27 | S3TestTools.xcscheme_^#shared#^_
28 |
29 | orderHint
30 | 3
31 |
32 |
33 | SuppressBuildableAutocreation
34 |
35 | S3
36 |
37 | primary
38 |
39 |
40 | S3DemoApp
41 |
42 | primary
43 |
44 |
45 | S3DemoRun
46 |
47 | primary
48 |
49 |
50 | S3Signer
51 |
52 | primary
53 |
54 |
55 | S3TestTools
56 |
57 | primary
58 |
59 |
60 | S3Tests
61 |
62 | primary
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/pro.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
21 |
22 |
23 |
25 |
37 |
38 |
39 |
41 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/pro.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | S3DemoRun.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 3
11 |
12 | S3Kit-Package.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 | S3Kit.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 1
21 |
22 | S3Signer.xcscheme_^#shared#^_
23 |
24 | orderHint
25 | 2
26 |
27 | S3TestTools.xcscheme_^#shared#^_
28 |
29 | orderHint
30 | 3
31 |
32 |
33 | SuppressBuildableAutocreation
34 |
35 | S3
36 |
37 | primary
38 |
39 |
40 | S3DemoRun
41 |
42 | primary
43 |
44 |
45 | S3Kit
46 |
47 | primary
48 |
49 |
50 | S3Provider
51 |
52 | primary
53 |
54 |
55 | S3Signer
56 |
57 | primary
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: swift
2 | os: osx
3 | osx_image: xcode9.4
4 | before_script:
5 | # - gem install xcpretty
6 | - brew install vapor/tap/vapor
7 | script:
8 | # - vapor xcode -y
9 | # - set -o pipefail && xcodebuild -scheme S3DemoRun clean build | xcpretty
10 | - vapor test --verbose
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM liveui/boost-base:1.0
2 |
3 | WORKDIR /boost
4 |
5 | ADD scripts ./scripts
6 | ADD Sources ./Sources
7 | ADD Tests ./Tests
8 | ADD Package.swift ./
9 |
10 | RUN swift build --configuration debug
11 |
12 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent any
3 | options {
4 | timeout(time: 15, unit: 'MINUTES')
5 | }
6 |
7 | stages {
8 | stage('Builds') {
9 | parallel {
10 | stage('Test') {
11 | steps {
12 | script {
13 | sh './scripts/test.sh'
14 | }
15 | }
16 | }
17 | }
18 | }
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 LiveUI
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 |
--------------------------------------------------------------------------------
/Other/Postman/Vapor S3.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "da87b927-b230-431d-91bf-57200936e59f",
4 | "name": "Vapor S3",
5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6 | },
7 | "item": [
8 | {
9 | "name": "Buckets list",
10 | "request": {
11 | "method": "GET",
12 | "header": [],
13 | "body": {},
14 | "url": {
15 | "raw": "{{SERVER}}buckets",
16 | "host": [
17 | "{{SERVER}}buckets"
18 | ]
19 | }
20 | },
21 | "response": []
22 | },
23 | {
24 | "name": "Bucket (create)",
25 | "request": {
26 | "method": "PUT",
27 | "header": [],
28 | "body": {},
29 | "url": {
30 | "raw": "{{SERVER}}bucket",
31 | "host": [
32 | "{{SERVER}}bucket"
33 | ]
34 | }
35 | },
36 | "response": []
37 | },
38 | {
39 | "name": "Bucket (delete)",
40 | "request": {
41 | "method": "DELETE",
42 | "header": [],
43 | "body": {},
44 | "url": {
45 | "raw": "{{SERVER}}bucket",
46 | "host": [
47 | "{{SERVER}}bucket"
48 | ]
49 | }
50 | },
51 | "response": []
52 | },
53 | {
54 | "name": "Bucket (location)",
55 | "request": {
56 | "method": "GET",
57 | "header": [],
58 | "body": {},
59 | "url": {
60 | "raw": "{{SERVER}}bucket/location",
61 | "host": [
62 | "{{SERVER}}bucket"
63 | ],
64 | "path": [
65 | "location"
66 | ]
67 | }
68 | },
69 | "response": []
70 | },
71 | {
72 | "name": "Files (list)",
73 | "request": {
74 | "method": "GET",
75 | "header": [],
76 | "body": {},
77 | "url": {
78 | "raw": "{{SERVER}}files",
79 | "host": [
80 | "{{SERVER}}files"
81 | ]
82 | }
83 | },
84 | "response": []
85 | },
86 | {
87 | "name": "Files (test)",
88 | "request": {
89 | "method": "GET",
90 | "header": [],
91 | "body": {},
92 | "url": {
93 | "raw": "{{SERVER}}files/test",
94 | "host": [
95 | "{{SERVER}}files"
96 | ],
97 | "path": [
98 | "test"
99 | ]
100 | }
101 | },
102 | "response": []
103 | }
104 | ]
105 | }
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "async-http-client",
6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "64851a1a0a2a9e8fa7ae7b3508ce46a1da4a2e1d",
10 | "version": "1.0.0-alpha.2"
11 | }
12 | },
13 | {
14 | "package": "async-kit",
15 | "repositoryURL": "https://github.com/vapor/async-kit.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "b5742bfbbe2d60f3b77465a0907777261e418f23",
19 | "version": "1.0.0-alpha.1"
20 | }
21 | },
22 | {
23 | "package": "console-kit",
24 | "repositoryURL": "https://github.com/vapor/console-kit.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "58d000a6236df517e84f162d21afeca757afc2d1",
28 | "version": "4.0.0-alpha.2"
29 | }
30 | },
31 | {
32 | "package": "HTTPMediaTypes",
33 | "repositoryURL": "https://github.com/Einstore/HTTPMediaTypes.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "cbbb333bce74fbe5ef34648de1d3c2bfef2327fa",
37 | "version": "0.0.1"
38 | }
39 | },
40 | {
41 | "package": "open-crypto",
42 | "repositoryURL": "https://github.com/vapor/open-crypto.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "06d26edb8e28295bb7103b4f950d5ea58d634c1b",
46 | "version": "4.0.0-alpha.2"
47 | }
48 | },
49 | {
50 | "package": "routing-kit",
51 | "repositoryURL": "https://github.com/vapor/routing-kit.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "6c7f4b471f9662d05045d82e64e22d5572a16a82",
55 | "version": "4.0.0-alpha.1"
56 | }
57 | },
58 | {
59 | "package": "swift-log",
60 | "repositoryURL": "https://github.com/apple/swift-log.git",
61 | "state": {
62 | "branch": null,
63 | "revision": "e8aabbe95db22e064ad42f1a4a9f8982664c70ed",
64 | "version": "1.1.1"
65 | }
66 | },
67 | {
68 | "package": "swift-nio",
69 | "repositoryURL": "https://github.com/apple/swift-nio.git",
70 | "state": {
71 | "branch": null,
72 | "revision": "32760eae40e6b7cb81d4d543bb0a9f548356d9a2",
73 | "version": "2.7.1"
74 | }
75 | },
76 | {
77 | "package": "swift-nio-extras",
78 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
79 | "state": {
80 | "branch": null,
81 | "revision": "66f9a509ed3cc56b6eb367515e421beca4a0af53",
82 | "version": "1.2.0"
83 | }
84 | },
85 | {
86 | "package": "swift-nio-http2",
87 | "repositoryURL": "https://github.com/apple/swift-nio-http2.git",
88 | "state": {
89 | "branch": null,
90 | "revision": "86ce1dcd0df501401eb1a0d445dbd90aaad84a64",
91 | "version": "1.5.0"
92 | }
93 | },
94 | {
95 | "package": "swift-nio-ssl",
96 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
97 | "state": {
98 | "branch": null,
99 | "revision": "f5dd7a60ff56f501ff7bf9be753e4b1875bfaf20",
100 | "version": "2.4.0"
101 | }
102 | },
103 | {
104 | "package": "vapor",
105 | "repositoryURL": "https://github.com/vapor/vapor.git",
106 | "state": {
107 | "branch": null,
108 | "revision": "35f8dc3df0976cff76945a03bfcb763e46a16440",
109 | "version": "4.0.0-alpha.3.1"
110 | }
111 | },
112 | {
113 | "package": "WebError",
114 | "repositoryURL": "https://github.com/Einstore/WebErrorKit.git",
115 | "state": {
116 | "branch": null,
117 | "revision": "183170b95c46ca5a900d0ea870980c9c280a573a",
118 | "version": "0.0.1"
119 | }
120 | },
121 | {
122 | "package": "XMLCoding",
123 | "repositoryURL": "https://github.com/LiveUI/XMLCoding.git",
124 | "state": {
125 | "branch": null,
126 | "revision": "8c760e960a5e53a5338c2871c4fcdf06b8c5ace4",
127 | "version": "0.4.0"
128 | }
129 | }
130 | ]
131 | },
132 | "version": 1
133 | }
134 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "S3Kit",
6 | products: [
7 | .library(name: "S3Kit", targets: ["S3Kit"]),
8 | .library(name: "S3Signer", targets: ["S3Signer"]),
9 | // .library(name: "S3TestTools", targets: ["S3TestTools"])
10 | ],
11 | dependencies: [
12 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.5.0"),
13 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-alpha.3"),
14 | .package(url: "https://github.com/vapor/open-crypto.git", from: "4.0.0-alpha.2"),
15 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0-alpha.2"),
16 | .package(url: "https://github.com/Einstore/HTTPMediaTypes.git", from: "0.0.1"),
17 | .package(url: "https://github.com/Einstore/WebErrorKit.git", from: "0.0.1"),
18 | .package(url: "https://github.com/LiveUI/XMLCoding.git", from: "0.1.0")
19 | ],
20 | targets: [
21 | .target(
22 | name: "S3Kit",
23 | dependencies: [
24 | "S3Signer",
25 | "AsyncHTTPClient",
26 | "HTTPMediaTypes",
27 | "XMLCoding"
28 | ]
29 | ),
30 | .target(
31 | name: "S3Provider",
32 | dependencies: [
33 | "Vapor",
34 | "S3Kit"
35 | ]
36 | ),
37 | .target(
38 | name: "S3DemoRun",
39 | dependencies: [
40 | "Vapor",
41 | "S3Provider"
42 | ]
43 | ),
44 | .target(
45 | name: "S3Signer",
46 | dependencies: [
47 | "OpenCrypto",
48 | "NIOHTTP1",
49 | "HTTPMediaTypes",
50 | "WebErrorKit"
51 | ]
52 | ),
53 | // .target(name: "S3TestTools", dependencies: [
54 | // "Vapor",
55 | // "S3Kit"
56 | // ]
57 | // ),
58 | .testTarget(name: "S3Tests", dependencies: [
59 | "S3Kit"
60 | ]
61 | )
62 | ]
63 | )
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.com/lluuaapp/S3)
2 |
3 | # S3 client for Vapor 3
4 |
5 | ## Functionality
6 |
7 | - [x] Signing headers for any region
8 | - [x] Listing buckets
9 | - [x] Create bucket
10 | - [x] Delete bucket
11 | - [x] Locate bucket region
12 | - [x] List objects
13 | - [x] Upload file
14 | - [x] Get file
15 | - [x] Delete file
16 | - [x] Copy file
17 | - [x] Move file (copy then delete old one)
18 | - [x] Object info (HEAD)
19 | - [ ] Object info (ACL)
20 | - [x] Parsing error responses
21 |
22 | ## Usage
23 |
24 | Update dependencies and targets in Package.swift
25 |
26 | ```swift
27 | dependencies: [
28 | ...
29 | .package(url: "https://github.com/LiveUI/S3.git", from: "3.0.0-RC3.2"),
30 | ],
31 | targets: [
32 | .target(name: "App", dependencies: ["Vapor", "S3"]),
33 | ...
34 | ]
35 | ```
36 |
37 | Run ```vapor update```
38 |
39 | Register S3Client as a service in your configure method
40 |
41 | ```swift
42 | try services.register(s3: S3Signer.Config(...), defaultBucket: "my-bucket")
43 | ```
44 |
45 | to use a custom Minio server, use this Config/Region:
46 |
47 | ```
48 | S3Signer.Config(accessKey: accessKey,
49 | secretKey: secretKey,
50 | region: Region(name: RegionName.usEast1,
51 | hostName: "127.0.0.1:9000",
52 | useTLS: false)
53 | ```
54 |
55 | use S3Client
56 |
57 | ```swift
58 | import S3
59 |
60 | let s3 = try req.makeS3Client() // or req.make(S3Client.self) as? S3
61 | s3.put(...)
62 | s3.get(...)
63 | s3.delete(...)
64 | ```
65 |
66 | if you only want to use the signer
67 |
68 | ```swift
69 | import S3Signer
70 |
71 | let s3 = try req.makeS3Signer() // or req.make(S3Signer.self)
72 | s3.headers(...)
73 | ```
74 |
75 | ### Available methods
76 |
77 | ```swift
78 | /// S3 client Protocol
79 | public protocol S3Client: Service {
80 |
81 | /// Get list of objects
82 | func buckets(on: Container) -> EventLoopFuture
83 |
84 | /// Create a bucket
85 | func create(bucket: String, region: Region?, on container: Container) -> EventLoopFuture
86 |
87 | /// Delete a bucket
88 | func delete(bucket: String, region: Region?, on container: Container) -> EventLoopFuture
89 |
90 | /// Get bucket location
91 | func location(bucket: String, on container: Container) -> EventLoopFuture
92 |
93 | /// Get list of objects
94 | func list(bucket: String, region: Region?, on container: Container) -> EventLoopFuture
95 |
96 | /// Get list of objects
97 | func list(bucket: String, region: Region?, headers: [String: String], on container: Container) -> EventLoopFuture
98 |
99 | /// Upload file to S3
100 | func put(file: File.Upload, headers: [String: String], on: Container) throws -> EventLoopEventLoopFuture
101 |
102 | /// Upload file to S3
103 | func put(file url: URL, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture
104 |
105 | /// Upload file to S3
106 | func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture
107 |
108 | /// Upload file to S3
109 | func put(file path: String, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture
110 |
111 | /// Upload file to S3
112 | func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture
113 |
114 | /// Upload file to S3
115 | func put(string: String, destination: String, on: Container) -> EventLoopFuture
116 |
117 | /// Upload file to S3
118 | func put(string: String, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture
119 |
120 | /// Upload file to S3
121 | func put(string: String, mime: MediaType, destination: String, on: Container) -> EventLoopFuture
122 |
123 | /// Upload file to S3
124 | func put(string: String, mime: MediaType, destination: String, access: AccessControlList, on: Container) -> EventLoopFuture
125 |
126 | /// Upload file to S3
127 | func put(string: String, mime: MediaType, destination: String, bucket: String?, access: AccessControlList, on: Container) -> EventLoopFuture
128 |
129 | /// Retrieve file data from S3
130 | func get(fileInfo file: LocationConvertible, on container: Container) -> EventLoopFuture
131 |
132 | /// Retrieve file data from S3
133 | func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) -> EventLoopFuture
134 |
135 | /// Retrieve file data from S3
136 | func get(file: LocationConvertible, on: Container) -> EventLoopFuture
137 |
138 | /// Retrieve file data from S3
139 | func get(file: LocationConvertible, headers: [String: String], on: Container) -> EventLoopFuture
140 |
141 | /// Delete file from S3
142 | func delete(file: LocationConvertible, on: Container) -> EventLoopFuture
143 |
144 | /// Delete file from S3
145 | func delete(file: LocationConvertible, headers: [String: String], on: Container) -> EventLoopFuture
146 | }
147 | ```
148 |
149 | ### Example usage
150 |
151 | ```swift
152 | public func routes(_ router: Router) throws {
153 |
154 | // Get all available buckets
155 | router.get("buckets") { req -> EventLoopFuture in
156 | let s3 = try req.makeS3Client()
157 | return try s3.buckets(on: req)
158 | }
159 |
160 | // Create new bucket
161 | router.put("bucket") { req -> EventLoopFuture in
162 | let s3 = try req.makeS3Client()
163 | return try s3.create(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) {
164 | return ":)"
165 | }.catchMap({ (error) -> (String) in
166 | if let error = error.s3ErrorMessage() {
167 | return error.message
168 | }
169 | return ":("
170 | }
171 | )
172 | }
173 |
174 | // Locate bucket (get region)
175 | router.get("bucket/location") { req -> EventLoopFuture in
176 | let s3 = try req.makeS3Client()
177 | return try s3.location(bucket: "bucket-name", on: req).map(to: String.self) { region in
178 | return region.hostUrlString()
179 | }.catchMap({ (error) -> (String) in
180 | if let error = error as? S3.Error {
181 | switch error {
182 | case .errorResponse(_, let error):
183 | return error.message
184 | default:
185 | return "S3 :("
186 | }
187 | }
188 | return ":("
189 | }
190 | )
191 | }
192 | // Delete bucket
193 | router.delete("bucket") { req -> EventLoopFuture in
194 | let s3 = try req.makeS3Client()
195 | return try s3.delete(bucket: "api-created-bucket", region: .euCentral1, on: req).map(to: String.self) {
196 | return ":)"
197 | }.catchMap({ (error) -> (String) in
198 | if let error = error.s3ErrorMessage() {
199 | return error.message
200 | }
201 | return ":("
202 | }
203 | )
204 | }
205 |
206 | // Get list of objects
207 | router.get("files") { req -> EventLoopFuture in
208 | let s3 = try req.makeS3Client()
209 | return try s3.list(bucket: "booststore", region: .usEast1, headers: [:], on: req).catchMap({ (error) -> (BucketResults) in
210 | if let error = error.s3ErrorMessage() {
211 | print(error.message)
212 | }
213 | throw error
214 | })
215 | }
216 |
217 | // Demonstrate work with files
218 | router.get("files/test") { req -> EventLoopFuture in
219 | let string = "Content of my example file"
220 |
221 | let fileName = "file-hu.txt"
222 |
223 | let s3 = try req.makeS3Client()
224 | do {
225 | // Upload a file from string
226 | return try s3.put(string: string, destination: fileName, access: .publicRead, on: req).flatMap(to: String.self) { putResponse in
227 | print("PUT response:")
228 | print(putResponse)
229 | // Get the content of the newly uploaded file
230 | return try s3.get(file: fileName, on: req).flatMap(to: String.self) { getResponse in
231 | print("GET response:")
232 | print(getResponse)
233 | print(String(data: getResponse.data, encoding: .utf8) ?? "Unknown content!")
234 | // Get info about the file (HEAD)
235 | return try s3.get(fileInfo: fileName, on: req).flatMap(to: String.self) { infoResponse in
236 | print("HEAD/Info response:")
237 | print(infoResponse)
238 | // Delete the file
239 | return try s3.delete(file: fileName, on: req).map() { response in
240 | print("DELETE response:")
241 | print(response)
242 | let json = try JSONEncoder().encode(infoResponse)
243 | return String(data: json, encoding: .utf8) ?? "Unknown content!"
244 | }.catchMap({ error -> (String) in
245 | if let error = error.s3ErrorMessage() {
246 | return error.message
247 | }
248 | return ":("
249 | }
250 | )
251 | }
252 | }
253 | }
254 | } catch {
255 | print(error)
256 | fatalError()
257 | }
258 | }
259 | }
260 | ```
261 |
262 | ## Support
263 |
264 | Join our [Slack](http://bit.ly/2B0dEyt), channel #help-boost to ... well, get help :)
265 |
266 | ## Einstore AppStore
267 |
268 | Core package for [Einstore](http://www.einstore.io), a completely open source enterprise AppStore written in Swift!
269 | - Website: http://www.einstore.io
270 | - Github: https://github.com/Einstore/Einstore
271 |
272 | ## Other core packages
273 |
274 | * [EinstoreCore](https://github.com/Einstore/EinstoreCore/) - AppStore core module
275 | * [ApiCore](https://github.com/LiveUI/ApiCore/) - API core module with users and team management
276 | * [MailCore](https://github.com/LiveUI/MailCore/) - Mailing wrapper for multiple mailing services like MailGun, SendGrig or SMTP (coming)
277 | * [DBCore](https://github.com/LiveUI/DbCore/) - Set of tools for work with PostgreSQL database
278 | * [VaporTestTools](https://github.com/LiveUI/VaporTestTools) - Test tools and helpers for Vapor 3
279 |
280 | ## Code contributions
281 |
282 | We love PR’s, we can’t get enough of them ... so if you have an interesting improvement, bug-fix or a new feature please don’t hesitate to get in touch. If you are not sure about something before you start the development you can always contact our dev and product team through our Slack.
283 |
284 | ## Credits
285 |
286 | #### Author
287 | Ondrej Rafaj (@rafiki270 on [Github](https://github.com/rafiki270), [Twitter](https://twitter.com/rafiki270), [LiveUI Slack](http://bit.ly/2B0dEyt) and [Vapor Slack](https://vapor.team/))
288 |
289 | #### Thanks
290 | Anthoni Castelli (@anthonycastelli on [Github](https://github.com/anthonycastelli), @anthony on [Vapor Slack](https://vapor.team/)) for his help on updating S3Signer for Vapor3
291 |
292 | JustinM1 (@JustinM1 on [Github](https://github.com/JustinM1)) for his amazing original signer package
293 |
294 | ## License
295 |
296 | See the LICENSE file for more info.
297 |
--------------------------------------------------------------------------------
/Sources/S3DemoRun/main.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import S3Provider
3 |
4 |
5 | let DEFAULT_BUCKET = "s3-lib-test.einstore.mgw.cz"
6 |
7 |
8 | func routes(_ router: Routes, _ c: Container) throws {
9 | guard let key = Environment.get("S3_ACCESS_KEY"), let secret = Environment.get("S3_SECRET") else {
10 | fatalError("Missing AWS API key/secret")
11 | }
12 |
13 | let config = S3Signer.Config(accessKey: key, secretKey: secret, region: Region.euCentral1)
14 | let s3: S3Client = try S3(defaultBucket: DEFAULT_BUCKET, config: config)
15 |
16 | // Get all available buckets
17 | router.get("buckets") { req -> EventLoopFuture in
18 | return s3.buckets(on: req.eventLoop)
19 | }
20 |
21 | // Create new bucket
22 | router.put("bucket") { req -> EventLoopFuture in
23 | return s3.create(bucket: "api-created-bucket", region: .euCentral1, on: req.eventLoop).map {
24 | return ":)"
25 | }.recover { error in
26 | if let error = error.s3ErrorMessage() {
27 | return error.message
28 | }
29 | return ":("
30 | }
31 | }
32 |
33 | // Delete bucket
34 | router.delete("bucket") { req -> EventLoopFuture in
35 | return s3.delete(bucket: "api-created-bucket", region: .euCentral1, on: req.eventLoop).map {
36 | return ":)"
37 | }.recover { error in
38 | if let error = error.s3ErrorMessage() {
39 | return error.message
40 | }
41 | return ":("
42 | }
43 | }
44 |
45 | // Delete bucket
46 | router.get("files") { req -> EventLoopFuture in
47 | return s3.list(bucket: DEFAULT_BUCKET, region: .euCentral1, headers: [:], on: req.eventLoop).flatMapErrorThrowing { error in
48 | if let error = error.s3ErrorMessage() {
49 | print(error.message)
50 | }
51 |
52 | throw error
53 | }
54 | }
55 |
56 | // Bucket location
57 | router.get("bucket", "location") { req -> EventLoopFuture in
58 | return s3.location(bucket: DEFAULT_BUCKET, on: req.eventLoop).map { region in
59 | return region.hostUrlString()
60 | }.recover { error -> String in
61 | if let error = error as? S3.Error {
62 | switch error {
63 | case .errorResponse(_, let error):
64 | return error.message
65 | default:
66 | return "S3 :("
67 | }
68 | }
69 | return ":("
70 | }
71 | }
72 |
73 | // Demonstrate work with files
74 | router.get("files", "test") { req -> EventLoopFuture in
75 | let string = "Content of my example file"
76 |
77 | let fileName = "file-hu.txt"
78 | return s3.put(string: string, destination: fileName, access: .publicRead, on: req.eventLoop).flatMap { putResponse -> EventLoopFuture in
79 | print("PUT response:")
80 | print(putResponse)
81 | return s3.get(file: fileName, on: req.eventLoop).flatMap { getResponse in
82 | print("GET response:")
83 | print(getResponse)
84 | print(String(data: getResponse.data, encoding: .utf8) ?? "Unknown content!")
85 |
86 | return s3.get(fileInfo: fileName, on: req.eventLoop).flatMap { infoResponse in
87 | print("HEAD/Info response:")
88 | print(infoResponse)
89 |
90 | return s3.delete(file: fileName, on: req.eventLoop).flatMapThrowing { response in
91 | print("DELETE response:")
92 | print(response)
93 | let json = try JSONEncoder().encode(infoResponse)
94 | return String(data: json, encoding: .utf8) ?? "Unknown content!"
95 | }.recover { error -> (String) in
96 | if let error = error.s3ErrorMessage() {
97 | return error.message
98 | }
99 | return ":("
100 | }
101 | }
102 | }
103 | }.recover { error -> (String) in
104 | if let error = error.s3ErrorMessage() {
105 | return error.message
106 | }
107 | return ":("
108 | }
109 | }
110 | }
111 |
112 | /// Called before your application initializes.
113 | func configure(_ s: inout Services) throws {
114 | /// Register routes
115 | s.extend(Routes.self) { r, c in
116 | try routes(r, c)
117 | }
118 |
119 | /// Register middleware
120 | s.register(MiddlewareConfiguration.self) { c in
121 | // Create _empty_ middleware config
122 | var middlewares = MiddlewareConfiguration()
123 |
124 | // Serves files from `Public/` directory
125 | /// middlewares.use(FileMiddleware.self)
126 |
127 | // Catches errors and converts to HTTP response
128 | try middlewares.use(c.make(ErrorMiddleware.self))
129 |
130 | return middlewares
131 | }
132 | }
133 |
134 | func boot(_ app: Application) throws {
135 | try LoggingSystem.bootstrap(from: &app.environment)
136 | try app.boot()
137 | }
138 |
139 | public func app(_ environment: Environment) throws -> Application {
140 | let app = Application.init(environment: environment) { s in
141 | try configure(&s)
142 | }
143 | try boot(app)
144 | return app
145 | }
146 |
147 | try app(.detect()).run()
148 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Exports.swift:
--------------------------------------------------------------------------------
1 | @_exported import NIOHTTP1
2 | @_exported import S3Signer
3 | @_exported import class NIO.EventLoopFuture
4 | @_exported import protocol NIO.EventLoop
5 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/Error+S3.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | extension Error {
5 |
6 | /// Return S3 Error if possible
7 | public func s3Error() -> S3.Error? {
8 | guard let error = self as? S3.Error else {
9 | return nil
10 | }
11 | return error
12 | }
13 |
14 | /// Return S3 ErrorMessage if possible
15 | public func s3ErrorMessage() -> ErrorMessage? {
16 | guard let error = self as? S3.Error else {
17 | return nil
18 | }
19 | switch error {
20 | case .errorResponse(_, let errorMessage):
21 | return errorMessage
22 | default:
23 | return nil
24 | }
25 | }
26 |
27 | /// Return S3 error status code if possible
28 | public func s3ErrorCode() -> HTTPResponseStatus? {
29 | guard let error = self as? S3.Error else {
30 | return nil
31 | }
32 | switch error {
33 | case .errorResponse(let errorCode, _):
34 | return errorCode
35 | default:
36 | return nil
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/HTTPHeaders+Tools.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | extension HTTPHeaders {
5 |
6 | func string(_ name: String) -> String? {
7 | return self[name].first
8 | }
9 |
10 | func int(_ name: String) -> Int? {
11 | guard let headerValue = string(name) else {
12 | return nil
13 | }
14 | return Int(headerValue)
15 | }
16 |
17 | func date(_ name: String) -> Date? {
18 | guard let headerValue = string(name) else {
19 | return nil
20 | }
21 | return S3.headerDateFormatter.date(from: headerValue)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/Region+Tools.swift:
--------------------------------------------------------------------------------
1 | import S3Signer
2 | import Foundation
3 |
4 |
5 | extension Region {
6 |
7 | /// Get S3 URL string for bucket
8 | public func urlString(bucket: String) -> String {
9 | return host.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + bucket
10 | }
11 |
12 | /// Get S3 URL for bucket
13 | public func url(bucket: String) -> URL? {
14 | return URL(string: urlString(bucket: bucket))
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/Response+XMLDecoding.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XMLCoding
3 | import AsyncHTTPClient
4 |
5 |
6 | extension HTTPClient.Response {
7 |
8 | func decode(to: T.Type) throws -> T where T: Decodable {
9 | guard var b = body, let data = b.readBytes(length: b.readableBytes) else {
10 | throw S3.Error.badResponse(self)
11 | }
12 |
13 | let decoder = XMLDecoder()
14 | decoder.dateDecodingStrategy = .formatted(S3.dateFormatter)
15 | return try decoder.decode(T.self, from: Data(data))
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Bucket.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 | import S3Signer
4 |
5 |
6 | // Helper S3 extension for working with buckets
7 | extension S3 {
8 |
9 | // MARK: Buckets
10 |
11 | /// Get bucket location
12 | public func location(bucket: String, on eventLoop: EventLoop) -> EventLoopFuture {
13 | let url: URL
14 | let awsHeaders: HTTPHeaders
15 | let region = Region.euWest2
16 |
17 | do {
18 | url = try makeURLBuilder().url(region: region, bucket: bucket, path: nil)
19 | awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, bucket: bucket, payload: .none)
20 | } catch let error {
21 | return eventLoop.makeFailedFuture(error)
22 | }
23 |
24 | return make(request: url, method: .GET, headers: awsHeaders, data: Data(), on: eventLoop).flatMapThrowing { response in
25 | if response.status == .notFound {
26 | throw Error.notFound
27 | }
28 | if response.status == .ok {
29 | return region
30 | } else {
31 | if let error = try? response.decode(to: ErrorMessage.self), error.code == "PermanentRedirect", let endpoint = error.endpoint {
32 | if endpoint == "s3.amazonaws.com" {
33 | return Region.usEast1
34 | } else {
35 | // Split bucket.s3.region.amazonaws.com into parts
36 | // Drop .com and .amazonaws
37 | // Get region (last part)
38 | guard let regionString = endpoint.split(separator: ".").dropLast(2).last?.lowercased() else {
39 | throw Error.badResponse(response)
40 | }
41 | return Region(name: .init(regionString))
42 | }
43 | } else {
44 | throw Error.badResponse(response)
45 | }
46 | }
47 | }
48 | }
49 |
50 | /// Delete bucket
51 | public func delete(bucket: String, region: Region? = nil, on eventLoop: EventLoop) -> EventLoopFuture {
52 | let url: URL
53 | let awsHeaders: HTTPHeaders
54 |
55 | do {
56 | url = try makeURLBuilder().url(region: region, bucket: bucket, path: nil)
57 | awsHeaders = try signer.headers(for: .DELETE, urlString: url.absoluteString, region: region, bucket: bucket, payload: .none)
58 | } catch let error {
59 | return eventLoop.makeFailedFuture(error)
60 | }
61 |
62 | return make(request: url, method: .DELETE, headers: awsHeaders, data: Data(), on: eventLoop).flatMapThrowing(self.check).map { _ in
63 | return Void()
64 | }
65 | }
66 |
67 | /// Create a bucket
68 | public func create(bucket: String, region: Region? = nil, on eventLoop: EventLoop) -> EventLoopFuture {
69 | let region = region ?? signer.config.region
70 | let content = """
71 |
72 | \(region.name)
73 |
74 | """
75 | let data = Data(content.utf8)
76 |
77 | let awsHeaders: HTTPHeaders
78 | let url: URL
79 |
80 | do {
81 | url = try makeURLBuilder().url(region: region, bucket: bucket, path: nil)
82 | awsHeaders = try signer.headers(for: .PUT, urlString: url.absoluteString, region: region, bucket: bucket, payload: .bytes(data))
83 | } catch let error {
84 | return eventLoop.makeFailedFuture(error)
85 | }
86 |
87 | return make(request: url, method: .PUT, headers: awsHeaders, data: data, on: eventLoop).flatMapThrowing(self.check).map { _ in
88 | return Void()
89 | }
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Copy.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 | import AsyncHTTPClient
4 |
5 |
6 | extension S3 {
7 |
8 | // MARK: Copy
9 |
10 | /// Copy file on S3
11 | public func copy(file: LocationConvertible, to: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
12 | do {
13 | let destinationUrl = try makeURLBuilder().url(file: to)
14 |
15 | var awsHeaders: [String: String] = strHeaders
16 | awsHeaders["x-amz-copy-source"] = "\(file.bucket ?? defaultBucket)/\(file.path)"
17 | let headers = try signer.headers(
18 | for: .PUT,
19 | urlString: destinationUrl.absoluteString,
20 | headers: awsHeaders,
21 | payload: .none
22 | )
23 |
24 | return make(request: destinationUrl, method: .PUT, headers: headers, on: eventLoop).flatMapThrowing { response in
25 | return try self.check(response).decode(to: File.CopyResponse.self)
26 | }
27 | } catch let error {
28 | return eventLoop.makeFailedFuture(error)
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Delete.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 |
4 |
5 | // Helper S3 extension for deleting files by their URL/path
6 | extension S3 {
7 |
8 | // MARK: Delete
9 |
10 | /// Delete file from S3
11 | public func delete(file: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
12 | let headers: HTTPHeaders
13 | let url: URL
14 |
15 | do {
16 | url = try makeURLBuilder().url(file: file)
17 | headers = try signer.headers(for: .DELETE, urlString: url.absoluteString, headers: strHeaders, payload: .none)
18 | } catch let error {
19 | return eventLoop.makeFailedFuture(error)
20 | }
21 |
22 | return make(request: url, method: .DELETE, headers: headers, data: nil, on: eventLoop).flatMapThrowing(self.check).map { _ in
23 | return Void()
24 | }
25 | }
26 |
27 | /// Delete file from S3
28 | public func delete(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture {
29 | return
30 | delete(file: file, headers: [:], on: eventLoop)
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Get.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 |
4 | // Helper S3 extension for loading (getting) files by their URL/path
5 | extension S3 {
6 |
7 | // MARK: URL
8 |
9 | /// File URL
10 | public func url(fileInfo file: LocationConvertible) throws -> URL {
11 | let builder = makeURLBuilder()
12 | let url = try builder.url(file: file)
13 | return url
14 | }
15 |
16 | // MARK: Get
17 |
18 | /// Retrieve file data from S3
19 | public func get(file: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
20 | let url: URL
21 | let headers: HTTPHeaders
22 |
23 | do {
24 | url = try makeURLBuilder().url(file: file)
25 | headers = try signer.headers(for: .GET, urlString: url.absoluteString, headers: strHeaders, payload: .none)
26 | } catch let error {
27 | return eventLoop.makeFailedFuture(error)
28 | }
29 |
30 | return make(request: url, method: .GET, headers: headers, on: eventLoop).flatMapThrowing { response in
31 | try self.check(response)
32 |
33 | guard var b = response.body, let data = b.readBytes(length: b.readableBytes) else {
34 | throw Error.missingData
35 | }
36 |
37 | let res = File.Response(data: Data(data), bucket: file.bucket ?? self.defaultBucket, path: file.path, access: nil, mime: self.mimeType(forFileAtUrl: url))
38 | return res
39 | }
40 | }
41 |
42 | /// Retrieve file data from S3
43 | public func get(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture {
44 | return get(file: file, headers: [:], on: eventLoop)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+List.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 | import NIOHTTP1
4 |
5 |
6 | // Helper S3 extension for getting file indexes
7 | extension S3 {
8 |
9 | /// Get list of objects
10 | public func list(bucket: String, region: Region? = nil, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
11 | let region = region ?? signer.config.region
12 | guard let baseUrl = URL(string: region.hostUrlString(bucket: bucket)), let host = baseUrl.host,
13 | var components = URLComponents(string: baseUrl.absoluteString) else {
14 | return eventLoop.makeFailedFuture(S3.Error.invalidUrl)
15 | }
16 | components.queryItems = [
17 | URLQueryItem(name: "list-type", value: "2")
18 | ]
19 | guard let url = components.url else {
20 | return eventLoop.makeFailedFuture(S3.Error.invalidUrl)
21 | }
22 |
23 | let awsHeaders: HTTPHeaders
24 |
25 | do {
26 | var headers = headers
27 | headers["host"] = host
28 | awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, bucket: bucket, headers: headers, payload: .none)
29 | } catch let error {
30 | return eventLoop.makeFailedFuture(error)
31 | }
32 |
33 | return make(request: url, method: .GET, headers: awsHeaders, data: Data(), on: eventLoop).flatMapThrowing { response in
34 | try self.check(response)
35 | return try response.decode(to: BucketResults.self)
36 | }
37 | }
38 |
39 | /// Get list of objects
40 | public func list(bucket: String, region: Region? = nil, on eventLoop: EventLoop) -> EventLoopFuture {
41 | return list(bucket: bucket, region: region, headers: [:], on: eventLoop)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Move.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 |
4 |
5 | extension S3 {
6 |
7 | // MARK: Move
8 |
9 | /// Copy file on S3
10 | public func move(file: LocationConvertible, to destination: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
11 | return copy(file: file, to: destination, headers: headers, on: eventLoop).flatMap { copyResult in
12 | return self.delete(file: file, on: eventLoop).map { _ in
13 | return copyResult
14 | }
15 | }
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+ObjectInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import S3Signer
3 |
4 |
5 | // Helper S3 extension for working with buckets
6 | extension S3 {
7 |
8 | // MARK: Buckets
9 |
10 | /// Get acl file information (ACL)
11 | /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGETacl.html
12 | public func get(acl file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
13 | fatalError("Not implemented")
14 | }
15 |
16 | /// Get acl file information
17 | /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGETacl.html
18 | func get(acl file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture {
19 | return get(fileInfo: file, headers: [:], on: eventLoop)
20 | }
21 |
22 | /// Get file information (HEAD)
23 | /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html
24 | public func get(fileInfo file: LocationConvertible, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
25 | let url: URL
26 | let headers: HTTPHeaders
27 |
28 | do {
29 | url = try makeURLBuilder().url(file: file)
30 | headers = try signer.headers(for: .HEAD, urlString: url.absoluteString, headers: strHeaders, payload: .none)
31 | } catch let error {
32 | return eventLoop.makeFailedFuture(error)
33 | }
34 |
35 | return make(request: url, method: .HEAD, headers: headers, data: Data(), on: eventLoop).flatMapThrowing { response in
36 | try self.check(response)
37 |
38 | let bucket = file.bucket ?? self.defaultBucket
39 | let region = file.region ?? self.signer.config.region
40 | let mime = response.headers.string(File.Info.CodingKeys.mime.rawValue)
41 | let size = response.headers.int(File.Info.CodingKeys.size.rawValue)
42 | let server = response.headers.string(File.Info.CodingKeys.server.rawValue)
43 | let etag = response.headers.string(File.Info.CodingKeys.etag.rawValue)
44 | let expiration = response.headers.date(File.Info.CodingKeys.expiration.rawValue)
45 | let created = response.headers.date(File.Info.CodingKeys.created.rawValue)
46 | let modified = response.headers.date(File.Info.CodingKeys.modified.rawValue)
47 | let versionId = response.headers.string(File.Info.CodingKeys.versionId.rawValue)
48 | let storageClass = response.headers.string(File.Info.CodingKeys.storageClass.rawValue)
49 |
50 | let info = File.Info(bucket: bucket, region: region, path: file.path, access: .authenticatedRead, mime: mime, size: size, server: server, etag: etag, expiration: expiration, created: created, modified: modified, versionId: versionId, storageClass: storageClass)
51 | return info
52 | }
53 | }
54 |
55 | /// Get file information (HEAD)
56 | /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html
57 | public func get(fileInfo file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture {
58 | return get(fileInfo: file, headers: [:], on: eventLoop)
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Put.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | // Helper S3 extension for uploading files by their URL/path
5 | extension S3 {
6 |
7 | // MARK: Upload
8 |
9 | /// Upload file to S3
10 | public func put(file: File.Upload, headers strHeaders: [String: String], on eventLoop: EventLoop) -> EventLoopFuture {
11 | let headers: HTTPHeaders
12 | let url: URL
13 |
14 | do {
15 | url = try makeURLBuilder().url(file: file)
16 |
17 | var awsHeaders: [String: String] = strHeaders
18 | awsHeaders["Content-Type"] = file.mime.description
19 | awsHeaders["x-amz-acl"] = file.access.rawValue
20 | headers = try signer.headers(
21 | for: .PUT,
22 | urlString: url.absoluteString,
23 | headers: awsHeaders,
24 | payload: .bytes(file.data)
25 | )
26 | } catch let error {
27 | return eventLoop.makeFailedFuture(error)
28 | }
29 |
30 | return make(request: url, method: .PUT, headers: headers, data: file.data, on: eventLoop).flatMapThrowing { response in
31 | try self.check(response)
32 | let res = File.Response(data: file.data, bucket: file.bucket ?? self.defaultBucket, path: file.path, access: file.access, mime: file.mime)
33 | return res
34 | }
35 | }
36 |
37 | /// Upload file to S3
38 | public func put(file: File.Upload, on eventLoop: EventLoop) -> EventLoopFuture {
39 | return put(file: file, headers: [:], on: eventLoop)
40 | }
41 |
42 | /// Upload file by it's URL to S3
43 | public func put(file url: URL, destination: String, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture {
44 | let data: Data
45 | do {
46 | data = try Data(contentsOf: url)
47 | } catch let error {
48 | return eventLoop.makeFailedFuture(error)
49 | }
50 |
51 | let file = File.Upload(data: data, bucket: nil, destination: destination, access: access, mime: mimeType(forFileAtUrl: url))
52 | return put(file: file, on: eventLoop)
53 | }
54 |
55 | /// Upload file by it's path to S3
56 | public func put(file path: String, destination: String, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture {
57 | let url: URL = URL(fileURLWithPath: path)
58 | return put(file: url, destination: destination, bucket: nil, access: access, on: eventLoop)
59 | }
60 |
61 | /// Upload file by it's URL to S3, full set
62 | public func put(file url: URL, destination: String, bucket: String?, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture {
63 | let data: Data
64 | do {
65 | data = try Data(contentsOf: url)
66 | } catch let error {
67 | return eventLoop.makeFailedFuture(error)
68 | }
69 |
70 | let file = File.Upload(data: data, bucket: bucket, destination: destination, access: access, mime: mimeType(forFileAtUrl: url))
71 | return put(file: file, on: eventLoop)
72 | }
73 |
74 | /// Upload file by it's path to S3, full set
75 | public func put(file path: String, destination: String, bucket: String?, access: AccessControlList = .privateAccess, on eventLoop: EventLoop) -> EventLoopFuture {
76 | let url: URL = URL(fileURLWithPath: path)
77 | return put(file: url, destination: destination, bucket: bucket, access: access, on: eventLoop)
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Request.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 | import AsyncHTTPClient
4 |
5 |
6 | extension S3 {
7 |
8 | /// Make an S3 request
9 | func make(request url: URL, method: HTTPMethod, headers: HTTPHeaders, data: Data? = nil, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, on eventLoop: EventLoop) -> EventLoopFuture {
10 | do {
11 | let body: HTTPClient.Body?
12 | if let data = data {
13 | body = HTTPClient.Body.data(data)
14 | } else {
15 | body = nil
16 | }
17 |
18 | var headers = headers
19 | headers.add(name: "User-Agent", value: "S3Kit-for-Swift")
20 | headers.add(name: "Accept", value: "*/*")
21 | headers.add(name: "Connection", value: "keep-alive")
22 | headers.add(name: "Content-Length", value: String(data?.count ?? 0))
23 |
24 | let request = try HTTPClient.Request(
25 | url: url.absoluteString,
26 | method: method,
27 | headers: headers,
28 | body: body
29 | )
30 | let client = HTTPClient(eventLoopGroupProvider: .shared(eventLoop))
31 | return client.execute(request: request)
32 | } catch {
33 | return eventLoop.makeFailedFuture(error)
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Service.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | // Helper S3 extension for working with services
5 | extension S3 {
6 |
7 | // MARK: Buckets
8 |
9 | /// Get list of buckets
10 | public func buckets(on eventLoop: EventLoop) -> EventLoopFuture {
11 | let headers: HTTPHeaders
12 | let url: URL
13 |
14 | do {
15 | url = try makeURLBuilder().plain(region: nil)
16 | headers = try signer.headers(for: .GET, urlString: url.absoluteString, payload: .none)
17 | } catch let error {
18 | return eventLoop.makeFailedFuture(error)
19 | }
20 |
21 | return make(request: url, method: .GET, headers: headers, data: Data(), on: eventLoop).flatMapThrowing { response in
22 | try self.check(response)
23 | return try response.decode(to: BucketsInfo.self)
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/S3+Strings.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import HTTPMediaTypes
3 |
4 |
5 | extension S3 {
6 |
7 | /// Upload file content to S3, full set
8 | public func put(string: String, mime: HTTPMediaType, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture {
9 | guard let data: Data = string.data(using: String.Encoding.utf8) else {
10 | return eventLoop.makeFailedFuture(Error.badStringData)
11 | }
12 | let file = File.Upload(data: data, bucket: bucket, destination: destination, access: access, mime: mime.description)
13 | return put(file: file, on: eventLoop)
14 | }
15 |
16 | /// Upload file content to S3
17 | public func put(string: String, mime: HTTPMediaType, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture {
18 | return put(string: string, mime: mime, destination: destination, bucket: nil, access: access, on: eventLoop)
19 | }
20 |
21 | /// Upload file content to S3
22 | public func put(string: String, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture {
23 | return put(string: string, mime: .plainText, destination: destination, bucket: nil, access: access, on: eventLoop)
24 | }
25 |
26 | /// Upload file content to S3
27 | public func put(string: String, mime: HTTPMediaType, destination: String, on eventLoop: EventLoop) -> EventLoopFuture {
28 | return put(string: string, mime: mime, destination: destination, access: .privateAccess, on: eventLoop)
29 | }
30 |
31 | /// Upload file content to S3
32 | public func put(string: String, destination: String, on eventLoop: EventLoop) -> EventLoopFuture {
33 | return put(string: string, mime: .plainText, destination: destination, bucket: nil, access: .privateAccess, on: eventLoop)
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Extensions/String+Tools.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | extension String {
5 |
6 | func finished(with string: String) -> String {
7 | if let last = last, String(last) == string {
8 | return self
9 | }
10 | return appending(string)
11 | }
12 |
13 | var bytes: [UInt8] { .init(utf8) }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/AccessControlList.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | /// Available access control list values for "x-amz-acl" header as specified in AWS documentation
5 | public enum AccessControlList: String, Codable {
6 |
7 | /// Owner gets FULL_CONTROL. No one else has access rights (default).
8 | case privateAccess = "private"
9 |
10 | /// Owner gets FULL_CONTROL. The AllUsers group (see Who Is a Grantee? at https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#specifying-grantee) gets READ access.
11 | case publicRead = "public-read"
12 |
13 | /// Owner gets FULL_CONTROL. The AllUsers group gets READ and WRITE access. Granting this on a bucket is generally not recommended.
14 | case publicReadWrite = "public-read-write"
15 |
16 | /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an Amazon Machine Image (AMI) bundle from Amazon S3.
17 | case awsExecRead = "aws-exec-read"
18 |
19 | /// Owner gets FULL_CONTROL. The AuthenticatedUsers group gets READ access.
20 | case authenticatedRead = "authenticated-read"
21 |
22 | /// Object owner gets FULL_CONTROL. Bucket owner gets READ access. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it.
23 | case bucketOwnerRead = "bucket-owner-read"
24 |
25 | /// Both the object owner and the bucket owner get FULL_CONTROL over the object. If you specify this canned ACL when creating a bucket, Amazon S3 ignores it.
26 | case bucketOwnerFullControl = "bucket-owner-full-control"
27 |
28 | /// The LogDelivery group gets WRITE and READ_ACP permissions on the bucket. For more information about logs, see (https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerLogs.html).
29 | case logDeliveryWrite = "log-delivery-write"
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/Bucket.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | /// Bucket model
5 | public struct Bucket: Codable {
6 |
7 | /// Creating new bucket
8 | public struct New: Codable {
9 |
10 | /// Name of the new bucket
11 | public let name: String
12 |
13 | /// New bucket initializer
14 | public init(name: String) {
15 | self.name = name
16 | }
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case name = "Name"
20 | }
21 |
22 | }
23 |
24 | /// Bucket location object
25 | public struct Location: Codable {
26 |
27 | /// Location of the bucket
28 | public let region: String
29 |
30 | enum CodingKeys: String, CodingKey {
31 | case region = "LocationConstraint"
32 | }
33 |
34 | }
35 |
36 | /// Name of a bucket
37 | public let name: String
38 |
39 | /// Bucket creation date
40 | public let created: Date
41 |
42 | enum CodingKeys: String, CodingKey {
43 | case name = "Name"
44 | case created = "CreationDate"
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/BucketResults.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | public struct BucketResults: Codable {
5 |
6 | /// Name of the bucket
7 | public let name: String
8 |
9 | /// Keys that begin with the indicated prefix
10 | public let prefix: String?
11 |
12 | /**
13 | All of the keys rolled up into a common prefix count as a single return when calculating the number of returns. See MaxKeys.
14 |
15 | A response can contain CommonPrefixes only if you specify a delimiter.
16 | CommonPrefixes contains all (if there are any) keys between Prefix and the next occurrence of the string specified by a delimiter.
17 | CommonPrefixes lists keys that act like subdirectories in the directory specified by Prefix.
18 | For example, if the prefix is notes/ and the delimiter is a slash (/) as in notes/summer/july, the common prefix is notes/summer/. All of the keys that roll up into a common prefix count as a single return when calculating the number of returns. See MaxKeys
19 | */
20 | public let commonPrefixes: [CommonPrefix]?
21 |
22 | /// Returns the number of keys included in the response. The value is always less than or equal to the MaxKeys value
23 | public let keyCount: Int?
24 |
25 | /// The maximum number of keys returned in the response body
26 | public let maxKeys: Int
27 |
28 | /// Causes keys that contain the same string between the prefix and the first occurrence of the delimiter to be rolled up into a single result element in the CommonPrefixes collection. These rolled-up keys are not returned elsewhere in the response. Each rolled-up result counts as only one return against the MaxKeys value
29 | public let delimiter: String?
30 |
31 | /// Encoding type used by Amazon S3 to encode object key names in the XML response
32 | public let encodingType: String?
33 |
34 | /// Pagination; If StartAfter was sent with the request, it is included in the response
35 | public let startAfter: String?
36 |
37 | /// If the response is truncated, Amazon S3 returns this parameter with a continuation token. You can specify the token as the continuation-token in your next request to retrieve the next set of keys
38 | public let nextContinuationToken: String?
39 |
40 | /// Set to false if all of the results were returned. Set to true if more keys are available to return. If the number of results exceeds that specified by MaxKeys, all of the results might not be returned
41 | public let isTruncated: Bool
42 |
43 | /// Objects
44 | public let objects: [Object]?
45 |
46 | enum CodingKeys: String, CodingKey {
47 | case name = "Name"
48 | case prefix = "Prefix"
49 | case commonPrefixes = "CommonPrefixes"
50 | case keyCount = "KeyCount"
51 | case maxKeys = "MaxKeys"
52 | case delimiter = "Delimiter"
53 | case encodingType = "Encoding-Type"
54 | case startAfter = "StartAfter"
55 | case nextContinuationToken = "NextContinuationToken"
56 | case isTruncated = "IsTruncated"
57 | case objects = "Contents"
58 | }
59 |
60 | }
61 |
62 |
63 | public struct CommonPrefix: Codable {
64 |
65 | /// Common prefix name
66 | let path: String
67 |
68 | enum CodingKeys: String, CodingKey {
69 | case path = "Prefix"
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/BucketsInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | /// Base object for /buckets endpoint
5 | public struct BucketsInfo: Codable {
6 |
7 | /// Owner
8 | public let owner: Owner?
9 |
10 | /// Available buckets
11 | public let buckets: [Bucket]?
12 |
13 | /// Max keys
14 | public let maxKeys: Int?
15 |
16 | /// Max keys
17 | public let truncated: Bool?
18 |
19 | /// Coding keys
20 | enum CodingKeys: String, CodingKey {
21 | case owner = "Owner"
22 | case additionalInfo = "Buckets"
23 | case maxKeys = "MaxKeys"
24 | case truncated = "IsTruncated"
25 | }
26 |
27 | /// Additional (helper) coding keys
28 | enum AdditionalInfoKeys: String, CodingKey {
29 | case buckets = "Bucket"
30 | }
31 |
32 | /// Init from Decoder
33 | public init(from decoder: Decoder) throws {
34 | let values = try decoder.container(keyedBy: CodingKeys.self)
35 | owner = try? values.decode(Owner.self, forKey: .owner)
36 | // TODO: Make the following better!!!!!!
37 | maxKeys = Int((try? values.decode(String.self, forKey: .maxKeys)) ?? "1000") ?? 1000
38 | truncated = Bool((try? values.decode(String.self, forKey: .truncated)) ?? "false") ?? false
39 |
40 | if let additionalInfo = try? values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo) {
41 | buckets = try? additionalInfo.decode([Bucket].self, forKey: .buckets)
42 | } else {
43 | buckets = nil
44 | }
45 | }
46 |
47 | /// Encode
48 | public func encode(to encoder: Encoder) throws {
49 | var container = encoder.container(keyedBy: CodingKeys.self)
50 | try container.encode(owner, forKey: .owner)
51 |
52 | var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
53 |
54 | try additionalInfo.encode(buckets, forKey: .buckets)
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/ErrorMessage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | /// Generic response error message
5 | public struct ErrorMessage: Codable {
6 |
7 | /// Error code
8 | public let code: String
9 |
10 | /// Error message
11 | public let message: String
12 |
13 | /// Bucket involved?
14 | public let bucket: String?
15 |
16 | /// Header involved?
17 | public let endpoint: String?
18 |
19 | /// Header involved?
20 | public let header: String?
21 |
22 | /// Request Id
23 | public let requestId: String?
24 |
25 | /// Host Id
26 | public let hostId: String?
27 |
28 | enum CodingKeys: String, CodingKey {
29 | case code = "Code"
30 | case message = "Message"
31 | case bucket = "BucketName"
32 | case endpoint = "Endpoint"
33 | case header = "Header"
34 | case requestId = "RequestId"
35 | case hostId = "HostId"
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/File.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import HTTPMediaTypes
3 |
4 |
5 | /// File data
6 | public struct File {
7 |
8 | /// File to be uploaded (PUT)
9 | public struct Upload: LocationConvertible {
10 |
11 | /// Data
12 | public internal(set) var data: Data
13 |
14 | /// Override target bucket
15 | public internal(set) var bucket: String?
16 |
17 | /// S3 file path
18 | public internal(set) var path: String
19 |
20 | /// S3 Region
21 | public internal(set) var region: Region?
22 |
23 | /// Desired access control for file
24 | public internal(set) var access: AccessControlList = .privateAccess
25 |
26 | /// Desired file type (mime) for the uploaded file
27 | public internal(set) var mime: String = HTTPMediaType.plainText.description
28 |
29 | // MARK: Initialization
30 |
31 | /// File data to be uploaded
32 | public init(data: Data, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess, mime: String = HTTPMediaType.plainText.description) {
33 | self.data = data
34 | self.bucket = bucket
35 | self.path = destination
36 | self.access = access
37 | self.mime = mime
38 | }
39 |
40 | /// File to be uploaded
41 | public init(file: URL, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess) throws {
42 | self.data = try Data(contentsOf: file)
43 | self.bucket = bucket
44 | self.path = destination
45 | self.access = access
46 | self.mime = S3.mimeType(forFileAtUrl: file)
47 | }
48 |
49 | /// File to be uploaded
50 | public init(file: String, bucket: String? = nil, destination: String, access: AccessControlList = .privateAccess) throws {
51 | guard let url = URL(string: file) else {
52 | throw S3.Error.invalidUrl
53 | }
54 | try self.init(file: url, bucket: bucket, destination: destination, access: access)
55 | }
56 |
57 | }
58 |
59 | /// File to be located
60 | public struct Location: LocationConvertible {
61 |
62 | /// Override target bucket
63 | public internal(set) var bucket: String?
64 |
65 | /// S3 file path
66 | public internal(set) var path: String
67 |
68 | /// Region
69 | public internal(set) var region: Region?
70 |
71 | /// Initializer
72 | public init(path: String, bucket: String? = nil, region: Region? = nil) {
73 | self.path = path
74 | self.bucket = bucket
75 | self.region = region
76 | }
77 |
78 | }
79 |
80 | /// File data response comming back from S3
81 | public struct Response: Codable {
82 |
83 | /// Data
84 | public internal(set) var data: Data
85 |
86 | /// Override target bucket
87 | public internal(set) var bucket: String
88 |
89 | /// S3 file path
90 | public internal(set) var path: String
91 |
92 | /// Access control for file
93 | public internal(set) var access: AccessControlList?
94 |
95 | /// File type (mime)
96 | public internal(set) var mime: String
97 |
98 | }
99 |
100 | /// Copy file response comming back from S3
101 | public struct CopyResponse: Codable {
102 |
103 | /// ETag
104 | public let etag: String
105 |
106 | /// Last modified
107 | public let modified: Date
108 |
109 | enum CodingKeys: String, CodingKey {
110 | case etag = "ETag"
111 | case modified = "LastModified"
112 | }
113 |
114 | }
115 |
116 | /// File info response comming back from S3
117 | public struct Info: Codable {
118 |
119 | /// Override target bucket
120 | public internal(set) var bucket: String
121 |
122 | /// Override target bucket
123 | public internal(set) var region: Region
124 |
125 | /// S3 file path
126 | public internal(set) var path: String
127 |
128 | /// Access control for file
129 | public internal(set) var access: AccessControlList
130 |
131 | /// File type (mime)
132 | public internal(set) var mime: String?
133 |
134 | /// File size
135 | public internal(set) var size: Int?
136 |
137 | /// Server
138 | public internal(set) var server: String?
139 |
140 | /// ETag
141 | public internal(set) var etag: String?
142 |
143 | /// Expiration
144 | public internal(set) var expiration: Date?
145 |
146 | /// Date created
147 | public internal(set) var created: Date?
148 |
149 | /// Last modified
150 | public internal(set) var modified: Date?
151 |
152 | /// Version ID
153 | public internal(set) var versionId: String?
154 |
155 | /// Storage class
156 | public internal(set) var storageClass: String?
157 |
158 | enum CodingKeys: String, CodingKey {
159 | case bucket = "Bucket"
160 | case region = "Region"
161 | case path = "Path"
162 | case access = "Access"
163 | case mime = "Content-Type"
164 | case size = "Content-Length"
165 | case server = "Server"
166 | case etag = "ETag"
167 | case expiration = "x-amz-expiration"
168 | case created = "Date"
169 | case modified = "Last-Modified"
170 | case versionId = "x-amz-version-id"
171 | case storageClass = "x-amz-storage-class"
172 | }
173 |
174 | }
175 |
176 | }
177 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/Object.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | /// S3 object
5 | public struct Object: Codable {
6 |
7 | /// The object's key / file name
8 | public let fileName: String
9 |
10 | /// STANDARD | STANDARD_IA | ONEZONE_IA | REDUCED_REDUNDANCY | GLACIER
11 | public let storageClass: String?
12 |
13 | /// The entity tag is an MD5 hash of the object. ETag reflects only changes to the contents of an object, not its metadata
14 | public let etag: String
15 |
16 | /// Owner
17 | public let owner: Owner?
18 |
19 | /// Size in bytes of the object
20 | public let size: Int?
21 |
22 | /// Date and time the object was last modified
23 | public let lastModified: Date
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case fileName = "Key"
27 | case storageClass = "StorageClass"
28 | case etag = "ETag"
29 | case owner = "Owner"
30 | case size = "Size"
31 | case lastModified = "LastModified"
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Models/Owner.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | /// Owner object
5 | public struct Owner: Codable {
6 |
7 | /// Owner's ID
8 | public let id: String
9 |
10 | /**
11 | Owner's name
12 | - *This value is only included in the response in the US East (N. Virginia), US West (N. California), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Asia Pacific (Tokyo), EU (Ireland), and South America (São Paulo) regions.*
13 | - *For a list of all the Amazon S3 supported regions and endpoints, see Regions and Endpoints in the AWS General Reference.*
14 | */
15 | public let name: String?
16 |
17 | enum CodingKeys: String, CodingKey {
18 | case id = "ID"
19 | case name = "DisplayName"
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Protocols/LocationConvertible.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationConvertible.swift
3 | // S3
4 | //
5 | // Created by Ondrej Rafaj on 19/04/2018.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | public protocol LocationConvertible {
12 |
13 | /// Override target bucket
14 | var bucket: String? { get }
15 |
16 | /// S3 file path
17 | var path: String { get }
18 |
19 | /// Region
20 | var region: Region? { get }
21 |
22 | }
23 |
24 |
25 | /// String should be convertible into S3 path
26 | extension String: LocationConvertible {
27 |
28 | /// Bucket name on a path, will be nil on a string
29 | public var bucket: String? {
30 | return nil
31 | }
32 |
33 | /// S3 file path
34 | public var path: String {
35 | return self
36 | }
37 |
38 | /// Region
39 | public var region: Region? {
40 | return nil
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/S3Kit/Protocols/S3Client.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIO
3 | import HTTPMediaTypes
4 |
5 |
6 | /// S3 client Protocol
7 | public protocol S3Client {
8 |
9 | /// Get list of objects
10 | func buckets(on eventLoop: EventLoop) -> EventLoopFuture
11 |
12 | /// Create a bucket
13 | func create(bucket: String, region: Region?, on eventLoop: EventLoop) -> EventLoopFuture
14 |
15 | /// Delete a bucket wherever it is
16 | // func delete(bucket: String, on container: Container) -> EventLoopFuture
17 |
18 | /// Delete a bucket
19 | func delete(bucket: String, region: Region?, on eventLoop: EventLoop) -> EventLoopFuture
20 |
21 | /// Get bucket location
22 | func location(bucket: String, on eventLoop: EventLoop) -> EventLoopFuture
23 |
24 | /// Get list of objects
25 | func list(bucket: String, region: Region?, on eventLoop: EventLoop) -> EventLoopFuture
26 |
27 | /// Get list of objects
28 | func list(bucket: String, region: Region?, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture
29 |
30 | /// Upload file to S3
31 | func put(file: File.Upload, on eventLoop: EventLoop) -> EventLoopFuture
32 |
33 | /// Upload file to S3
34 | func put(file: File.Upload, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture
35 |
36 | /// Upload file to S3
37 | func put(file url: URL, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture
38 |
39 | /// Upload file to S3
40 | func put(file url: URL, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture
41 |
42 | /// Upload file to S3
43 | func put(file path: String, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture
44 |
45 | /// Upload file to S3
46 | func put(file path: String, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture
47 |
48 | /// Upload file to S3
49 | func put(string: String, destination: String, on eventLoop: EventLoop) -> EventLoopFuture
50 |
51 | /// Upload file to S3
52 | func put(string: String, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture
53 |
54 | /// Upload file to S3
55 | func put(string: String, mime: HTTPMediaType, destination: String, on eventLoop: EventLoop) -> EventLoopFuture
56 |
57 | /// Upload file to S3
58 | func put(string: String, mime: HTTPMediaType, destination: String, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture
59 |
60 | /// Upload file to S3
61 | func put(string: String, mime: HTTPMediaType, destination: String, bucket: String?, access: AccessControlList, on eventLoop: EventLoop) -> EventLoopFuture
62 |
63 | /// File URL
64 | func url(fileInfo file: LocationConvertible) throws -> URL
65 |
66 | /// Retrieve file data from S3
67 | func get(fileInfo file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture
68 |
69 | /// Retrieve file data from S3
70 | func get(fileInfo file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture
71 |
72 | /// Retrieve file data from S3
73 | func get(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture
74 |
75 | /// Retrieve file data from S3
76 | func get(file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture
77 |
78 | /// Delete file from S3
79 | func delete(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture
80 |
81 | /// Delete file from S3
82 | func delete(file: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture
83 |
84 | /// Copy file on S3
85 | func copy(file: LocationConvertible, to: LocationConvertible, headers: [String: String], on eventLoop: EventLoop) -> EventLoopFuture
86 | }
87 |
88 | extension S3Client {
89 |
90 | /// Retrieve file data from S3
91 | func get(file: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture {
92 | return get(file: file, headers: [:], on: eventLoop)
93 | }
94 |
95 | /// Copy file on S3
96 | public func copy(file: LocationConvertible, to: LocationConvertible, on eventLoop: EventLoop) -> EventLoopFuture {
97 | return self.copy(file: file, to: to, headers: [:], on: eventLoop)
98 | }
99 |
100 | static var dateFormatter: DateFormatter {
101 | let formatter = DateFormatter()
102 | formatter.calendar = Calendar(identifier: .iso8601)
103 | formatter.locale = Locale(identifier: "en_US_POSIX")
104 | formatter.timeZone = TimeZone(secondsFromGMT: 0)
105 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
106 | return formatter
107 | }
108 |
109 | static var headerDateFormatter: DateFormatter {
110 | let formatter = DateFormatter()
111 | formatter.calendar = Calendar(identifier: .iso8601)
112 | formatter.locale = Locale(identifier: "en_US_POSIX")
113 | formatter.timeZone = TimeZone(secondsFromGMT: 0)
114 | formatter.dateFormat = "EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"
115 | return formatter
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/S3Kit/S3.swift:
--------------------------------------------------------------------------------
1 | import S3Signer
2 | import Foundation
3 | import HTTPMediaTypes
4 | import AsyncHTTPClient
5 |
6 |
7 | /// Main S3 class
8 | public class S3: S3Client {
9 |
10 | /// Error messages
11 | public enum Error: Swift.Error {
12 | case invalidUrl
13 | case errorResponse(HTTPResponseStatus, ErrorMessage)
14 | case badClientResponse(HTTPClient.Response)
15 | case badResponse(HTTPClient.Response)
16 | case badStringData
17 | case missingData
18 | case notFound
19 | case s3NotRegistered
20 | }
21 |
22 | /// If set, this bucket name value will be used globally unless overriden by a specific call
23 | public internal(set) var defaultBucket: String
24 |
25 | /// Signer instance
26 | public let signer: S3Signer
27 |
28 | let urlBuilder: URLBuilder?
29 |
30 | // MARK: Initialization
31 |
32 | /// Basic initialization method, also registers S3Signer and self with services
33 | @discardableResult public convenience init(defaultBucket: String, config: S3Signer.Config) throws {
34 | let signer = try S3Signer(config)
35 | try self.init(defaultBucket: defaultBucket, signer: signer)
36 | }
37 |
38 | /// Basic initialization method
39 | public init(defaultBucket: String, signer: S3Signer) throws {
40 | self.defaultBucket = defaultBucket
41 | self.signer = signer
42 | self.urlBuilder = nil
43 | }
44 |
45 | /// Basic initialization method
46 | public init(urlBuilder: URLBuilder, defaultBucket: String, signer: S3Signer) throws {
47 | self.defaultBucket = defaultBucket
48 | self.signer = signer
49 | self.urlBuilder = nil
50 | }
51 |
52 | }
53 |
54 | // MARK: - Helper methods
55 |
56 | extension S3 {
57 |
58 | /// Check response for error
59 | @discardableResult func check(_ response: HTTPClient.Response) throws -> HTTPClient.Response {
60 | guard response.status == .ok || response.status == .noContent else {
61 | if let error = try? response.decode(to: ErrorMessage.self) {
62 | if var body = response.body, let content = body.readString(length: body.readableBytes) {
63 | print(content)
64 | }
65 | throw Error.errorResponse(response.status, error)
66 | } else {
67 | if var body = response.body, let content = body.readString(length: body.readableBytes) {
68 | print(content)
69 | }
70 | throw Error.badResponse(response)
71 | }
72 | }
73 | return response
74 | }
75 |
76 | /// Get mime type for file
77 | static func mimeType(forFileAtUrl url: URL) -> String {
78 | guard let mediaType = HTTPMediaType.fileExtension(url.pathExtension) else {
79 | return HTTPMediaType(type: "application", subType: "octet-stream").description
80 | }
81 | return mediaType.description
82 | }
83 |
84 | /// Get mime type for file
85 | func mimeType(forFileAtUrl url: URL) -> String {
86 | return S3.mimeType(forFileAtUrl: url)
87 | }
88 |
89 | /// Create URL builder
90 | func makeURLBuilder() -> URLBuilder {
91 | return urlBuilder ?? S3URLBuilder(defaultBucket: defaultBucket, config: signer.config)
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/S3Kit/URLBuilder/S3URLBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import S3Signer
3 |
4 |
5 | /// URL builder
6 | public final class S3URLBuilder: URLBuilder {
7 |
8 | /// Default bucket
9 | let defaultBucket: String
10 |
11 | /// S3 Configuration
12 | let config: S3Signer.Config
13 |
14 | /// Initializer
15 | public init(defaultBucket: String, config: S3Signer.Config) {
16 | self.defaultBucket = defaultBucket
17 | self.config = config
18 | }
19 |
20 | /// Plain Base URL with no bucket specified
21 | /// *Format: https://s3.eu-west-2.amazonaws.com/
22 | public func plain(region: Region? = nil) throws -> URL {
23 | let urlString = (region ?? config.region).hostUrlString()
24 | guard let url = URL(string: urlString) else {
25 | throw S3.Error.invalidUrl
26 | }
27 | return url
28 | }
29 |
30 | /// Base URL for S3 region
31 | /// *Format: https://bucket.s3.eu-west-2.amazonaws.com/path_or_parameter*
32 | public func url(region: Region? = nil, bucket: String? = nil, path: String? = nil) throws -> URL {
33 | let urlString = (region ?? config.region).hostUrlString(bucket: (bucket ?? defaultBucket))
34 | guard let url = URL(string: urlString) else {
35 | throw S3.Error.invalidUrl
36 | }
37 | return url
38 | }
39 |
40 | /// Base URL for a file in a bucket
41 | /// * Format: https://s3.eu-west-2.amazonaws.com/bucket/file.txt
42 | /// * We can't have a bucket in the host or DELETE will attempt to delete the bucket, not file!
43 | public func url(file: LocationConvertible) throws -> URL {
44 | let urlString = (file.region ?? config.region).hostUrlString()
45 | guard let url = URL(string: urlString)?.appendingPathComponent(file.bucket ?? defaultBucket).appendingPathComponent(file.path) else {
46 | throw S3.Error.invalidUrl
47 | }
48 | return url
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/S3Kit/URLBuilder/URLBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import S3Signer
3 |
4 |
5 | extension Region {
6 |
7 | /// Host URL including scheme
8 | public func hostUrlString(bucket: String? = nil) -> String {
9 | // return "http://localhost:4444/"
10 | if let bucket = bucket {
11 | return urlProtocol + host.finished(with: "/") + bucket
12 | }
13 | return urlProtocol + host.finished(with: "/")
14 | }
15 |
16 | private var urlProtocol: String {
17 | return useTLS ? "https://" : "http://"
18 | }
19 |
20 | }
21 |
22 |
23 | /// URL builder
24 | public protocol URLBuilder {
25 |
26 | /// Initializer
27 | init(defaultBucket: String, config: S3Signer.Config)
28 |
29 | /// Plain Base URL with no bucket specified
30 | /// *Format: https://s3.eu-west-2.amazonaws.com/
31 | func plain(region: Region?) throws -> URL
32 |
33 | /// Base URL for S3 region
34 | /// *Format: https://bucket.s3.eu-west-2.amazonaws.com/path_or_parameter*
35 | func url(region: Region?, bucket: String?, path: String?) throws -> URL
36 |
37 | /// Base URL for a file in a bucket
38 | /// * Format: https://s3.eu-west-2.amazonaws.com/bucket/file.txt
39 | /// * We can't have a bucket in the host or DELETE will attempt to delete the bucket, not file!
40 | func url(file: LocationConvertible) throws -> URL
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/S3Provider/Exports.swift:
--------------------------------------------------------------------------------
1 | @_exported import Vapor
2 | @_exported import S3Kit
3 |
--------------------------------------------------------------------------------
/Sources/S3Provider/Model/Models+Content.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 |
4 | extension BucketsInfo: Content { }
5 | extension BucketResults: Content { }
6 |
7 | extension AccessControlList: Content { }
8 | extension Bucket: Content { }
9 | extension ErrorMessage: Content { }
10 | extension S3Kit.File.Info: Content { }
11 | extension S3Kit.File.Response: Content { }
12 | extension S3Kit.File.CopyResponse: Content { }
13 | extension Object: Content { }
14 | extension Owner: Content { }
15 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Dates.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | struct Dates {
5 |
6 | /// The ISO8601 basic format timestamp of signature creation. YYYYMMDD'T'HHMMSS'Z'.
7 | let long: String
8 |
9 | /// The short timestamp of signature creation. YYYYMMDD.
10 | let short: String
11 |
12 | init(_ date: Date) {
13 | self.short = date.timestampShort
14 | self.long = date.timestampLong
15 | }
16 |
17 | }
18 |
19 |
20 | extension Date {
21 |
22 | private static let shortDateFormatter: DateFormatter = {
23 | let formatter = DateFormatter()
24 | formatter.dateFormat = "yyyyMMdd"
25 | formatter.timeZone = TimeZone(abbreviation: "UTC")
26 | formatter.locale = Locale(identifier: "en_US_POSIX")
27 | return formatter
28 | }()
29 |
30 | private static let longDateFormatter: DateFormatter = {
31 | let formatter = DateFormatter()
32 | formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
33 | formatter.timeZone = TimeZone(abbreviation: "UTC")
34 | formatter.locale = Locale(identifier: "en_US_POSIX")
35 | return formatter
36 | }()
37 |
38 | var timestampShort: String {
39 | return Date.shortDateFormatter.string(from: self)
40 | }
41 |
42 | var timestampLong: String {
43 | return Date.longDateFormatter.string(from: self)
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Derived_from_LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 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 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Expiration.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | /// Pre-sign URL expiration time
5 | public enum Expiration {
6 |
7 | public typealias Seconds = Int
8 |
9 | /// 15 minutes
10 | case fifteenMinutes
11 |
12 | /// 30 minutes
13 | case thirtyMinutes
14 |
15 | /// 60 minutes
16 | case hour
17 |
18 | /// 180 minutes
19 | case threeHours
20 |
21 | /// Custom expiration time, in seconds.
22 | case custom(Seconds)
23 | }
24 |
25 |
26 | extension Expiration {
27 |
28 | /// Expiration Value
29 | var value: Seconds {
30 | switch self {
31 | case .fifteenMinutes:
32 | return 60 * 15
33 | case .thirtyMinutes:
34 | return 60 * 30
35 | case .hour:
36 | return 60 * 60
37 | case .threeHours:
38 | return 60 * 60 * 3
39 | case .custom(let exp):
40 | return exp
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Exports.swift:
--------------------------------------------------------------------------------
1 | @_exported import Foundation
2 | @_exported import enum NIOHTTP1.HTTPMethod
3 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Extensions/HMAC+Tools.swift:
--------------------------------------------------------------------------------
1 | import OpenCrypto
2 |
3 |
4 | extension HMAC {
5 |
6 | static func signature(_ stringToSign: String, key: [UInt8]) -> HashedAuthenticationCode {
7 | let signature = HMAC.authenticationCode(
8 | for: stringToSign.bytes,
9 | using: .init(data: key)
10 | )
11 | return signature
12 | }
13 |
14 | static func signature(_ stringToSign: String, key: HashedAuthenticationCode) -> HashedAuthenticationCode {
15 | let signature = HMAC.authenticationCode(
16 | for: stringToSign.bytes,
17 | using: .init(data: key)
18 | )
19 | return signature
20 | }
21 |
22 | }
23 |
24 | extension HashedAuthenticationCode {
25 |
26 | var data: Data {
27 | return Data(self)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Extensions/HTTPMethod+Description.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIOHTTP1
3 |
4 |
5 | extension HTTPMethod {
6 |
7 | var description: String {
8 | switch self {
9 | case .GET:
10 | return "GET"
11 | case .PUT:
12 | return "PUT"
13 | case .ACL:
14 | return "ACL"
15 | case .HEAD:
16 | return "HEAD"
17 | case .POST:
18 | return "POST"
19 | case .COPY:
20 | return "COPY"
21 | case .LOCK:
22 | return "LOCK"
23 | case .MOVE:
24 | return "MOVE"
25 | case .BIND:
26 | return "BIND"
27 | case .LINK:
28 | return "LINK"
29 | case .PATCH:
30 | return "PATCH"
31 | case .TRACE:
32 | return "TRACE"
33 | case .MKCOL:
34 | return "MKCOL"
35 | case .MERGE:
36 | return "MERGE"
37 | case .PURGE:
38 | return "PURGE"
39 | case .NOTIFY:
40 | return "NOTIFY"
41 | case .SOURCE:
42 | return "SOURCE"
43 | case .SEARCH:
44 | return "SEARCH"
45 | case .UNLOCK:
46 | return "UNLOCK"
47 | case .REBIND:
48 | return "REBIND"
49 | case .UNBIND:
50 | return "UNBIND"
51 | case .REPORT:
52 | return "REPORT"
53 | case .DELETE:
54 | return "DELETE"
55 | case .UNLINK:
56 | return "UNLINK"
57 | case .CONNECT:
58 | return "CONNECT"
59 | case .MSEARCH:
60 | return "MSEARCH"
61 | case .OPTIONS:
62 | return "OPTIONS"
63 | case .PROPFIND:
64 | return "PROPFIND"
65 | case .CHECKOUT:
66 | return "CHECKOUT"
67 | case .PROPPATCH:
68 | return "PROPPATCH"
69 | case .SUBSCRIBE:
70 | return "SUBSCRIBE"
71 | case .MKCALENDAR:
72 | return "MKCALENDAR"
73 | case .MKACTIVITY:
74 | return "MKACTIVITY"
75 | case .UNSUBSCRIBE:
76 | return "UNSUBSCRIBE"
77 | case .RAW(let value):
78 | return value
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Extensions/S3Signer+Private.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import OpenCrypto
3 | import NIOHTTP1
4 | import HTTPMediaTypes
5 |
6 |
7 | /// Private interface
8 | extension S3Signer {
9 |
10 | func canonicalHeadersV2(_ headers: [String: String]) -> String {
11 | let unfoldedHeaders = headers
12 | .filter { $0.key.lowercased().hasPrefix("x-amz") }
13 | .mapValues {
14 | // unfold values as per RFC 2616 section 4.2
15 | $0.split(separator: "\n")
16 | .map { $0.trimmingCharacters(in: .whitespaces) }
17 | .joined(separator: " ")
18 | }
19 | let groupedHeaders = Dictionary(unfoldedHeaders.map { ($0.key.lowercased(), $0.value) },
20 | uniquingKeysWith: { "\($0),\($1)" })
21 | return Array(groupedHeaders.keys)
22 | .sorted(by: { $0.localizedCompare($1) == ComparisonResult.orderedAscending })
23 | .map {
24 | let trimmedHeader = $0.trimmingCharacters(in: .whitespaces)
25 | return "\(trimmedHeader):\(groupedHeaders[$0]!)"
26 | }
27 | .joined(separator: "\n")
28 | }
29 |
30 | func canonicalHeaders(_ headers: [String: String]) -> String {
31 | let headerList = Array(headers.keys)
32 | .map { "\($0.lowercased()):\(headers[$0]!)" }
33 | .filter { $0 != "authorization" }
34 | .sorted(by: { $0.localizedCompare($1) == ComparisonResult.orderedAscending })
35 | .joined(separator: "\n")
36 | .appending("\n")
37 | return headerList
38 | }
39 |
40 | func createCanonicalRequest(_ httpMethod: HTTPMethod, url: URL, headers: [String: String], bodyDigest: String) throws -> String {
41 | let query = try self.query(url) ?? ""
42 | return [
43 | httpMethod.description,
44 | path(url),
45 | query,
46 | canonicalHeaders(headers),
47 | signed(headers: headers),
48 | bodyDigest
49 | ].joined(separator: "\n")
50 | }
51 |
52 | func createSignature(_ stringToSign: String, timeStampShort: String, region: Region) throws -> String {
53 | let dateKey = HMAC.signature(timeStampShort, key: "AWS4\(config.secretKey)".bytes)
54 | let dateRegionKey = HMAC.signature(region.name.description, key: dateKey)
55 | let dateRegionServiceKey = HMAC.signature(config.service, key: dateRegionKey)
56 | let signingKey = HMAC.signature("aws4_request", key: dateRegionServiceKey)
57 | let signature = HMAC.signature(stringToSign, key: signingKey)
58 | return signature.description
59 | }
60 |
61 | func createStringToSign(_ canonicalRequest: String, dates: Dates, region: Region) throws -> String {
62 | let canonRequestHash = SHA256.hash(data: canonicalRequest.bytes).description
63 | let components = [
64 | "AWS4-HMAC-SHA256",
65 | dates.long,
66 | credentialScope(dates.short, region: region),
67 | canonRequestHash
68 | ]
69 | return components.joined(separator: "\n")
70 | }
71 |
72 | func credentialScope(_ timeStampShort: String, region: Region) -> String {
73 | let arr = [timeStampShort, region.name.description, config.service, "aws4_request"]
74 | return arr.joined(separator: "/")
75 | }
76 |
77 | static fileprivate let canonicalSubresources = [
78 | "acl",
79 | "lifecycle",
80 | "location",
81 | "logging",
82 | "notification",
83 | "partNumber",
84 | "policy",
85 | "requestPayment",
86 | "torrent",
87 | "uploadId",
88 | "uploads",
89 | "versionId",
90 | "versioning",
91 | "versions",
92 | "website"
93 | ]
94 | static fileprivate let canonicalOverridingQueryItems = [
95 | "response-content-type",
96 | "response-content-language",
97 | "response-expires",
98 | "response-cache-control",
99 | "response-content-disposition",
100 | "response-content-encoding"
101 | ]
102 |
103 | fileprivate func canonicalResourceV2(url: URL, region: Region, bucket: String?) -> String {
104 | // unless there is a custom hostname, S3URLBuilder uses virtual hosting (bucket name is in host name part)
105 | var canonical = ""
106 | let bucketString = bucket ?? ""
107 | if region.hostName == nil, !bucketString.isEmpty {
108 | canonical = "/\(bucketString)"
109 | }
110 | let path = url.path
111 | canonical += path.isEmpty ? "/" : path
112 |
113 | if let bucket = bucket, !bucket.isEmpty, url.path.isEmpty || url.path == "/" {
114 | return "/\(bucket.trimmingCharacters(in: CharacterSet(charactersIn: "/")))/"
115 | }
116 | if url.path.isEmpty {
117 | return "/"
118 | }
119 | if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
120 | let queryItems = components.queryItems {
121 | let relevantItems: [String] = queryItems
122 | .filter {
123 | let name = $0.name.lowercased()
124 | return S3Signer.canonicalSubresources.contains(name) || S3Signer.canonicalOverridingQueryItems.contains(name)
125 | }
126 | .sorted {
127 | let result = $0.name.caseInsensitiveCompare($1.name)
128 | return result == .orderedAscending
129 | }
130 | .map {
131 | if let value = $0.value {
132 | return "\($0.name)=\(value)"
133 | }
134 | return $0.name
135 | }
136 | if !relevantItems.isEmpty {
137 | canonical += relevantItems.joined(separator: "&")
138 | }
139 | }
140 | return url.path.encode(type: .pathAllowed) ?? "/"
141 | }
142 |
143 | func generateAuthHeaderV2(_ httpMethod: HTTPMethod, url: URL, headers: [String: String], dates: Dates, region: Region, bucket: String?) throws -> String {
144 | let method = httpMethod.description
145 | let contentMD5 = headers["content-MD5"] ?? ""
146 | let contentType = headers["content-type"] ?? ""
147 | let date = headers["Date"] ?? headers["Date"] ?? ""
148 | let canonicalizedAmzHeaders = canonicalHeadersV2(headers)
149 | let canonicalizedResource = canonicalResourceV2(url: url, region: region, bucket: bucket)
150 | let stringToSign = "\(method)\n\(contentMD5)\n\(contentType)\n\(date)\n\(canonicalizedAmzHeaders)\n\(canonicalizedResource)"
151 | let signature = HMAC.signature(stringToSign, key: config.secretKey.bytes)
152 | let authHeader = "AWS \(config.accessKey):\(signature.data.base64EncodedString())"
153 | return authHeader
154 | }
155 |
156 | func generateAuthHeader(_ httpMethod: HTTPMethod, url: URL, headers: [String: String], bodyDigest: String, dates: Dates, region: Region) throws -> String {
157 | // print("\n\n\n------------------- CRH:\n")
158 | let canonicalRequestHex = try createCanonicalRequest(httpMethod, url: url, headers: headers, bodyDigest: bodyDigest)
159 | // print(canonicalRequestHex)
160 | let stringToSign = try createStringToSign(canonicalRequestHex, dates: dates, region: region)
161 | // print("\n\n\n------------------- STS:\n")
162 | // print(stringToSign)
163 | let signature = try createSignature(stringToSign, timeStampShort: dates.short, region: region)
164 | let authHeader = "AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(credentialScope(dates.short, region: region)), SignedHeaders=\(signed(headers: headers)), Signature=\(signature)"
165 | return authHeader
166 | }
167 |
168 | func getDates(_ date: Date) -> Dates {
169 | return Dates(date)
170 | }
171 |
172 | func path(_ url: URL) -> String {
173 | return !url.path.isEmpty ? url.path.encode(type: .pathAllowed) ?? "/" : "/"
174 | }
175 |
176 | func presignedURLCanonRequest(_ httpMethod: HTTPMethod, dates: Dates, expiration: Expiration, url: URL, region: Region, headers: [String: String]) throws -> (String, URL) {
177 | guard let credScope = credentialScope(dates.short, region: region).encode(type: .queryAllowed),
178 | let signHeaders = signed(headers: headers).encode(type: .queryAllowed) else {
179 | throw Error.invalidEncoding
180 | }
181 | let fullURL = "\(url.absoluteString)?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=\(config.accessKey)%2F\(credScope)&X-Amz-Date=\(dates.long)&X-Amz-Expires=\(expiration.value)&X-Amz-SignedHeaders=\(signHeaders)"
182 |
183 | // This should never throw.
184 | guard let url = URL(string: fullURL) else {
185 | throw Error.badURL(fullURL)
186 | }
187 |
188 | let query = try self.query(url) ?? ""
189 | return (
190 | [
191 | httpMethod.description,
192 | path(url),
193 | query,
194 | canonicalHeaders(headers),
195 | signed(headers: headers),
196 | "UNSIGNED-PAYLOAD"
197 | ].joined(separator: "\n"),
198 | url
199 | )
200 | }
201 |
202 | func query(_ url: URL) throws -> String? {
203 | if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems {
204 | let items = queryItems.map({ ($0.name.encode(type: .queryAllowed) ?? "", $0.value?.encode(type: .queryAllowed) ?? "") })
205 | let encodedItems = items.map({ "\($0.0)=\($0.1)" })
206 | return encodedItems.sorted().joined(separator: "&")
207 | }
208 | return nil
209 | }
210 |
211 | func signed(headers: [String: String]) -> String {
212 | return Array(headers.keys).map { $0.lowercased() }.filter { $0 != "authorization" }.sorted().joined(separator: ";")
213 | }
214 |
215 | func update(headers: [String: String], url: URL, longDate: String, bodyDigest: String, region: Region?) -> [String: String] {
216 | var updatedHeaders = headers
217 | updatedHeaders["x-amz-date"] = longDate
218 | if (updatedHeaders["Host"] ?? updatedHeaders["Host"]) == nil {
219 | updatedHeaders["Host"] = (url.host ?? (region ?? config.region).host)
220 | }
221 | if config.authVersion == .v4 && bodyDigest != "UNSIGNED-PAYLOAD" && config.service == "s3" {
222 | updatedHeaders["x-amz-content-sha256"] = bodyDigest
223 | }
224 | // According to http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#RequestWithSTS
225 | if let token = config.securityToken {
226 | updatedHeaders["x-amz-security-token"] = token
227 | }
228 | return updatedHeaders
229 | }
230 |
231 | func presignedURL(for httpMethod: HTTPMethod, url: URL, expiration: Expiration, region: Region? = nil, headers: [String: String] = [:], dates: Dates) throws -> URL? {
232 | guard config.authVersion == .v4 else {
233 | throw Error.featureNotAvailableWithV2Signing
234 | }
235 |
236 | var updatedHeaders = headers
237 |
238 | let region = region ?? config.region
239 |
240 | updatedHeaders["Host"] = url.host ?? region.host
241 |
242 | let (canonRequest, fullURL) = try presignedURLCanonRequest(httpMethod, dates: dates, expiration: expiration, url: url, region: region, headers: updatedHeaders)
243 |
244 | let stringToSign = try createStringToSign(canonRequest, dates: dates, region: region)
245 | let signature = try createSignature(stringToSign, timeStampShort: dates.short, region: region)
246 | let presignedURL = URL(string: fullURL.absoluteString.appending("&X-Amz-Signature=\(signature)"))
247 | return presignedURL
248 | }
249 |
250 | func headers(
251 | for httpMethod: HTTPMethod,
252 | urlString: String,
253 | region: Region? = nil,
254 | bucket: String? = nil,
255 | headers: [String: String] = [:],
256 | payload: Payload,
257 | dates: Dates
258 | ) throws -> HTTPHeaders {
259 | guard let url = URL(string: urlString) else {
260 | throw Error.badURL("\(urlString)")
261 | }
262 |
263 | let bodyDigest = (config.authVersion == .v4) ? payload.hashed() : ""
264 | let region = region ?? config.region
265 | var updatedHeaders = update(headers: headers, url: url, longDate: dates.long, bodyDigest: bodyDigest, region: region)
266 |
267 | if httpMethod == .PUT && payload.isBytes {
268 | let s = Data(Insecure.MD5.hash(data: payload.bytes)).base64EncodedString()
269 | updatedHeaders["content-md5"] = s
270 | }
271 |
272 | if httpMethod == .PUT || httpMethod == .DELETE {
273 | updatedHeaders["content-length"] = payload.size()
274 | if httpMethod == .PUT && url.pathExtension != "" {
275 | updatedHeaders["Content-Type"] = (HTTPMediaType.fileExtension(url.pathExtension) ?? .plainText).description
276 | }
277 | }
278 |
279 | switch config.authVersion {
280 | case .v2:
281 | updatedHeaders["Authorization"] = try generateAuthHeaderV2(httpMethod, url: url, headers: updatedHeaders, dates: dates, region: region, bucket: bucket)
282 | case .v4:
283 | updatedHeaders["Authorization"] = try generateAuthHeader(httpMethod, url: url, headers: updatedHeaders, bodyDigest: bodyDigest, dates: dates, region: region)
284 | }
285 |
286 | var headers = HTTPHeaders()
287 | for (key, value) in updatedHeaders {
288 | headers.add(name: key, value: value)
289 | }
290 |
291 | return headers
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Extensions/String+Encoding.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 |
4 | enum AWSEncoding: String {
5 | case queryAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~=&"
6 | case pathAllowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~/"
7 | }
8 |
9 |
10 | extension String {
11 |
12 | func encode(type: AWSEncoding) -> String? {
13 | var allowed = CharacterSet.alphanumerics
14 | allowed.insert(charactersIn: type.rawValue)
15 | return addingPercentEncoding(withAllowedCharacters: allowed)
16 | }
17 |
18 | var bytes: [UInt8] { .init(utf8) }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Payload.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import OpenCrypto
3 |
4 |
5 | /// Payload object
6 | public enum Payload {
7 |
8 | /// Data payload
9 | case bytes(Data)
10 |
11 | /// No payload
12 | case none
13 |
14 | /// Unsigned payload
15 | case unsigned
16 |
17 | }
18 |
19 | extension Payload {
20 |
21 | var bytes: Data {
22 | switch self {
23 | case .bytes(let bytes):
24 | return bytes
25 | default:
26 | return Data()
27 | }
28 | }
29 |
30 | func hashed() -> String {
31 | switch self {
32 | case .bytes(let bytes):
33 | return SHA256.hash(data: [UInt8](bytes)).description
34 | case .none:
35 | return SHA256.hash(data: []).description
36 | case .unsigned:
37 | return "UNSIGNED-PAYLOAD"
38 | }
39 | }
40 |
41 | var isBytes: Bool {
42 | switch self {
43 | case .bytes(_), .none:
44 | return true
45 | default:
46 | return false
47 | }
48 | }
49 |
50 | func size() -> String {
51 | switch self {
52 | case .unsigned:
53 | return "UNSIGNED-PAYLOAD"
54 | default:
55 | return bytes.count.description
56 | }
57 | }
58 |
59 | var isUnsigned: Bool {
60 | switch self {
61 | case .unsigned:
62 | return true
63 | default:
64 | return false
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/S3Signer/Region.swift:
--------------------------------------------------------------------------------
1 | /// AWS Region
2 | public struct Region {
3 |
4 | /// Name of the region, see Name
5 | public let name: Name
6 |
7 | /// Name of the custom host, can contain IP and/or port (e.g. 127.0.0.1:9000)
8 | public let hostName: String?
9 |
10 | /// Use TLS/https (defaults to true)
11 | public let useTLS: Bool
12 |
13 | public struct Name: ExpressibleByStringLiteral, LosslessStringConvertible {
14 |
15 | public typealias StringLiteralType = String
16 |
17 | /// US East (N. Virginia)
18 | public static let usEast1: Name = "us-east-1"
19 |
20 | /// US East (Ohio)
21 | public static let usEast2: Name = "us-east-2"
22 |
23 | /// US West (N. California)
24 | public static let usWest1: Name = "us-west-1"
25 |
26 | /// US West (Oregon)
27 | public static let usWest2: Name = "us-west-2"
28 |
29 | /// Canada (Central)
30 | public static let caCentral1: Name = "ca-central-1"
31 |
32 | /// EU (Frankfurt)
33 | public static let euCentral1: Name = "eu-central-1"
34 |
35 | /// EU (Ireland)
36 | public static let euWest1: Name = "eu-west-1"
37 |
38 | /// EU (London)
39 | public static let euWest2: Name = "eu-west-2"
40 |
41 | /// EU (Paris)
42 | public static let euWest3: Name = "eu-west-3"
43 |
44 | /// Asia Pacific (Tokyo)
45 | public static let apNortheast1: Name = "ap-northeast-1"
46 |
47 | /// Asia Pacific (Seoul)
48 | public static let apNortheast2: Name = "ap-northeast-2"
49 |
50 | /// Asia Pacific (Osaka-Local)
51 | public static let apNortheast3: Name = "ap-northeast-3"
52 |
53 | /// Asia Pacific (Singapore)
54 | public static let apSoutheast1: Name = "ap-southeast-1"
55 |
56 | /// Asia Pacific (Sydney)
57 | public static let apSoutheast2: Name = "ap-southeast-2"
58 |
59 | /// Asia Pacific (Mumbai)
60 | public static let apSouth1: Name = "ap-south-1"
61 |
62 | /// South America (São Paulo)
63 | public static let saEast1: Name = "sa-east-1"
64 |
65 |
66 | /// Custom region
67 | /// - Parameter name: region identifier
68 | public static func custom(_ identifier: String) -> Name {
69 | return .init(stringLiteral: identifier)
70 | }
71 |
72 | /// Region as string
73 | public let description: String
74 |
75 | /// Initializer
76 | public init(stringLiteral value: String) {
77 | self.description = value
78 | }
79 |
80 | /// Initializer
81 | public init(_ value: String) {
82 | self.init(stringLiteral: value)
83 | }
84 |
85 | }
86 |
87 | /// Initializer for a (custom) region. If you use a custom hostName, you
88 | /// - Note: still need a region (e.g. use usEast1 for Minio)
89 | public init(name: Name, hostName: String? = nil, useTLS: Bool = true) {
90 | self.name = name
91 | self.hostName = hostName
92 | self.useTLS = useTLS
93 | }
94 |
95 | }
96 |
97 |
98 | extension Region {
99 |
100 | /// Base URL / Host
101 | public var host: String {
102 | return hostName ?? "s3.\(name).amazonaws.com"
103 | }
104 |
105 | }
106 |
107 | extension Region {
108 |
109 | /// convenience var for US East (N. Virginia)
110 | public static let usEast1 = Region(name: .usEast1)
111 |
112 | /// convenience var for US East (Ohio)
113 | public static let usEast2 = Region(name: .usEast2)
114 |
115 | /// convenience var for US West (N. California)
116 | public static let usWest1 = Region(name: .usWest1)
117 |
118 | /// convenience var for US West (Oregon)
119 | public static let usWest2 = Region(name: .usWest2)
120 |
121 | /// convenience var for Canada (Central)
122 | public static let caCentral1 = Region(name: .caCentral1)
123 |
124 | /// convenience var for EU (Frankfurt)
125 | public static let euCentral1 = Region(name: .euCentral1)
126 |
127 | /// convenience var for EU (Ireland)
128 | public static let euWest1 = Region(name: .euWest1)
129 |
130 | /// convenience var for EU (London)
131 | public static let euWest2 = Region(name: .euWest2)
132 |
133 | /// convenience var for EU (Paris)
134 | public static let euWest3 = Region(name: .euWest3)
135 |
136 | /// convenience var for Asia Pacific (Tokyo)
137 | public static let apNortheast1 = Region(name: .apNortheast1)
138 |
139 | /// convenience var for Asia Pacific (Seoul)
140 | public static let apNortheast2 = Region(name: .apNortheast2)
141 |
142 | /// convenience var for Asia Pacific (Osaka-Local)
143 | public static let apNortheast3 = Region(name: .apNortheast3)
144 |
145 | /// convenience var for Asia Pacific (Singapore)
146 | public static let apSoutheast1 = Region(name: .apSoutheast1)
147 |
148 | /// convenience var for Asia Pacific (Sydney)
149 | public static let apSoutheast2 = Region(name: .apSoutheast2)
150 |
151 | /// convenience var for Asia Pacific (Mumbai)
152 | public static let apSouth1 = Region(name: .apSouth1)
153 |
154 | /// convenience var for South America (São Paulo)
155 | public static let saEast1 = Region(name: .saEast1)
156 |
157 | }
158 |
159 | /// Codable support for Region
160 | extension Region: Codable {
161 |
162 | /// Decodes a string (see Name) to a Region (does not support custom hosts)
163 | public init(from decoder: Decoder) throws {
164 | try self.init(name: .init(.init(from: decoder)))
165 | }
166 |
167 | /// Encodes the name (see Name, does not support custom hosts)
168 | public func encode(to encoder: Encoder) throws {
169 | try name.description.encode(to: encoder)
170 | }
171 |
172 | }
173 |
--------------------------------------------------------------------------------
/Sources/S3Signer/S3Signer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import OpenCrypto
3 | import NIOHTTP1
4 | import WebErrorKit
5 |
6 |
7 | /// S3 Client: All network calls to and from AWS' S3 servers
8 | public final class S3Signer {
9 |
10 | /// Errors
11 | public enum Error: SerializableWebError {
12 |
13 | case badURL(String)
14 | case invalidEncoding
15 | case featureNotAvailableWithV2Signing
16 |
17 | public var serializedCode: String {
18 | switch self {
19 | case .badURL:
20 | return "s3.bad_url"
21 | case .invalidEncoding:
22 | return "s3.invalid_encoding"
23 | case .featureNotAvailableWithV2Signing:
24 | return "s3.not_available_on_v2_signing"
25 | }
26 | }
27 |
28 | public var reason: String {
29 | switch self {
30 | case .badURL(let url):
31 | return "Invalid URL: \(url)"
32 | case .invalidEncoding:
33 | return "Invalid encoding"
34 | case .featureNotAvailableWithV2Signing:
35 | return "Feature is not available on V2 signing"
36 | }
37 | }
38 |
39 | }
40 |
41 | /// S3 authentication support version
42 | public enum Version {
43 | case v2
44 | case v4
45 | }
46 |
47 | /// S3 Configuration
48 | public struct Config {
49 |
50 | /// AWS authentication version
51 | let authVersion: Version
52 |
53 | /// AWS Access Key
54 | let accessKey: String
55 |
56 | /// AWS Secret Key
57 | let secretKey: String
58 |
59 | /// The region where S3 bucket is located.
60 | public let region: Region
61 |
62 | /// AWS Security Token. Used to validate temporary credentials, such as those from an EC2 Instance's IAM role
63 | let securityToken : String?
64 |
65 | /// AWS Service type
66 | let service: String = "s3"
67 |
68 |
69 | /// Initalizer
70 | /// - Parameter accessKey: S3 access token
71 | /// - Parameter secretKey: S3 secret
72 | /// - Parameter region: Region
73 | /// - Parameter version: Signing version
74 | /// - Parameter securityToken: Temporary security token
75 | public init(accessKey: String, secretKey: String, region: Region, version: Version = .v4, securityToken: String? = nil) {
76 | self.accessKey = accessKey
77 | self.secretKey = secretKey
78 | self.region = region
79 | self.securityToken = securityToken
80 | self.authVersion = version
81 | }
82 |
83 | }
84 |
85 | /// Configuration
86 | public private(set) var config: Config
87 |
88 | /// Initializer
89 | public init(_ config: Config) throws {
90 | self.config = config
91 | }
92 |
93 | }
94 |
95 |
96 | extension S3Signer {
97 |
98 |
99 | /// Generates auth headers for Simple Storage Services
100 | /// - Parameter httpMethod: HTTP method
101 | /// - Parameter urlString: URL
102 | /// - Parameter region: Region
103 | /// - Parameter bucket: Bucket (default will be used if nil)
104 | /// - Parameter headers: Headers to sign
105 | /// - Parameter payload: Payload
106 | public func headers(for httpMethod: HTTPMethod, urlString: String, region: Region? = nil, bucket: String? = nil, headers: [String: String] = [:], payload: Payload) throws -> HTTPHeaders {
107 | return try self.headers(for: httpMethod, urlString: urlString, region: region, bucket: bucket, headers: headers, payload: payload, dates: Dates(Date()))
108 | }
109 |
110 |
111 | /// Create a pre-signed URL for later use
112 | /// - Parameter httpMethod: HTTP method
113 | /// - Parameter url: URL
114 | /// - Parameter expiration: Expiration time
115 | /// - Parameter region: AWS Region
116 | /// - Parameter headers: Headers to sign
117 | public func presignedURL(for httpMethod: HTTPMethod, url: URL, expiration: Expiration, region: Region? = nil, headers: [String: String] = [:]) throws -> URL? {
118 | return try presignedURL(for: httpMethod, url: url, expiration: expiration, region: region, headers: headers, dates: Dates(Date()))
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import S3Tests
3 |
4 | XCTMain([
5 | testCase(AWSTestSuite.allTests),
6 | testCase(S3Tests.allTests),
7 | testCase(S3SignerAWSTests.allTests)
8 | ])
9 |
--------------------------------------------------------------------------------
/Tests/S3Tests/AWSTestSuite.swift:
--------------------------------------------------------------------------------
1 | @testable import S3Signer
2 | @testable import S3Kit
3 | import XCTest
4 |
5 |
6 | class AWSTestSuite: BaseTestCase {
7 |
8 | static var allTests = [
9 | ("test_Get_Vanilla", test_Get_Vanilla),
10 | ("test_Get_Vanilla_with_added_headers", test_Get_Vanilla_with_added_headers),
11 | ("test_Post_With_Param_Vanilla", test_Post_With_Param_Vanilla)
12 | ]
13 |
14 | func test_Get_Vanilla() {
15 | let requestURLString = region.hostUrlString()
16 | let requestURL = URL(string: requestURLString)!
17 |
18 | let updatedHeaders = signer.update(headers: [:], url: requestURL, longDate: overridenDate.long, bodyDigest: Payload.none.hashed(), region: region)
19 |
20 | let expectedCanonRequest = [
21 | "GET",
22 | "/",
23 | "",
24 | "host:\(region.host)",
25 | "x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
26 | "x-amz-date:20130524T000000Z",
27 | "",
28 | "host;x-amz-content-sha256;x-amz-date",
29 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]
30 | .joined(separator: "\n")
31 |
32 | let canonRequest = try! signer.createCanonicalRequest(.GET,
33 | url: requestURL,
34 | headers: updatedHeaders,
35 | bodyDigest: Payload.none.hashed())
36 |
37 | XCTAssertEqual(expectedCanonRequest, canonRequest)
38 |
39 | let expectedStringToSign = [
40 | "AWS4-HMAC-SHA256",
41 | "20130524T000000Z",
42 | "20130524/us-east-1/s3/aws4_request",
43 | "64669d70b364645a9118ecbd15e6f62aee6db08e63d2f74a7f183eb685d871cd"
44 | ].joined(separator: "\n")
45 |
46 | let stringToSign = try! signer.createStringToSign(
47 | canonRequest,
48 | dates: overridenDate,
49 | region: region
50 | )
51 |
52 | XCTAssertEqual(expectedStringToSign, stringToSign)
53 |
54 | let expectedSignature = "8745d16e49fb5550634d56c2c4bb6841e42d7595f8529cf9ea14d05d51935b20"
55 | let signature = try! signer.createSignature(
56 | stringToSign,
57 | timeStampShort: overridenDate.short,
58 | region: region
59 | )
60 |
61 | XCTAssertEqual(expectedSignature, signature)
62 |
63 | let expectedAuthHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=8745d16e49fb5550634d56c2c4bb6841e42d7595f8529cf9ea14d05d51935b20"
64 |
65 | let authHeader = try! signer.generateAuthHeader(.GET,
66 | url: requestURL,
67 | headers: updatedHeaders,
68 | bodyDigest: Payload.none.hashed(),
69 | dates: overridenDate,
70 | region: region)
71 |
72 | XCTAssertEqual(expectedAuthHeader, authHeader)
73 |
74 | let allExpectedHeadersForRequest = [
75 | "host": "s3.us-east-1.amazonaws.com",
76 | "x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
77 | "x-amz-date": "20130524T000000Z",
78 | "authorization": "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=8745d16e49fb5550634d56c2c4bb6841e42d7595f8529cf9ea14d05d51935b20"
79 | ]
80 |
81 | let allHeadersForRequest = try! signer.headers(for: .GET, urlString: requestURLString, payload: .none, dates: overridenDate)
82 |
83 | XCTAssertEqual(allExpectedHeadersForRequest, allHeadersForRequest.dictionaryRepresentation())
84 | }
85 |
86 | func test_Get_Vanilla_with_added_headers() {
87 | let requestURLString = region.hostUrlString()
88 | let requestURL = URL(string: requestURLString)!
89 |
90 | let updatedHeaders = signer.update(headers: ["My-Header1": "value4,value1,value3,value2"],
91 | url: requestURL,
92 | longDate: overridenDate.long,
93 | bodyDigest: Payload.none.hashed(),
94 | region: region)
95 |
96 | let expectedCanonRequest = [
97 | "GET",
98 | "/",
99 | "",
100 | "host:s3.us-east-1.amazonaws.com",
101 | "my-header1:value4,value1,value3,value2",
102 | "x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
103 | "x-amz-date:20130524T000000Z",
104 | "",
105 | "host;my-header1;x-amz-content-sha256;x-amz-date",
106 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
107 | ].joined(separator: "\n")
108 |
109 | let canonRequest = try! signer.createCanonicalRequest(.GET,
110 | url: requestURL,
111 | headers: updatedHeaders,
112 | bodyDigest: Payload.none.hashed())
113 |
114 | XCTAssertEqual(expectedCanonRequest, canonRequest)
115 |
116 | let expectedStringToSign = [
117 | "AWS4-HMAC-SHA256",
118 | "20130524T000000Z",
119 | "20130524/us-east-1/s3/aws4_request",
120 | "349cbfc1c3b792a0a1c113db82e905774d59a3a783b8a4c1635cf46e77b0fd4a"
121 | ].joined(separator: "\n")
122 |
123 | let stringToSign = try! signer.createStringToSign(canonRequest,
124 | dates: overridenDate,
125 | region: region)
126 |
127 | XCTAssertEqual(expectedStringToSign, stringToSign)
128 |
129 | let expectedSignature = "b6d537c39971b5174582a0191500f5815737863d2efec1d73fe0b7dd60433006"
130 |
131 | let signature = try! signer.createSignature(stringToSign,
132 | timeStampShort: overridenDate.short,
133 | region: region)
134 |
135 | XCTAssertEqual(expectedSignature, signature)
136 |
137 | let expectedAuthHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;my-header1;x-amz-content-sha256;x-amz-date, Signature=b6d537c39971b5174582a0191500f5815737863d2efec1d73fe0b7dd60433006"
138 |
139 | let authHeader = try! signer.generateAuthHeader(.GET, url: requestURL,
140 | headers: updatedHeaders,
141 | bodyDigest: Payload.none.hashed(),
142 | dates: overridenDate,
143 | region: region)
144 |
145 | XCTAssertEqual(expectedAuthHeader, authHeader)
146 | }
147 |
148 |
149 | func test_Post_With_Param_Vanilla() {
150 | let requestURLString = region.hostUrlString() + "?Param1=value1"
151 | let requestURL = URL(string: requestURLString)!
152 |
153 | let updatedHeaders = signer.update(headers: [:],
154 | url: requestURL,
155 | longDate: overridenDate.long,
156 | bodyDigest: Payload.none.hashed(),
157 | region: region)
158 |
159 | let expectedCanonRequest = [
160 | "POST",
161 | "/",
162 | "Param1=value1",
163 | "host:s3.us-east-1.amazonaws.com",
164 | "x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
165 | "x-amz-date:20130524T000000Z",
166 | "",
167 | "host;x-amz-content-sha256;x-amz-date",
168 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
169 | ].joined(separator: "\n")
170 |
171 | let canonRequest = try! signer.createCanonicalRequest(.POST,
172 | url: requestURL,
173 | headers: updatedHeaders,
174 | bodyDigest: Payload.none.hashed())
175 |
176 | XCTAssertEqual(expectedCanonRequest, canonRequest)
177 |
178 | let expectedStringToSign = [
179 | "AWS4-HMAC-SHA256",
180 | "20130524T000000Z",
181 | "20130524/us-east-1/s3/aws4_request",
182 | "9a8ec1a42be3e36ebd0880ea21ff11dac3c3519c3ab00a23ddb1b1ac4d4163b7"
183 | ].joined(separator: "\n")
184 |
185 | let stringToSign = try! signer.createStringToSign(canonRequest, dates: overridenDate, region: region)
186 |
187 | XCTAssertEqual(expectedStringToSign, stringToSign)
188 |
189 | let expectedSignature = "ea870aa535725edbb806253d7eaac9b0c38cdb256efc42c18739a2e8c14bc2ee"
190 |
191 | let signature = try! signer.createSignature(stringToSign, timeStampShort: overridenDate.short, region: region)
192 |
193 | XCTAssertEqual(expectedSignature, signature)
194 |
195 | let expectedAuthHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ea870aa535725edbb806253d7eaac9b0c38cdb256efc42c18739a2e8c14bc2ee"
196 |
197 | let authHeader = try! signer.generateAuthHeader(.POST,
198 | url: requestURL,
199 | headers: updatedHeaders,
200 | bodyDigest: Payload.none.hashed(),
201 | dates: overridenDate,
202 | region: region)
203 |
204 | XCTAssertEqual(expectedAuthHeader, authHeader)
205 |
206 | let allExpectedHeadersForRequest = [
207 | "host": "s3.us-east-1.amazonaws.com",
208 | "x-amz-date": "20130524T000000Z",
209 | "x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
210 | "authorization": "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ea870aa535725edbb806253d7eaac9b0c38cdb256efc42c18739a2e8c14bc2ee"
211 | ]
212 |
213 | let allHeadersForRequest = try! signer.headers(for: .POST, urlString: requestURLString, payload: .none, dates: overridenDate)
214 |
215 | XCTAssertEqual(allExpectedHeadersForRequest, allHeadersForRequest.dictionaryRepresentation())
216 | }
217 |
218 | }
219 |
220 |
221 | extension HTTPHeaders {
222 |
223 | func dictionaryRepresentation() -> [String: String] {
224 | var result = [String: String]()
225 | self.forEach { (header) in
226 | result[header.name] = header.value
227 | }
228 | return result
229 | }
230 |
231 | }
232 |
233 |
234 |
--------------------------------------------------------------------------------
/Tests/S3Tests/BaseTestCase.swift:
--------------------------------------------------------------------------------
1 | @testable import S3Signer
2 | import XCTest
3 |
4 |
5 | class BaseTestCase: XCTestCase {
6 |
7 | let accessKey = "AKIAIOSFODNN7EXAMPLE"
8 | let secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
9 |
10 | var overridenDate: Dates!
11 | var signer: S3Signer!
12 | var region: Region!
13 |
14 | override func setUp() {
15 | super.setUp()
16 | region = Region.usEast1
17 | signer = try! S3Signer(S3Signer.Config(accessKey: accessKey, secretKey: secretKey, region: region))
18 |
19 | // this is the "seconds" representation of "20130524T000000Z"
20 | overridenDate = Dates(Date(timeIntervalSince1970: (60*60*24) * 15849))
21 |
22 | if let s = try? S3Signer(S3Signer.Config(accessKey: accessKey, secretKey: secretKey, region: region)) {
23 | signer = s
24 | } else {
25 | XCTFail("Could not intialize signer")
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/S3Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests/S3Tests/S3SignerAWSTests.swift:
--------------------------------------------------------------------------------
1 | @testable import S3Signer
2 | import XCTest
3 |
4 | class S3SignerAWSTests: BaseTestCase {
5 |
6 | static var allTests = [
7 | ("test_TimeFromNow_Expiration", test_TimeFromNow_Expiration),
8 | ("test_Payload_bytes", test_Payload_bytes),
9 | ("test_Payload_none", test_Payload_none),
10 | ("test_Payload_unsigned", test_Payload_unsigned),
11 | ("test_Dates_formatting", test_Dates_formatting),
12 | ("test_Region_host", test_Region_host),
13 | ("test_S3Signer_get_dates", test_S3Signer_get_dates),
14 | ("test_S3Signer_service", test_S3Signer_service),
15 | ("test_Put_with_pathExtension_adds_content_length_And_content_type", test_Put_with_pathExtension_adds_content_length_And_content_type),
16 | ("test_Throws_on_bad_url", test_Throws_on_bad_url),
17 | ]
18 |
19 | func test_Dates_formatting() {
20 | let date = Date()
21 | let dates = Dates(date)
22 | XCTAssertEqual(dates.short, date.timestampShort)
23 | XCTAssertEqual(dates.long, date.timestampLong)
24 | }
25 |
26 | func test_TimeFromNow_Expiration() {
27 | let thiryMinutes = Expiration.thirtyMinutes
28 | XCTAssertEqual(thiryMinutes.value, 60 * 30)
29 | let oneHour = Expiration.hour
30 | XCTAssertEqual(oneHour.value, 60 * 60)
31 | let threeHours = Expiration.threeHours
32 | XCTAssertEqual(threeHours.value, 60 * 60 * 3)
33 | }
34 |
35 | func test_Payload_bytes() {
36 | let sampleBytes = "S3SignerAWS".data(using: .utf8)!
37 | let payloadBytes = Payload.bytes(sampleBytes)
38 | let payloadSize = sampleBytes.count.description
39 | XCTAssertTrue(payloadBytes.isBytes)
40 | XCTAssertFalse(payloadBytes.isUnsigned)
41 | XCTAssertEqual(sampleBytes, payloadBytes.bytes)
42 | XCTAssertEqual(payloadBytes.size(), payloadSize)
43 | }
44 |
45 | func test_Payload_none() {
46 | let sampleBytes = "".data(using: .utf8)!
47 | let payloadNone = Payload.none
48 | let payloadSize = sampleBytes.count.description
49 | XCTAssertTrue(payloadNone.isBytes)
50 | XCTAssertFalse(payloadNone.isUnsigned)
51 | XCTAssertEqual(sampleBytes, payloadNone.bytes)
52 | XCTAssertEqual(payloadNone.size(), payloadSize)
53 | }
54 |
55 | func test_Payload_unsigned() {
56 | let unsigned = "UNSIGNED-PAYLOAD"
57 | let payloadUnsigned = Payload.unsigned
58 | XCTAssertFalse(payloadUnsigned.isBytes)
59 | XCTAssertTrue(payloadUnsigned.isUnsigned)
60 | XCTAssertEqual(unsigned, payloadUnsigned.size())
61 | XCTAssertEqual(unsigned, payloadUnsigned.hashed())
62 | }
63 |
64 | func test_Region_host() {
65 | XCTAssertEqual(Region.caCentral1.host, "s3.ca-central-1.amazonaws.com")
66 | XCTAssertEqual(Region.usEast1.host, "s3.us-east-1.amazonaws.com")
67 | XCTAssertEqual(Region.usEast2.host, "s3.us-east-2.amazonaws.com")
68 | XCTAssertEqual(Region.usWest1.host, "s3.us-west-1.amazonaws.com")
69 | XCTAssertEqual(Region.usWest2.host, "s3.us-west-2.amazonaws.com")
70 | XCTAssertEqual(Region.euWest1.host, "s3.eu-west-1.amazonaws.com")
71 | XCTAssertEqual(Region.euWest2.host, "s3.eu-west-2.amazonaws.com")
72 | XCTAssertEqual(Region.euCentral1.host, "s3.eu-central-1.amazonaws.com")
73 | XCTAssertEqual(Region.apSouth1.host, "s3.ap-south-1.amazonaws.com")
74 | XCTAssertEqual(Region.apSoutheast1.host, "s3.ap-southeast-1.amazonaws.com")
75 | XCTAssertEqual(Region.apSoutheast2.host, "s3.ap-southeast-2.amazonaws.com")
76 | XCTAssertEqual(Region.apNortheast1.host, "s3.ap-northeast-1.amazonaws.com")
77 | XCTAssertEqual(Region.apNortheast2.host, "s3.ap-northeast-2.amazonaws.com")
78 | XCTAssertEqual(Region.saEast1.host, "s3.sa-east-1.amazonaws.com")
79 | }
80 |
81 | func test_S3Signer_get_dates() {
82 | let date = Date()
83 | let dates = signer.getDates(date)
84 | XCTAssertEqual(dates.short, date.timestampShort)
85 | XCTAssertEqual(dates.long, date.timestampLong)
86 | }
87 |
88 | func test_S3Signer_service() {
89 | XCTAssertEqual(signer.config.service, "s3")
90 | }
91 |
92 | func test_Put_with_pathExtension_adds_content_length_And_content_type() {
93 | let randomBytesMessage = "Welcome to Amazon S3.".data(using: .utf8)!
94 | let headers = try! signer.headers(for: .PUT,
95 | urlString: "https://www.someURL.com/someFile.txt",
96 | payload: .bytes(randomBytesMessage),
97 | dates: overridenDate)
98 |
99 | XCTAssertNotNil(headers["Content-Length"].first)
100 | XCTAssertNotNil(headers["Content-Type"].first)
101 | XCTAssertEqual(headers["Content-Length"].first, Payload.bytes(randomBytesMessage).size())
102 | XCTAssertEqual(headers["Content-Type"].first, "text/plain")
103 | }
104 |
105 | func test_Throws_on_bad_url() {
106 | XCTAssertThrowsError(try signer.headers(for: .GET, urlString: "", payload: .none))
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Tests/S3Tests/S3SignerV2Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import S3Signer
2 | import XCTest
3 |
4 |
5 | class S3SignerV2Tests: BaseTestCase {
6 | override func setUp() {
7 | super.setUp()
8 | signer = try! S3Signer(S3Signer.Config(accessKey: accessKey, secretKey: secretKey, region: region, version: .v2))
9 | if let s = try? S3Signer(S3Signer.Config(accessKey: accessKey, secretKey: secretKey, region: region, version: .v2)) {
10 | signer = s
11 | } else {
12 | XCTFail("Could not intialize signer")
13 | }
14 | }
15 |
16 | // TOOD: appropriate testing would try various cases (where URL has signable query items, etc)
17 |
18 | func testSignatureV2_AWS() throws {
19 | let requestURLString = region.hostUrlString()
20 | let requestURL = URL(string: requestURLString)!
21 |
22 | let updatedHeaders = signer.update(headers: [:], url: requestURL, longDate: overridenDate.long, bodyDigest: Payload.none.hashed(), region: region)
23 | let signature = try signer.generateAuthHeaderV2(.GET, url: requestURL, headers: updatedHeaders, dates: overridenDate, region: region, bucket: nil)
24 |
25 | XCTAssertEqual(signature, "AWS AKIAIOSFODNN7EXAMPLE:6sBgrGyWpHXvBFC/ip2imdLWe1U=")
26 | }
27 |
28 | func testSignatureV2_AWS_Bucket() throws {
29 | let requestURLString = region.hostUrlString()
30 | let requestURL = URL(string: requestURLString)!
31 |
32 | let updatedHeaders = signer.update(headers: [:], url: requestURL, longDate: overridenDate.long, bodyDigest: Payload.none.hashed(), region: region)
33 | let signature = try signer.generateAuthHeaderV2(.GET, url: requestURL, headers: updatedHeaders, dates: overridenDate, region: region, bucket: "SomeBucket")
34 |
35 | XCTAssertEqual(signature, "AWS AKIAIOSFODNN7EXAMPLE:MoWa/bEpN+BIPWryvy9dMxSvFsw=")
36 | }
37 |
38 | func testSignatureV2_CustomHost() throws {
39 | // since we are not using virtual hosting for custom hosts, signature should be the same
40 | let region = Region(name: .usEast1, hostName: "some.custom.site.com", useTLS: false)
41 | let requestURLString = region.hostUrlString()
42 | let requestURL = URL(string: requestURLString)!
43 |
44 | let updatedHeaders = signer.update(headers: [:], url: requestURL, longDate: overridenDate.long, bodyDigest: Payload.none.hashed(), region: region)
45 | let signature = try signer.generateAuthHeaderV2(.GET, url: requestURL, headers: updatedHeaders, dates: overridenDate, region: region, bucket: "SomeBucket")
46 |
47 | XCTAssertEqual(signature, "AWS AKIAIOSFODNN7EXAMPLE:MoWa/bEpN+BIPWryvy9dMxSvFsw=")
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/S3Tests/S3Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import S3Signer
2 | import XCTest
3 |
4 | class S3Tests: BaseTestCase {
5 |
6 | static var allTests = [
7 | ("test_Get_Object", test_Get_Object),
8 | ("test_Put_Object", test_Put_Object),
9 | ("test_Get_bucket_lifecycle", test_Get_bucket_lifecycle),
10 | ("test_Get_bucket_list_object", test_Get_bucket_list_object),
11 | ("test_Presigned_URL_V4", test_Presigned_URL_V4)
12 | ]
13 |
14 | // S3 example signature calcuations
15 | // https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#example-signature-calculations
16 |
17 | func test_Get_Object() {
18 | let requestURL = URL(string: "https://examplebucket.s3.amazonaws.com/test.txt")!
19 | let updatedHeaders = signer.update(
20 | headers: ["Range": "bytes=0-9"],
21 | url: requestURL,
22 | longDate: overridenDate.long,
23 | bodyDigest: Payload.none.hashed(),
24 | region: region
25 | )
26 |
27 | let expectedCanonRequest = [
28 | "GET",
29 | "/test.txt",
30 | "",
31 | "host:examplebucket.s3.amazonaws.com",
32 | "range:bytes=0-9",
33 | "x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
34 | "x-amz-date:20130524T000000Z",
35 | "",
36 | "host;range;x-amz-content-sha256;x-amz-date",
37 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
38 | ].joined(separator: "\n")
39 |
40 | let canonRequest = try! signer.createCanonicalRequest(.GET, url: requestURL, headers: updatedHeaders, bodyDigest: Payload.none.hashed())
41 |
42 | XCTAssertEqual(expectedCanonRequest, canonRequest)
43 |
44 | let expectedStringToSign = [
45 | "AWS4-HMAC-SHA256",
46 | "20130524T000000Z",
47 | "20130524/us-east-1/s3/aws4_request",
48 | "7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972"
49 | ].joined(separator: "\n")
50 |
51 | let stringToSign = try! signer.createStringToSign(canonRequest, dates: overridenDate, region: region)
52 |
53 | XCTAssertEqual(expectedStringToSign, stringToSign)
54 |
55 | let expectedSignature = "f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41"
56 |
57 | let signature = try! signer.createSignature(stringToSign, timeStampShort: overridenDate.short, region: region)
58 |
59 | XCTAssertEqual(expectedSignature, signature)
60 |
61 | let expectedAuthHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;range;x-amz-content-sha256;x-amz-date, Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41"
62 |
63 | let authHeader = try! signer.generateAuthHeader(.GET, url: requestURL, headers: updatedHeaders, bodyDigest: Payload.none.hashed(), dates: overridenDate, region: region)
64 |
65 | XCTAssertEqual(expectedAuthHeader, authHeader)
66 | }
67 |
68 | func test_Put_Object() {
69 | guard let requestURL = URL(string: "https://examplebucket.s3.amazonaws.com/test$file.text"),
70 | let bytes = "Welcome to Amazon S3.".data(using: .utf8) else {
71 | XCTFail("Could not intialize request/data")
72 | return
73 | }
74 |
75 | let payload = Payload.bytes(bytes).hashed()
76 |
77 | let updatedHeaders = signer.update(headers: ["x-amz-storage-class": "REDUCED_REDUNDANCY", "Date": "Fri, 24 May 2013 00:00:00 GMT"], url: requestURL, longDate: overridenDate.long, bodyDigest: payload, region: region)
78 |
79 | let expectedCanonRequest = [
80 | "PUT",
81 | "/test%24file.text",
82 | "",
83 | "date:Fri, 24 May 2013 00:00:00 GMT",
84 | "host:examplebucket.s3.amazonaws.com",
85 | "x-amz-content-sha256:44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072",
86 | "x-amz-date:20130524T000000Z",
87 | "x-amz-storage-class:REDUCED_REDUNDANCY",
88 | "",
89 | "date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class",
90 | "44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072"
91 | ].joined(separator: "\n")
92 |
93 | let canonRequest = try! signer.createCanonicalRequest(.PUT, url: requestURL, headers: updatedHeaders, bodyDigest: payload)
94 |
95 | XCTAssertEqual(expectedCanonRequest, canonRequest)
96 |
97 | let expectedStringToSign = [
98 | "AWS4-HMAC-SHA256",
99 | "20130524T000000Z",
100 | "20130524/us-east-1/s3/aws4_request",
101 | "9e0e90d9c76de8fa5b200d8c849cd5b8dc7a3be3951ddb7f6a76b4158342019d"
102 | ].joined(separator: "\n")
103 |
104 | let stringToSign = try! signer.createStringToSign(canonRequest, dates: overridenDate, region: region)
105 |
106 | XCTAssertEqual(expectedStringToSign, stringToSign)
107 |
108 | let expectedSignature = "98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd"
109 |
110 | let signature = try! signer.createSignature(stringToSign, timeStampShort: overridenDate.short, region: region)
111 |
112 | XCTAssertEqual(expectedSignature, signature)
113 |
114 | let expectedAuthHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class, Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd"
115 |
116 | let authHeader = try! signer.generateAuthHeader(.PUT, url: requestURL, headers: updatedHeaders, bodyDigest: payload, dates: overridenDate, region: region)
117 |
118 | XCTAssertEqual(expectedAuthHeader, authHeader)
119 | }
120 |
121 | func test_Get_bucket_lifecycle() {
122 | let requestURL = URL(string: "https://examplebucket.s3.amazonaws.com?lifecycle")!
123 | let payload = Payload.none.hashed()
124 | let updatedHeaders = signer.update(headers: [:], url: requestURL, longDate: overridenDate.long, bodyDigest: payload, region: region)
125 |
126 | let expectedCanonRequest = [
127 | "GET",
128 | "/",
129 | "lifecycle=",
130 | "host:examplebucket.s3.amazonaws.com",
131 | "x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
132 | "x-amz-date:20130524T000000Z",
133 | "",
134 | "host;x-amz-content-sha256;x-amz-date",
135 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
136 | ].joined(separator: "\n")
137 |
138 | let canonRequest = try! signer.createCanonicalRequest(.GET, url: requestURL, headers: updatedHeaders, bodyDigest: payload)
139 |
140 | XCTAssertEqual(expectedCanonRequest, canonRequest)
141 |
142 | let expectedAuthHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543"
143 |
144 | let authHeader = try! signer.generateAuthHeader(.GET, url: requestURL, headers: updatedHeaders, bodyDigest: payload, dates: overridenDate, region: region)
145 |
146 | XCTAssertEqual(expectedAuthHeader, authHeader)
147 | }
148 |
149 | func test_Get_bucket_list_object() {
150 | let requestURL = URL(string: "https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J")!
151 | let payload = Payload.none.hashed()
152 | let updatedHeaders = signer.update(headers: [:], url: requestURL, longDate: overridenDate.long, bodyDigest: payload, region: region)
153 |
154 | let expectedCanonRequest = [
155 | "GET",
156 | "/",
157 | "max-keys=2&prefix=J",
158 | "host:examplebucket.s3.amazonaws.com",
159 | "x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
160 | "x-amz-date:20130524T000000Z",
161 | "",
162 | "host;x-amz-content-sha256;x-amz-date",
163 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
164 | ].joined(separator: "\n")
165 |
166 | let canonRequest = try! signer.createCanonicalRequest(.GET, url: requestURL, headers: updatedHeaders, bodyDigest: payload)
167 |
168 | XCTAssertEqual(expectedCanonRequest, canonRequest)
169 |
170 | let expectedAuthHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7"
171 |
172 | let authHeader = try! signer.generateAuthHeader(.GET, url: requestURL, headers: updatedHeaders, bodyDigest: payload, dates: overridenDate, region: region)
173 |
174 | XCTAssertEqual(expectedAuthHeader, authHeader)
175 | }
176 |
177 | // Taken from https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
178 | func test_Presigned_URL_V4() {
179 | let requestURL = URL(string: "https://examplebucket.s3.amazonaws.com/test.txt")!
180 |
181 | let expectedCanonRequest = [
182 | "GET",
183 | "/test.txt",
184 | "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host",
185 | "host:examplebucket.s3.amazonaws.com",
186 | "",
187 | "host",
188 | "UNSIGNED-PAYLOAD"
189 | ].joined(separator: "\n")
190 |
191 | let (canonRequest, _) = try! signer.presignedURLCanonRequest(.GET, dates: overridenDate, expiration: Expiration.custom(86400), url: requestURL, region: region, headers: ["Host": requestURL.host ?? Region.usEast1.host])
192 |
193 | XCTAssertEqual(expectedCanonRequest, canonRequest)
194 |
195 | let expectedStringToSign = [
196 | "AWS4-HMAC-SHA256",
197 | "20130524T000000Z",
198 | "20130524/us-east-1/s3/aws4_request",
199 | "3bfa292879f6447bbcda7001decf97f4a54dc650c8942174ae0a9121cf58ad04"
200 | // "414cf5ed57126f2233fac1f7b5e5e4b8dcf58040010cb69d21f126d353d13df7"
201 | ].joined(separator: "\n")
202 |
203 | let stringToSign = try! signer.createStringToSign(canonRequest, dates: overridenDate, region: region)
204 |
205 | NSLog("expectedStringToSign: \(expectedStringToSign)")
206 | NSLog("stringToSign: \(stringToSign)")
207 | XCTAssertEqual(expectedStringToSign, stringToSign)
208 |
209 | // let expectedSignature = "17594f59285415a5be4debfcf5227a2d78b7c2634442b7ab816cace9333ec989"
210 | let expectedSignature = "aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404"
211 |
212 | let signature = try! signer.createSignature(stringToSign, timeStampShort: overridenDate.short, region: region)
213 |
214 | XCTAssertEqual(expectedSignature, signature)
215 |
216 |
217 | let expectedURLString = "https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=17594f59285415a5be4debfcf5227a2d78b7c2634442b7ab816cace9333ec989"
218 |
219 | let presignedURL = try! signer.presignedURL(for: .GET, url: requestURL, expiration: Expiration.custom(86400), region: region, headers: [:], dates: overridenDate)
220 |
221 | XCTAssertEqual(expectedURLString, presignedURL?.absoluteString)
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # install dependencies
4 | gem install xcpretty
5 | brew install vapor/tap/vapor
6 |
7 | # Generate up-to-date test interface
8 | echo "👾 Generate up-to-date test interface"
9 | vapor xcode -y
10 |
11 | # Build
12 | echo "🤖 Build"
13 | set -o pipefail && xcodebuild -scheme S3DemoRun clean build | xcpretty
14 |
15 | # Run
16 | echo "🏃♀️ Test"
17 | set -o pipefail && xcodebuild -scheme S3DemoRun test | xcpretty
18 |
--------------------------------------------------------------------------------
/scripts/update.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | rm -rf .build
4 | vapor clean -y --verbose
5 | vapor xcode -n --verbose
6 |
--------------------------------------------------------------------------------
/scripts/upgrade.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | rm -rf .build
4 | vapor clean -y --verbose
5 | rm Package.resolved
6 | vapor xcode -n --verbose
7 |
--------------------------------------------------------------------------------