diff --git a/Other/Postman/Vapor S3.postman_collection.json b/Other/Postman/Vapor S3.postman_collection.json new file mode 100644 index 0000000..7f898c8 --- /dev/null +++ b/Other/Postman/Vapor S3.postman_collection.json @@ -0,0 +1,105 @@ +{ + "info": { + "_postman_id": "da87b927-b230-431d-91bf-57200936e59f", + "name": "Vapor S3", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Buckets list", + "request": { + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "{{SERVER}}buckets", + "host": [ + "{{SERVER}}buckets" + ] + } + }, + "response": [] + }, + { + "name": "Bucket (create)", + "request": { + "method": "PUT", + "header": [], + "body": {}, + "url": { + "raw": "{{SERVER}}bucket", + "host": [ + "{{SERVER}}bucket" + ] + } + }, + "response": [] + }, + { + "name": "Bucket (delete)", + "request": { + "method": "DELETE", + "header": [], + "body": {}, + "url": { + "raw": "{{SERVER}}bucket", + "host": [ + "{{SERVER}}bucket" + ] + } + }, + "response": [] + }, + { + "name": "Bucket (location)", + "request": { + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "{{SERVER}}bucket/location", + "host": [ + "{{SERVER}}bucket" + ], + "path": [ + "location" + ] + } + }, + "response": [] + }, + { + "name": "Files (list)", + "request": { + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "{{SERVER}}files", + "host": [ + "{{SERVER}}files" + ] + } + }, + "response": [] + }, + { + "name": "Files (test)", + "request": { + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "{{SERVER}}files/test", + "host": [ + "{{SERVER}}files" + ], + "path": [ + "test" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index 1249270..7b855d7 100644 --- a/Package.swift +++ b/Package.swift @@ -9,8 +9,8 @@ let package = Package( .library(name: "S3TestTools", targets: ["S3TestTools"]) ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0-rc.2"), - .package(url: "https://github.com/LiveUI/XMLCoding.git", .branch("master")), + .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), + .package(url: "https://github.com/LiveUI/XMLCoding.git", from: "0.1.0"), .package(url: "https://github.com/LiveUI/VaporTestTools.git", .branch("master")) ], targets: [ diff --git a/README.md b/README.md index c43f463..af09bec 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - [x] Listing buckets - [x] Create bucket - [x] Delete bucket +- [x] Locate bucket region - [x] List objects - [x] Upload file - [x] Get file @@ -54,14 +55,11 @@ public protocol S3Client: Service { /// Create a bucket func create(bucket: String, region: Region?, on container: Container) throws -> Future - /// Delete a bucket wherever it is (WIP) -// func delete(bucket: String, on container: Container) throws -> Future - /// Delete a bucket func delete(bucket: String, region: Region?, on container: Container) throws -> Future - /// Get bucket location (WIP) -// func location(bucket: String, on container: Container) throws -> Future + /// Get bucket location + func location(bucket: String, on container: Container) throws -> Future /// Get list of objects func list(bucket: String, region: Region?, on container: Container) throws -> Future @@ -144,6 +142,24 @@ public func routes(_ router: Router) throws { ) } + // Locate bucket (get region) + router.get("bucket/location") { req -> Future in + let s3 = try req.makeS3Client() + return try s3.location(bucket: "bucket-name", on: req).map(to: String.self) { region in + return region.hostUrlString() + }.catchMap({ (error) -> (String) in + if let error = error as? S3.Error { + switch error { + case .errorResponse(_, let error): + return error.message + default: + return "S3 :(" + } + } + return ":(" + } + ) + } // Delete bucket router.delete("bucket") { req -> Future in let s3 = try req.makeS3Client() diff --git a/Sources/S3/Extensions/S3+Bucket.swift b/Sources/S3/Extensions/S3+Bucket.swift index 2a523e0..0566935 100644 --- a/Sources/S3/Extensions/S3+Bucket.swift +++ b/Sources/S3/Extensions/S3+Bucket.swift @@ -15,44 +15,51 @@ public extension S3 { // MARK: Buckets -// /// Get bucket location -// public func location(bucket: String, on container: Container) throws -> Future { -// let region = region ?? signer.config.region -// guard let url = URL(string: "https://\(bucket).s3.amazonaws.com/?location"), let host = url.host else { -// throw Error.invalidUrl -// } -// -// let headers = [ -// "host": host -// ] -// let awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, headers: headers, payload: .none) -// return try make(request: url, method: .GET, headers: awsHeaders, data: "".convertToData(), on: container).map(to: Bucket.Location.self) { response in -// if response.http.status == .notFound { -// throw Error.notFound -// } -// guard response.http.status == .ok || response.http.status == .noContent else { -// if let error = try? response.decode(to: ErrorMessage.self) { -// throw Error.errorResponse(response.http.status, error) -// } else { -// throw Error.badResponse(response) -// } -// } -// return try response.decode(to: Bucket.Location.self) -// } -// } + /// Get bucket location + public func location(bucket: String, on container: Container) throws -> Future { + let builder = urlBuilder(for: container) + let region = Region.euWest2 + let url = try builder.url(region: region, bucket: bucket) + + let awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, payload: .none) + return try make(request: url, method: .GET, headers: awsHeaders, data: emptyData(), on: container).map(to: Region.self) { response in + if response.http.status == .notFound { + throw Error.notFound + } + if response.http.status == .ok { + return region + } else { + if let error = try? response.decode(to: ErrorMessage.self), error.code == "PermanentRedirect", let endpoint = error.endpoint { + if endpoint == "s3.amazonaws.com" { + return Region.usEast1 + } else { + // Split bucket.s3.region.amazonaws.com into parts + var parts = endpoint.split(separator: ".") + // Remove .com + parts.removeLast() + // Remove .amazonaws + parts.removeLast() + // Get region (lat part) + let regionString = String(parts.removeLast()).lowercased() + guard let region = Region(rawValue: regionString) else { + throw Error.badResponse(response) + } + return region + } + } else { + throw Error.badResponse(response) + } + } + } + } /// Delete bucket public func delete(bucket: String, region: Region? = nil, on container: Container) throws -> Future { - let region = region ?? signer.config.region - guard let url = URL(string: "https://\(bucket).s3.\(region.rawValue).amazonaws.com/"), let host = url.host else { - throw Error.invalidUrl - } + let builder = urlBuilder(for: container) + let url = try builder.url(region: region, bucket: bucket) - let headers = [ - "host": host - ] - let awsHeaders = try signer.headers(for: .DELETE, urlString: url.absoluteString, region: region, headers: headers, payload: .none) - return try make(request: url, method: .DELETE, headers: awsHeaders, data: "".convertToData(), on: container).map(to: Void.self) { response in + let awsHeaders = try signer.headers(for: .DELETE, urlString: url.absoluteString, region: region, payload: .none) + return try make(request: url, method: .DELETE, headers: awsHeaders, data: emptyData(), on: container).map(to: Void.self) { response in try self.check(response) return Void() } @@ -61,9 +68,9 @@ public extension S3 { /// Create a bucket public func create(bucket: String, region: Region? = nil, on container: Container) throws -> Future { let region = region ?? signer.config.region - guard let url = URL(string: "https://\(bucket).s3.\(region.rawValue).amazonaws.com/"), let host = url.host else { - throw Error.invalidUrl - } + + let builder = urlBuilder(for: container) + let url = try builder.url(region: region, bucket: bucket) let content = """ @@ -72,11 +79,7 @@ public extension S3 { """ let data = content.convertToData() - - let headers = [ - "host": host - ] - let awsHeaders = try signer.headers(for: .PUT, urlString: url.absoluteString, region: region, headers: headers, payload: .bytes(data)) + let awsHeaders = try signer.headers(for: .PUT, urlString: url.absoluteString, region: region, payload: .bytes(data)) return try make(request: url, method: .PUT, headers: awsHeaders, data: data, on: container).map(to: Void.self) { response in try self.check(response) return Void() diff --git a/Sources/S3/Extensions/S3+Delete.swift b/Sources/S3/Extensions/S3+Delete.swift index bc899c0..65f2a05 100644 --- a/Sources/S3/Extensions/S3+Delete.swift +++ b/Sources/S3/Extensions/S3+Delete.swift @@ -16,9 +16,11 @@ public extension S3 { /// Delete file from S3 public func delete(file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { - let url = try self.url(file: file, on: container) + let builder = urlBuilder(for: container) + let url = try builder.url(file: file) + let headers = try signer.headers(for: .DELETE, urlString: url.absoluteString, headers: headers, payload: .none) - return try make(request: url, method: .DELETE, headers: headers, data: "".convertToData(), on: container).map(to: Void.self) { response in + return try make(request: url, method: .DELETE, headers: headers, data: emptyData(), on: container).map(to: Void.self) { response in try self.check(response) return Void() diff --git a/Sources/S3/Extensions/S3+Get.swift b/Sources/S3/Extensions/S3+Get.swift index 0f39bfd..eff14fe 100644 --- a/Sources/S3/Extensions/S3+Get.swift +++ b/Sources/S3/Extensions/S3+Get.swift @@ -16,7 +16,9 @@ public extension S3 { /// Retrieve file data from S3 public func get(file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { - let url = try self.url(file: file, on: container) + let builder = urlBuilder(for: container) + let url = try builder.url(file: file) + let headers = try signer.headers(for: .GET, urlString: url.absoluteString, headers: headers, payload: .none) return try make(request: url, method: .GET, headers: headers, on: container).map(to: File.Response.self) { response in try self.check(response) diff --git a/Sources/S3/Extensions/S3+List.swift b/Sources/S3/Extensions/S3+List.swift index ddc6360..7386a81 100644 --- a/Sources/S3/Extensions/S3+List.swift +++ b/Sources/S3/Extensions/S3+List.swift @@ -27,7 +27,7 @@ extension S3 { var headers = headers headers["host"] = host let awsHeaders = try signer.headers(for: .GET, urlString: url.absoluteString, region: region, headers: headers, payload: .none) - return try make(request: url, method: .GET, headers: awsHeaders, data: "".convertToData(), on: container).map(to: BucketResults.self) { response in + return try make(request: url, method: .GET, headers: awsHeaders, data: emptyData(), on: container).map(to: BucketResults.self) { response in try self.check(response) return try response.decode(to: BucketResults.self) } diff --git a/Sources/S3/Extensions/S3+ObjectInfo.swift b/Sources/S3/Extensions/S3+ObjectInfo.swift index 664de80..0e4319a 100644 --- a/Sources/S3/Extensions/S3+ObjectInfo.swift +++ b/Sources/S3/Extensions/S3+ObjectInfo.swift @@ -30,9 +30,11 @@ public extension S3 { /// Get file information (HEAD) /// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html public func get(fileInfo file: LocationConvertible, headers: [String: String], on container: Container) throws -> Future { - let url = try self.url(file: file, on: container) + let builder = urlBuilder(for: container) + let url = try builder.url(file: file) + let headers = try signer.headers(for: .HEAD, urlString: url.absoluteString, headers: headers, payload: .none) - return try make(request: url, method: .HEAD, headers: headers, data: "".convertToData(), on: container).map(to: File.Info.self) { response in + return try make(request: url, method: .HEAD, headers: headers, data: emptyData(), on: container).map(to: File.Info.self) { response in try self.check(response) let bucket = file.bucket ?? self.defaultBucket diff --git a/Sources/S3/Extensions/S3+Put.swift b/Sources/S3/Extensions/S3+Put.swift index c5c815a..7cca8cb 100755 --- a/Sources/S3/Extensions/S3+Put.swift +++ b/Sources/S3/Extensions/S3+Put.swift @@ -17,7 +17,10 @@ public extension S3 { /// Upload file to S3 public func put(file: File.Upload, headers: [String: String], on container: Container) throws -> EventLoopFuture { - let url = try self.url(file: file, on: container) + let builder = urlBuilder(for: container) + let url = try builder.url(file: file) + +// let url = URL(string: "https://s3.eu-west-2.amazonaws.com/s3-liveui-test/file-hu.txt")! var awsHeaders: [String: String] = headers awsHeaders["content-type"] = file.mime.description diff --git a/Sources/S3/Extensions/S3+Service.swift b/Sources/S3/Extensions/S3+Service.swift index f6cc1f1..520a550 100644 --- a/Sources/S3/Extensions/S3+Service.swift +++ b/Sources/S3/Extensions/S3+Service.swift @@ -17,9 +17,10 @@ public extension S3 { /// Get list of buckets public func buckets(on container: Container) throws -> Future { - let url = try self.url(on: container) + let builder = urlBuilder(for: container) + let url = try builder.plain() let headers = try signer.headers(for: .GET, urlString: url.absoluteString, payload: .none) - return try make(request: url, method: .GET, headers: headers, data: "".convertToData(), on: container).map(to: BucketsInfo.self) { response in + return try make(request: url, method: .GET, headers: headers, data: emptyData(), on: container).map(to: BucketsInfo.self) { response in try self.check(response) return try response.decode(to: BucketsInfo.self) } diff --git a/Sources/S3/Libs/URLBuilder.swift b/Sources/S3/Libs/URLBuilder.swift new file mode 100644 index 0000000..e922a91 --- /dev/null +++ b/Sources/S3/Libs/URLBuilder.swift @@ -0,0 +1,77 @@ +// +// URLBuilder.swift +// S3 +// +// Created by Ondrej Rafaj on 16/05/2018. +// + +import Foundation +import Vapor +import S3Signer + + +extension Region { + + /// Host URL including scheme + public func hostUrlString(bucket: String? = nil) -> String { + var bucket = bucket + if let b = bucket { + bucket = b + "." + } + return "https://" + (bucket ?? "") + host.finished(with: "/") + } + +} + + +/// URL builder +class URLBuilder { + + /// Container + let container: Container + + /// Default bucket + let defaultBucket: String + + /// S3 Configuration + let config: S3Signer.Config + + /// Initializer + init(_ container: Container, defaultBucket: String, config: S3Signer.Config) { + self.container = container + self.defaultBucket = defaultBucket + self.config = config + } + + /// Plain Base URL with no bucket specified + /// *Format: https://s3.eu-west-2.amazonaws.com/ + func plain(region: Region? = nil) throws -> URL { + let urlString = (region ?? config.region).hostUrlString() + guard let url = URL(string: urlString) else { + throw S3.Error.invalidUrl + } + return url + } + + /// Base URL for S3 region + /// *Format: https://bucket.s3.eu-west-2.amazonaws.com/path_or_parameter* + func url(region: Region? = nil, bucket: String? = nil, path: String? = nil) throws -> URL { + let urlString = (region ?? config.region).hostUrlString(bucket: (bucket ?? defaultBucket)) + guard let url = URL(string: urlString) else { + throw S3.Error.invalidUrl + } + return url + } + + /// Base URL for a file in a bucket + /// * Format: https://s3.eu-west-2.amazonaws.com/bucket/file.txt + /// * We can't have a bucket in the host or DELETE will attempt to delete the bucket, not file! + func url(file: LocationConvertible) throws -> URL { + let urlString = (file.region ?? config.region).hostUrlString() + guard let url = URL(string: urlString)?.appendingPathComponent(file.bucket ?? defaultBucket).appendingPathComponent(file.path) else { + throw S3.Error.invalidUrl + } + return url + } + +} diff --git a/Sources/S3/Models/Bucket.swift b/Sources/S3/Models/Bucket.swift index 03a4aaa..024f462 100644 --- a/Sources/S3/Models/Bucket.swift +++ b/Sources/S3/Models/Bucket.swift @@ -9,49 +9,6 @@ import Foundation import Vapor -/// Base object for /buckets endpoint -public struct BucketsInfo: Content { - - /// Owner - public let owner: Owner - - /// Available buckets - public let buckets: [Bucket] - - /// Coding keys - enum CodingKeys: String, CodingKey { - case owner = "Owner" - case additionalInfo = "Buckets" - } - - /// Additional (helper) coding keys - enum AdditionalInfoKeys: String, CodingKey { - case buckets = "Bucket" - } - - /// Init from Decoder - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - owner = try values.decode(Owner.self, forKey: .owner) - - let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo) - - buckets = try additionalInfo.decode([Bucket].self, forKey: .buckets) - } - - /// Encode - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(owner, forKey: .owner) - - var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo) - - try additionalInfo.encode(buckets, forKey: .buckets) - } - -} - - /// Bucket model public struct Bucket: Content { diff --git a/Sources/S3/Models/BucketsInfo.swift b/Sources/S3/Models/BucketsInfo.swift new file mode 100644 index 0000000..a656585 --- /dev/null +++ b/Sources/S3/Models/BucketsInfo.swift @@ -0,0 +1,65 @@ +// +// BucketsInfo.swift +// S3 +// +// Created by Ondrej Rafaj on 16/05/2018. +// + +import Foundation +import Vapor + + +/// Base object for /buckets endpoint +public struct BucketsInfo: Content { + + /// Owner + public let owner: Owner? + + /// Available buckets + public let buckets: [Bucket]? + + /// Max keys + public let maxKeys: Int? + + /// Max keys + public let truncated: Bool? + + /// Coding keys + enum CodingKeys: String, CodingKey { + case owner = "Owner" + case additionalInfo = "Buckets" + case maxKeys = "MaxKeys" + case truncated = "IsTruncated" + } + + /// Additional (helper) coding keys + enum AdditionalInfoKeys: String, CodingKey { + case buckets = "Bucket" + } + + /// Init from Decoder + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + owner = try? values.decode(Owner.self, forKey: .owner) + // TODO: Make the following better!!!!!! + maxKeys = Int((try? values.decode(String.self, forKey: .maxKeys)) ?? "1000") ?? 1000 + truncated = Bool((try? values.decode(String.self, forKey: .truncated)) ?? "false") ?? false + + if let additionalInfo = try? values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo) { + buckets = try? additionalInfo.decode([Bucket].self, forKey: .buckets) + } else { + buckets = nil + } + } + + /// Encode + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(owner, forKey: .owner) + + var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo) + + try additionalInfo.encode(buckets, forKey: .buckets) + } + +} diff --git a/Sources/S3/Models/ErrorMessage.swift b/Sources/S3/Models/ErrorMessage.swift index 5c1b5b2..1dcd49a 100644 --- a/Sources/S3/Models/ErrorMessage.swift +++ b/Sources/S3/Models/ErrorMessage.swift @@ -21,6 +21,9 @@ public struct ErrorMessage: Content { /// Bucket involved? public let bucket: String? + /// Header involved? + public let endpoint: String? + /// Header involved? public let header: String? @@ -34,6 +37,7 @@ public struct ErrorMessage: Content { case code = "Code" case message = "Message" case bucket = "BucketName" + case endpoint = "Endpoint" case header = "Header" case requestId = "RequestId" case hostId = "HostId" diff --git a/Sources/S3/Protocols/S3Client.swift b/Sources/S3/Protocols/S3Client.swift index 3dcc649..3e577b0 100644 --- a/Sources/S3/Protocols/S3Client.swift +++ b/Sources/S3/Protocols/S3Client.swift @@ -25,7 +25,7 @@ public protocol S3Client: Service { func delete(bucket: String, region: Region?, on container: Container) throws -> Future /// Get bucket location -// func location(bucket: String, on container: Container) throws -> Future + func location(bucket: String, on container: Container) throws -> Future /// Get list of objects func list(bucket: String, region: Region?, on container: Container) throws -> Future diff --git a/Sources/S3/S3.swift b/Sources/S3/S3.swift index bf74f01..1709f73 100755 --- a/Sources/S3/S3.swift +++ b/Sources/S3/S3.swift @@ -55,6 +55,12 @@ public class S3: S3Client { extension S3 { + // QUESTION: Can we replace this with just Data()? + /// Serve empty data + func emptyData() -> Data { + return "".convertToData() + } + /// Check response for error @discardableResult func check(_ response: Response) throws -> Response { guard response.http.status == .ok || response.http.status == .noContent else { @@ -80,22 +86,9 @@ extension S3 { return S3.mimeType(forFileAtUrl: url) } - /// Base URL for S3 region - func url(region: Region? = nil, bucket: String? = nil, on container: Container) throws -> URL { - let urlString = (region ?? signer.config.region).hostUrlString + (bucket?.finished(with: "/") ?? "") - guard let url = URL(string: urlString) else { - throw Error.invalidUrl - } - return url - } - - /// Base URL for a file in a bucket - func url(file: LocationConvertible, on container: Container) throws -> URL { - let bucket = file.bucket ?? defaultBucket - guard let url = URL(string: signer.config.region.hostUrlString + bucket.finished(with: "/") + file.path) else { - throw Error.invalidUrl - } - return url + /// Create URL builder + func urlBuilder(for container: Container) -> URLBuilder { + return URLBuilder(container, defaultBucket: defaultBucket, config: signer.config) } } diff --git a/Sources/S3DemoApp/S3DemoApp.swift b/Sources/S3DemoApp/S3DemoApp.swift index 83c582e..ad8c18b 100644 --- a/Sources/S3DemoApp/S3DemoApp.swift +++ b/Sources/S3DemoApp/S3DemoApp.swift @@ -50,24 +50,24 @@ public func routes(_ router: Router) throws { }) } -// // Bucket location -// router.get("bucket/location") { req -> Future in -// let s3 = try req.makeS3Client() -// return try s3.location(bucket: "api-created-bucket", on: req).map(to: String.self) { location in -// return location.region -// }.catchMap({ (error) -> (String) in -// if let error = error as? S3.Error { -// switch error { -// case .errorResponse(_, let error): -// return error.message -// default: -// return "S3 :(" -// } -// } -// return ":(" -// } -// ) -// } + // Bucket location + router.get("bucket/location") { req -> Future in + let s3 = try req.makeS3Client() + return try s3.location(bucket: "adfasdfasdfasdf", on: req).map(to: String.self) { region in + return region.hostUrlString() + }.catchMap({ (error) -> (String) in + if let error = error as? S3.Error { + switch error { + case .errorResponse(_, let error): + return error.message + default: + return "S3 :(" + } + } + return ":(" + } + ) + } // Demonstrate work with files router.get("files/test") { req -> Future in @@ -101,7 +101,12 @@ public func routes(_ router: Router) throws { ) } } - } + }.catchMap({ error -> (String) in + if let error = error.s3ErroMessage() { + return error.message + } + return ":(" + }) } catch { print(error) fatalError() diff --git a/Sources/S3Signer/Region.swift b/Sources/S3Signer/Region.swift index 89e170f..46a3aff 100755 --- a/Sources/S3Signer/Region.swift +++ b/Sources/S3Signer/Region.swift @@ -57,14 +57,9 @@ public enum Region: String, Codable { extension Region { - /// Generate base URL + /// Base URL / Host public var host: String { - return "s3.\(rawValue).amazonaws.com".finished(with: "/") - } - - /// Host URL including scheme - public var hostUrlString: String { - return "https://" + host + return "s3.\(rawValue).amazonaws.com" } } diff --git a/Sources/S3Signer/S3Signer+Private.swift b/Sources/S3Signer/S3Signer+Private.swift index ddb6167..dd696a9 100755 --- a/Sources/S3Signer/S3Signer+Private.swift +++ b/Sources/S3Signer/S3Signer+Private.swift @@ -110,7 +110,7 @@ extension S3Signer { var updatedHeaders = headers updatedHeaders["x-amz-date"] = longDate if (updatedHeaders["host"] ?? updatedHeaders["host"]) == nil { - updatedHeaders["host"] = url.host ?? (region ?? config.region).host + updatedHeaders["host"] = (url.host ?? (region ?? config.region).host) } if bodyDigest != "UNSIGNED-PAYLOAD" && config.service == "s3" { updatedHeaders["x-amz-content-sha256"] = bodyDigest