mirror of
https://github.com/swift-server/swift-aws-lambda-runtime.git
synced 2026-06-02 07:27:33 +00:00
WIP deploy plugin
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftAWSLambdaRuntime open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the SwiftAWSLambdaRuntime project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import PackagePlugin
|
||||
|
||||
@main
|
||||
@available(macOS 15.0, *)
|
||||
struct AWSLambdaDeployer: CommandPlugin {
|
||||
|
||||
|
||||
func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws {
|
||||
let configuration = try Configuration(context: context, arguments: arguments)
|
||||
|
||||
if configuration.help {
|
||||
self.displayHelpMessage()
|
||||
return
|
||||
}
|
||||
|
||||
let tool = try context.tool(named: "AWSLambdaDeployerHelper")
|
||||
try Utils.execute(executable: tool.url, arguments: [], logLevel: .debug)
|
||||
}
|
||||
|
||||
private func displayHelpMessage() {
|
||||
print(
|
||||
"""
|
||||
OVERVIEW: A SwiftPM plugin to deploy a Lambda function.
|
||||
|
||||
USAGE: swift package lambda-deploy
|
||||
[--with-url]
|
||||
[--help] [--verbose]
|
||||
|
||||
OPTIONS:
|
||||
--with-url Add an URL to access the Lambda function
|
||||
--verbose Produce verbose output for debugging.
|
||||
--help Show help information.
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct Configuration: CustomStringConvertible {
|
||||
public let help: Bool
|
||||
public let verboseLogging: Bool
|
||||
|
||||
public init(
|
||||
context: PluginContext,
|
||||
arguments: [String]
|
||||
) throws {
|
||||
var argumentExtractor = ArgumentExtractor(arguments)
|
||||
let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0
|
||||
let helpArgument = argumentExtractor.extractFlag(named: "help") > 0
|
||||
|
||||
// help required ?
|
||||
self.help = helpArgument
|
||||
|
||||
// verbose logging required ?
|
||||
self.verboseLogging = verboseArgument
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"""
|
||||
{
|
||||
verboseLogging: \(self.verboseLogging)
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftAWSLambdaRuntime open source project
|
||||
//
|
||||
// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import PackagePlugin
|
||||
import Synchronization
|
||||
|
||||
@available(macOS 15.0, *)
|
||||
struct Utils {
|
||||
@discardableResult
|
||||
static func execute(
|
||||
executable: URL,
|
||||
arguments: [String],
|
||||
customWorkingDirectory: URL? = .none,
|
||||
logLevel: ProcessLogLevel
|
||||
) throws -> String {
|
||||
if logLevel >= .debug {
|
||||
print("\(executable.path()) \(arguments.joined(separator: " "))")
|
||||
}
|
||||
|
||||
let fd = dup(1)
|
||||
let stdout = fdopen(fd, "rw")
|
||||
defer { if let so = stdout { fclose(so) } }
|
||||
|
||||
// We need to use an unsafe transfer here to get the fd into our Sendable closure.
|
||||
// This transfer is fine, because we write to the variable from a single SerialDispatchQueue here.
|
||||
// We wait until the process is run below process.waitUntilExit().
|
||||
// This means no further writes to output will happen.
|
||||
// This makes it save for us to read the output
|
||||
struct UnsafeTransfer<Value>: @unchecked Sendable {
|
||||
let value: Value
|
||||
}
|
||||
|
||||
let outputMutex = Mutex("")
|
||||
let outputSync = DispatchGroup()
|
||||
let outputQueue = DispatchQueue(label: "AWSLambdaPackager.output")
|
||||
let unsafeTransfer = UnsafeTransfer(value: stdout)
|
||||
let outputHandler = { @Sendable (data: Data?) in
|
||||
dispatchPrecondition(condition: .onQueue(outputQueue))
|
||||
|
||||
outputSync.enter()
|
||||
defer { outputSync.leave() }
|
||||
|
||||
guard
|
||||
let _output = data.flatMap({
|
||||
String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"]))
|
||||
}), !_output.isEmpty
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
outputMutex.withLock { output in
|
||||
output += _output + "\n"
|
||||
}
|
||||
|
||||
switch logLevel {
|
||||
case .silent:
|
||||
break
|
||||
case .debug(let outputIndent), .output(let outputIndent):
|
||||
print(String(repeating: " ", count: outputIndent), terminator: "")
|
||||
print(_output)
|
||||
fflush(unsafeTransfer.value)
|
||||
}
|
||||
}
|
||||
|
||||
let pipe = Pipe()
|
||||
pipe.fileHandleForReading.readabilityHandler = { fileHandle in
|
||||
outputQueue.async { outputHandler(fileHandle.availableData) }
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
process.executableURL = executable
|
||||
process.arguments = arguments
|
||||
if let workingDirectory = customWorkingDirectory {
|
||||
process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory.path())
|
||||
}
|
||||
process.terminationHandler = { _ in
|
||||
outputQueue.async {
|
||||
outputHandler(try? pipe.fileHandleForReading.readToEnd())
|
||||
}
|
||||
}
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
// wait for output to be full processed
|
||||
outputSync.wait()
|
||||
|
||||
let output = outputMutex.withLock { $0 }
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
// print output on failure and if not already printed
|
||||
if logLevel < .output {
|
||||
print(output)
|
||||
fflush(stdout)
|
||||
}
|
||||
throw ProcessError.processFailed([executable.path()] + arguments, process.terminationStatus)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
enum ProcessError: Error, CustomStringConvertible {
|
||||
case processFailed([String], Int32)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .processFailed(let arguments, let code):
|
||||
return "\(arguments.joined(separator: " ")) failed with code \(code)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ProcessLogLevel: Comparable {
|
||||
case silent
|
||||
case output(outputIndent: Int)
|
||||
case debug(outputIndent: Int)
|
||||
|
||||
var naturalOrder: Int {
|
||||
switch self {
|
||||
case .silent:
|
||||
return 0
|
||||
case .output:
|
||||
return 1
|
||||
case .debug:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
static var output: Self {
|
||||
.output(outputIndent: 2)
|
||||
}
|
||||
|
||||
static var debug: Self {
|
||||
.debug(outputIndent: 2)
|
||||
}
|
||||
|
||||
static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool {
|
||||
lhs.naturalOrder < rhs.naturalOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftAWSLambdaRuntime open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the SwiftAWSLambdaRuntime project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
//
|
||||
// credentials.swift
|
||||
// aws-sign
|
||||
//
|
||||
// Created by Adam Fowler on 29/08/2019.
|
||||
//
|
||||
import class Foundation.ProcessInfo
|
||||
|
||||
/// Protocol for providing credential details for accessing AWS services
|
||||
public protocol Credential {
|
||||
var accessKeyId: String {get}
|
||||
var secretAccessKey: String {get}
|
||||
var sessionToken: String? {get}
|
||||
}
|
||||
|
||||
/// basic version of Credential where you supply the credentials
|
||||
public struct StaticCredential: Credential {
|
||||
public let accessKeyId: String
|
||||
public let secretAccessKey: String
|
||||
public let sessionToken: String?
|
||||
|
||||
public init(accessKeyId: String, secretAccessKey: String, sessionToken: String? = nil) {
|
||||
self.accessKeyId = accessKeyId
|
||||
self.secretAccessKey = secretAccessKey
|
||||
self.sessionToken = sessionToken
|
||||
}
|
||||
}
|
||||
|
||||
/// environment variable version of credential that uses system environment variables to get credential details
|
||||
public struct EnvironmentCredential: Credential {
|
||||
public let accessKeyId: String
|
||||
public let secretAccessKey: String
|
||||
public let sessionToken: String?
|
||||
|
||||
public init?() {
|
||||
guard let accessKeyId = ProcessInfo.processInfo.environment["AWS_ACCESS_KEY_ID"] else {
|
||||
return nil
|
||||
}
|
||||
guard let secretAccessKey = ProcessInfo.processInfo.environment["AWS_SECRET_ACCESS_KEY"] else {
|
||||
return nil
|
||||
}
|
||||
self.accessKeyId = accessKeyId
|
||||
self.secretAccessKey = secretAccessKey
|
||||
self.sessionToken = ProcessInfo.processInfo.environment["AWS_SESSION_TOKEN"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the SwiftAWSLambdaRuntime open source project
|
||||
//
|
||||
// Copyright (c) 2023 Apple Inc. and the SwiftAWSLambdaRuntime project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
//
|
||||
// signer.swift
|
||||
// AWSSigner
|
||||
//
|
||||
// Created by Adam Fowler on 2019/08/29.
|
||||
// Amazon Web Services V4 Signer
|
||||
// AWS documentation about signing requests is here https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html
|
||||
//
|
||||
|
||||
import struct Foundation.CharacterSet
|
||||
import struct Foundation.Data
|
||||
import struct Foundation.Date
|
||||
import class Foundation.DateFormatter
|
||||
import struct Foundation.Locale
|
||||
import struct Foundation.TimeZone
|
||||
import struct Foundation.URL
|
||||
import Crypto
|
||||
import NIO
|
||||
import NIOHTTP1
|
||||
|
||||
/// Amazon Web Services V4 Signer
|
||||
public struct AWSSigner {
|
||||
/// security credentials for accessing AWS services
|
||||
public let credentials: Credential
|
||||
/// service signing name. In general this is the same as the service name
|
||||
public let name: String
|
||||
/// AWS region you are working in
|
||||
public let region: String
|
||||
|
||||
static let hashedEmptyBody = SHA256.hash(data: [UInt8]()).hexDigest()
|
||||
|
||||
static private let timeStampDateFormatter: DateFormatter = createTimeStampDateFormatter()
|
||||
|
||||
/// Initialise the Signer class with AWS credentials
|
||||
public init(credentials: Credential, name: String, region: String) {
|
||||
self.credentials = credentials
|
||||
self.name = name
|
||||
self.region = region
|
||||
}
|
||||
|
||||
/// Enum for holding your body data
|
||||
public enum BodyData {
|
||||
case string(String)
|
||||
case data(Data)
|
||||
case byteBuffer(ByteBuffer)
|
||||
}
|
||||
|
||||
/// Generate signed headers, for a HTTP request
|
||||
public func signHeaders(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: BodyData? = nil, date: Date = Date()) -> HTTPHeaders {
|
||||
let bodyHash = AWSSigner.hashedPayload(body)
|
||||
let dateString = AWSSigner.timestamp(date)
|
||||
var headers = headers
|
||||
// add date, host, sha256 and if available security token headers
|
||||
headers.add(name: "X-Amz-Date", value: dateString)
|
||||
headers.add(name: "host", value: url.host ?? "")
|
||||
headers.add(name: "x-amz-content-sha256", value: bodyHash)
|
||||
if let sessionToken = credentials.sessionToken {
|
||||
headers.add(name: "x-amz-security-token", value: sessionToken)
|
||||
}
|
||||
|
||||
// construct signing data. Do this after adding the headers as it uses data from the headers
|
||||
let signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, bodyHash: bodyHash, date: dateString, signer: self)
|
||||
|
||||
// construct authorization string
|
||||
let authorization = "AWS4-HMAC-SHA256 " +
|
||||
"Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request, " +
|
||||
"SignedHeaders=\(signingData.signedHeaders), " +
|
||||
"Signature=\(signature(signingData: signingData))"
|
||||
|
||||
// add Authorization header
|
||||
headers.add(name: "Authorization", value: authorization)
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/// Generate a signed URL, for a HTTP request
|
||||
public func signURL(url: URL, method: HTTPMethod = .GET, body: BodyData? = nil, date: Date = Date(), expires: Int = 86400) -> URL {
|
||||
let headers = HTTPHeaders([("host", url.host ?? "")])
|
||||
// Create signing data
|
||||
var signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, date: AWSSigner.timestamp(date), signer: self)
|
||||
// Construct query string. Start with original query strings and append all the signing info.
|
||||
var query = url.query ?? ""
|
||||
if query.count > 0 {
|
||||
query += "&"
|
||||
}
|
||||
query += "X-Amz-Algorithm=AWS4-HMAC-SHA256"
|
||||
query += "&X-Amz-Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request"
|
||||
query += "&X-Amz-Date=\(signingData.datetime)"
|
||||
query += "&X-Amz-Expires=\(expires)"
|
||||
query += "&X-Amz-SignedHeaders=\(signingData.signedHeaders)"
|
||||
if let sessionToken = credentials.sessionToken {
|
||||
query += "&X-Amz-Security-Token=\(sessionToken.uriEncode())"
|
||||
}
|
||||
// Split the string and sort to ensure the order of query strings is the same as AWS
|
||||
query = query.split(separator: "&")
|
||||
.sorted()
|
||||
.joined(separator: "&")
|
||||
.queryEncode()
|
||||
|
||||
// update unsignedURL in the signingData so when the canonical request is constructed it includes all the signing query items
|
||||
signingData.unsignedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)! // NEED TO DEAL WITH SITUATION WHERE THIS FAILS
|
||||
query += "&X-Amz-Signature=\(signature(signingData: signingData))"
|
||||
|
||||
// Add signature to query items and build a new Request
|
||||
let signedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)!
|
||||
|
||||
return signedURL
|
||||
}
|
||||
|
||||
/// structure used to store data used throughout the signing process
|
||||
struct SigningData {
|
||||
let url : URL
|
||||
let method : HTTPMethod
|
||||
let hashedPayload : String
|
||||
let datetime : String
|
||||
let headersToSign: [String: String]
|
||||
let signedHeaders : String
|
||||
var unsignedURL : URL
|
||||
|
||||
var date : String { return String(datetime.prefix(8))}
|
||||
|
||||
init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: BodyData? = nil, bodyHash: String? = nil, date: String, signer: AWSSigner) {
|
||||
if url.path == "" {
|
||||
//URL has to have trailing slash
|
||||
self.url = url.appendingPathComponent("/")
|
||||
} else {
|
||||
self.url = url
|
||||
}
|
||||
self.method = method
|
||||
self.datetime = date
|
||||
self.unsignedURL = self.url
|
||||
|
||||
if let hash = bodyHash {
|
||||
self.hashedPayload = hash
|
||||
} else if signer.name == "s3" {
|
||||
self.hashedPayload = "UNSIGNED-PAYLOAD"
|
||||
} else {
|
||||
self.hashedPayload = AWSSigner.hashedPayload(body)
|
||||
}
|
||||
|
||||
let headersNotToSign: Set<String> = [
|
||||
"Authorization"
|
||||
]
|
||||
var headersToSign: [String: String] = [:]
|
||||
var signedHeadersArray: [String] = []
|
||||
for header in headers {
|
||||
if headersNotToSign.contains(header.name) {
|
||||
continue
|
||||
}
|
||||
headersToSign[header.name] = header.value
|
||||
signedHeadersArray.append(header.name.lowercased())
|
||||
}
|
||||
self.headersToSign = headersToSign
|
||||
self.signedHeaders = signedHeadersArray.sorted().joined(separator: ";")
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3 Calculating signature as in https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
||||
func signature(signingData: SigningData) -> String {
|
||||
let kDate = HMAC<SHA256>.authenticationCode(for: Data(signingData.date.utf8), using: SymmetricKey(data: Array("AWS4\(credentials.secretAccessKey)".utf8)))
|
||||
let kRegion = HMAC<SHA256>.authenticationCode(for: Data(region.utf8), using: SymmetricKey(data: kDate))
|
||||
let kService = HMAC<SHA256>.authenticationCode(for: Data(name.utf8), using: SymmetricKey(data: kRegion))
|
||||
let kSigning = HMAC<SHA256>.authenticationCode(for: Data("aws4_request".utf8), using: SymmetricKey(data: kService))
|
||||
let kSignature = HMAC<SHA256>.authenticationCode(for: stringToSign(signingData: signingData), using: SymmetricKey(data: kSigning))
|
||||
return kSignature.hexDigest()
|
||||
}
|
||||
|
||||
/// Stage 2 Create the string to sign as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||
func stringToSign(signingData: SigningData) -> Data {
|
||||
let stringToSign = "AWS4-HMAC-SHA256\n" +
|
||||
"\(signingData.datetime)\n" +
|
||||
"\(signingData.date)/\(region)/\(name)/aws4_request\n" +
|
||||
SHA256.hash(data: canonicalRequest(signingData: signingData)).hexDigest()
|
||||
return Data(stringToSign.utf8)
|
||||
}
|
||||
|
||||
/// Stage 1 Create the canonical request as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
func canonicalRequest(signingData: SigningData) -> Data {
|
||||
let canonicalHeaders = signingData.headersToSign.map { return "\($0.key.lowercased()):\($0.value.trimmingCharacters(in: CharacterSet.whitespaces))" }
|
||||
.sorted()
|
||||
.joined(separator: "\n")
|
||||
let canonicalRequest = "\(signingData.method.rawValue)\n" +
|
||||
"\(signingData.unsignedURL.path.uriEncodeWithSlash())\n" +
|
||||
"\(signingData.unsignedURL.query ?? "")\n" + // should really uriEncode all the query string values
|
||||
"\(canonicalHeaders)\n\n" +
|
||||
"\(signingData.signedHeaders)\n" +
|
||||
signingData.hashedPayload
|
||||
return Data(canonicalRequest.utf8)
|
||||
}
|
||||
|
||||
/// Create a SHA256 hash of the Requests body
|
||||
static func hashedPayload(_ payload: BodyData?) -> String {
|
||||
guard let payload = payload else { return hashedEmptyBody }
|
||||
let hash : String?
|
||||
switch payload {
|
||||
case .string(let string):
|
||||
hash = SHA256.hash(data: Data(string.utf8)).hexDigest()
|
||||
case .data(let data):
|
||||
hash = SHA256.hash(data: data).hexDigest()
|
||||
case .byteBuffer(let byteBuffer):
|
||||
let byteBufferView = byteBuffer.readableBytesView
|
||||
hash = byteBufferView.withContiguousStorageIfAvailable { bytes in
|
||||
return SHA256.hash(data: bytes).hexDigest()
|
||||
}
|
||||
}
|
||||
if let hash = hash {
|
||||
return hash
|
||||
} else {
|
||||
return hashedEmptyBody
|
||||
}
|
||||
}
|
||||
|
||||
/// return a hexEncoded string buffer from an array of bytes
|
||||
static func hexEncoded(_ buffer: [UInt8]) -> String {
|
||||
return buffer.map{String(format: "%02x", $0)}.joined(separator: "")
|
||||
}
|
||||
/// create timestamp dateformatter
|
||||
static private func createTimeStampDateFormatter() -> DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
|
||||
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
return formatter
|
||||
}
|
||||
|
||||
/// return a timestamp formatted for signing requests
|
||||
static func timestamp(_ date: Date) -> String {
|
||||
return timeStampDateFormatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func queryEncode() -> String {
|
||||
return addingPercentEncoding(withAllowedCharacters: String.queryAllowedCharacters) ?? self
|
||||
}
|
||||
|
||||
func uriEncode() -> String {
|
||||
return addingPercentEncoding(withAllowedCharacters: String.uriAllowedCharacters) ?? self
|
||||
}
|
||||
|
||||
func uriEncodeWithSlash() -> String {
|
||||
return addingPercentEncoding(withAllowedCharacters: String.uriAllowedWithSlashCharacters) ?? self
|
||||
}
|
||||
|
||||
static let uriAllowedWithSlashCharacters = CharacterSet(charactersIn:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/")
|
||||
static let uriAllowedCharacters = CharacterSet(charactersIn:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
|
||||
static let queryAllowedCharacters = CharacterSet(charactersIn:"/;+").inverted
|
||||
}
|
||||
|
||||
public extension Sequence where Element == UInt8 {
|
||||
/// return a hexEncoded string buffer from an array of bytes
|
||||
func hexDigest() -> String {
|
||||
return self.map{String(format: "%02x", $0)}.joined(separator: "")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
print("Deployer")
|
||||
Reference in New Issue
Block a user