Parse basic options in OptionsBundle
- Handle isEncrypted inside CryptoContainer - Rename ParsingError to OptionsError Reuse OptionsBundle in ConfigurationParser.
This commit is contained in:
parent
e7dadefabb
commit
b9b9c4db60
|
@ -137,6 +137,10 @@
|
|||
0EC1BBA620D712DE007C4C7B /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */; };
|
||||
0EC1BBA820D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; };
|
||||
0EC1BBA920D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; };
|
||||
0ECC60D5225497400020BEAC /* OptionsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D4225497400020BEAC /* OptionsBundle.swift */; };
|
||||
0ECC60D6225497400020BEAC /* OptionsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D4225497400020BEAC /* OptionsBundle.swift */; };
|
||||
0ECC60D82254981A0020BEAC /* OptionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* OptionsError.swift */; };
|
||||
0ECC60D92254981A0020BEAC /* OptionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* OptionsError.swift */; };
|
||||
0ECE3528212EB7770040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; };
|
||||
0ECE352A212EB88E0040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; };
|
||||
0ECEB1152252C8E900E9E551 /* tunnelbear.enc.8.ovpn in Resources */ = {isa = PBXBuildFile; fileRef = 0ECEB1132252C8E900E9E551 /* tunnelbear.enc.8.ovpn */; };
|
||||
|
@ -342,6 +346,8 @@
|
|||
0EBBF2FF2085196000E36B40 /* NWTCPConnectionState+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWTCPConnectionState+Description.swift"; sourceTree = "<group>"; };
|
||||
0EC1BBA420D71190007C4C7B /* DNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
|
||||
0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = "<group>"; };
|
||||
0ECC60D4225497400020BEAC /* OptionsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsBundle.swift; sourceTree = "<group>"; };
|
||||
0ECC60D72254981A0020BEAC /* OptionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsError.swift; sourceTree = "<group>"; };
|
||||
0ECE3527212EB7770040F253 /* CryptoContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoContainer.swift; sourceTree = "<group>"; };
|
||||
0ECEB1132252C8E900E9E551 /* tunnelbear.enc.8.ovpn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.ovpn; sourceTree = "<group>"; };
|
||||
0ECEB1142252C8E900E9E551 /* tunnelbear.enc.8.key */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.key; sourceTree = "<group>"; };
|
||||
|
@ -646,6 +652,8 @@
|
|||
0EFEB42D2006D3C800F81029 /* MSS.h */,
|
||||
0EFEB43D2006D3C800F81029 /* MSS.m */,
|
||||
0E12B29D21449ADB00B4BAE9 /* NSRegularExpression+Shortcuts.swift */,
|
||||
0ECC60D4225497400020BEAC /* OptionsBundle.swift */,
|
||||
0ECC60D72254981A0020BEAC /* OptionsError.swift */,
|
||||
0EFEB43E2006D3C800F81029 /* Packet.swift */,
|
||||
0EE7A79420F61EDC00B42E6A /* PacketMacros.h */,
|
||||
0EE7A79720F6296F00B42E6A /* PacketMacros.m */,
|
||||
|
@ -1225,6 +1233,7 @@
|
|||
0EFEB4722006D3C800F81029 /* ReplayProtector.m in Sources */,
|
||||
0EFEB4782006D3C800F81029 /* TunnelKitProvider+Configuration.swift in Sources */,
|
||||
0E3E0F212108A8CC00B371C1 /* SessionProxy+PushReply.swift in Sources */,
|
||||
0ECC60D82254981A0020BEAC /* OptionsError.swift in Sources */,
|
||||
0EFEB4752006D3C800F81029 /* Errors.m in Sources */,
|
||||
0E58BF532240FAA6006FB157 /* SessionProxy+CompressionAlgorithm.swift in Sources */,
|
||||
0E12B2A521454F7F00B4BAE9 /* BidirectionalState.swift in Sources */,
|
||||
|
@ -1232,6 +1241,7 @@
|
|||
0EFEB4762006D3C800F81029 /* DataPath.m in Sources */,
|
||||
0E0C2127212ED29D008AB282 /* SessionProxy+Configuration.swift in Sources */,
|
||||
0EFEB4692006D3C800F81029 /* Packet.swift in Sources */,
|
||||
0ECC60D5225497400020BEAC /* OptionsBundle.swift in Sources */,
|
||||
0E011F7A2196D93600BA59EE /* SocketType.swift in Sources */,
|
||||
0EFEB45A2006D3C800F81029 /* TunnelInterface.swift in Sources */,
|
||||
);
|
||||
|
@ -1292,6 +1302,7 @@
|
|||
0EFEB4AF2007627700F81029 /* InterfaceObserver.swift in Sources */,
|
||||
0EFEB4A42006D7F300F81029 /* DataPath.m in Sources */,
|
||||
0EBBF2E62084FE6F00E36B40 /* GenericSocket.swift in Sources */,
|
||||
0ECC60D92254981A0020BEAC /* OptionsError.swift in Sources */,
|
||||
0E3E0F222108A8CC00B371C1 /* SessionProxy+PushReply.swift in Sources */,
|
||||
0E58BF542240FAA6006FB157 /* SessionProxy+CompressionAlgorithm.swift in Sources */,
|
||||
0E12B2A621454F7F00B4BAE9 /* BidirectionalState.swift in Sources */,
|
||||
|
@ -1299,6 +1310,7 @@
|
|||
0EFEB49D2006D7F300F81029 /* IOInterface.swift in Sources */,
|
||||
0E0C2128212ED29D008AB282 /* SessionProxy+Configuration.swift in Sources */,
|
||||
0EFEB4972006D7F300F81029 /* SessionProxy+Authenticator.swift in Sources */,
|
||||
0ECC60D6225497400020BEAC /* OptionsBundle.swift in Sources */,
|
||||
0E011F7B2196D93600BA59EE /* SocketType.swift in Sources */,
|
||||
0EFEB49B2006D7F300F81029 /* Packet.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -32,22 +32,6 @@ private let log = SwiftyBeaver.self
|
|||
/// Provides methods to parse a `SessionProxy.Configuration` from an .ovpn configuration file.
|
||||
public class ConfigurationParser {
|
||||
|
||||
/// Error raised by the parser, with details about the line that triggered it.
|
||||
public enum ParsingError: Error {
|
||||
|
||||
/// The file misses a required option.
|
||||
case missingConfiguration(option: String)
|
||||
|
||||
/// The file includes an unsupported option.
|
||||
case unsupportedConfiguration(option: String)
|
||||
|
||||
/// Passphrase required to decrypt private keys.
|
||||
case encryptionPassphrase
|
||||
|
||||
/// Encryption passphrase is incorrect or key is corrupt.
|
||||
case unableToDecrypt(error: Error)
|
||||
}
|
||||
|
||||
/// Result of the parser.
|
||||
public struct ParsingResult {
|
||||
|
||||
|
@ -69,51 +53,8 @@ public class ConfigurationParser {
|
|||
/// - Seealso: `ConfigurationParser.parsed(...)`
|
||||
public let strippedLines: [String]?
|
||||
|
||||
/// Holds an optional `ParsingError` that didn't block the parser, but it would be worth taking care of.
|
||||
public let warning: ParsingError?
|
||||
}
|
||||
|
||||
private struct Regex {
|
||||
static let proto = NSRegularExpression("^proto +(udp6?|tcp6?)")
|
||||
|
||||
static let port = NSRegularExpression("^port +\\d+")
|
||||
|
||||
static let remote = NSRegularExpression("^remote +[^ ]+( +\\d+)?( +(udp6?|tcp6?))?")
|
||||
|
||||
static let cipher = NSRegularExpression("^cipher +[^,\\s]+")
|
||||
|
||||
static let auth = NSRegularExpression("^auth +[\\w\\-]+")
|
||||
|
||||
static let compLZO = NSRegularExpression("^comp-lzo.*")
|
||||
|
||||
static let compress = NSRegularExpression("^compress.*")
|
||||
|
||||
static let ping = NSRegularExpression("^ping +\\d+")
|
||||
|
||||
static let renegSec = NSRegularExpression("^reneg-sec +\\d+")
|
||||
|
||||
static let keyDirection = NSRegularExpression("^key-direction +\\d")
|
||||
|
||||
static let eku = NSRegularExpression("^remote-cert-tls +server")
|
||||
|
||||
static let blockBegin = NSRegularExpression("^<[\\w\\-]+>")
|
||||
|
||||
static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>")
|
||||
|
||||
static let dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+")
|
||||
|
||||
static let remoteRandom = NSRegularExpression("^remote-random")
|
||||
|
||||
// unsupported
|
||||
|
||||
// static let fragment = NSRegularExpression("^fragment +\\d+")
|
||||
static let fragment = NSRegularExpression("^fragment")
|
||||
|
||||
static let proxy = NSRegularExpression("^\\w+-proxy")
|
||||
|
||||
static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ")
|
||||
|
||||
static let connection = NSRegularExpression("^<connection>")
|
||||
/// Holds an optional `OptionsError` that didn't block the parser, but it would be worth taking care of.
|
||||
public let warning: OptionsError?
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,7 +64,7 @@ public class ConfigurationParser {
|
|||
- Parameter passphrase: The optional passphrase for encrypted data.
|
||||
- Parameter returnsStripped: When `true`, stores the stripped file into `ParsingResult.strippedLines`. Defaults to `false`.
|
||||
- Returns: The `ParsingResult` outcome of the parsing.
|
||||
- Throws: `ParsingError` if the configuration file is wrong or incomplete.
|
||||
- Throws: `OptionsError` if the configuration file is wrong or incomplete.
|
||||
*/
|
||||
public static func parsed(fromURL url: URL, passphrase: String? = nil, returnsStripped: Bool = false) throws -> ParsingResult {
|
||||
let lines = try String(contentsOf: url).trimmedLines()
|
||||
|
@ -138,362 +79,56 @@ public class ConfigurationParser {
|
|||
- Parameter originalURL: The optional original URL of the configuration file.
|
||||
- Parameter returnsStripped: When `true`, stores the stripped file into `ParsingResult.strippedLines`. Defaults to `false`.
|
||||
- Returns: The `ParsingResult` outcome of the parsing.
|
||||
- Throws: `ParsingError` if the configuration file is wrong or incomplete.
|
||||
- Throws: `OptionsError` if the configuration file is wrong or incomplete.
|
||||
*/
|
||||
public static func parsed(fromLines lines: [String], passphrase: String? = nil, originalURL: URL? = nil, returnsStripped: Bool = false) throws -> ParsingResult {
|
||||
var strippedLines: [String]? = returnsStripped ? [] : nil
|
||||
var warning: ParsingError? = nil
|
||||
let options = try OptionsBundle(from: lines, returnsStripped: returnsStripped)
|
||||
|
||||
var defaultProto: SocketType?
|
||||
var defaultPort: UInt16?
|
||||
var remotes: [(String, UInt16?, SocketType?)] = []
|
||||
|
||||
var cipher: SessionProxy.Cipher?
|
||||
var digest: SessionProxy.Digest?
|
||||
var compressionFraming: SessionProxy.CompressionFraming = .disabled
|
||||
var compressionAlgorithm: SessionProxy.CompressionAlgorithm = .disabled
|
||||
var optCA: CryptoContainer?
|
||||
var clientCertificate: CryptoContainer?
|
||||
var clientKey: CryptoContainer?
|
||||
var checksEKU = false
|
||||
var keepAliveSeconds: TimeInterval?
|
||||
var renegotiateAfterSeconds: TimeInterval?
|
||||
var keyDirection: StaticKey.Direction?
|
||||
var tlsStrategy: SessionProxy.TLSWrap.Strategy?
|
||||
var tlsKeyLines: [Substring]?
|
||||
var tlsWrap: SessionProxy.TLSWrap?
|
||||
var dnsServers: [String]?
|
||||
var randomizeEndpoint = false
|
||||
|
||||
var currentBlockName: String?
|
||||
var currentBlock: [String] = []
|
||||
var unsupportedError: ParsingError? = nil
|
||||
|
||||
log.verbose("Configuration file:")
|
||||
for line in lines {
|
||||
log.verbose(line)
|
||||
|
||||
var isHandled = false
|
||||
var strippedLine = line
|
||||
defer {
|
||||
if isHandled {
|
||||
strippedLines?.append(strippedLine)
|
||||
}
|
||||
}
|
||||
|
||||
// check blocks first
|
||||
Regex.connection.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = ParsingError.unsupportedConfiguration(option: "<connection> blocks")
|
||||
}
|
||||
|
||||
if unsupportedError == nil {
|
||||
if currentBlockName == nil {
|
||||
Regex.blockBegin.enumerateComponents(in: line) {
|
||||
isHandled = true
|
||||
let tag = $0.first!
|
||||
let from = tag.index(after: tag.startIndex)
|
||||
let to = tag.index(before: tag.endIndex)
|
||||
|
||||
currentBlockName = String(tag[from..<to])
|
||||
currentBlock = []
|
||||
}
|
||||
}
|
||||
Regex.blockEnd.enumerateComponents(in: line) {
|
||||
isHandled = true
|
||||
let tag = $0.first!
|
||||
let from = tag.index(tag.startIndex, offsetBy: 2)
|
||||
let to = tag.index(before: tag.endIndex)
|
||||
|
||||
let blockName = String(tag[from..<to])
|
||||
guard blockName == currentBlockName else {
|
||||
return
|
||||
}
|
||||
|
||||
// first is opening tag
|
||||
currentBlock.removeFirst()
|
||||
switch blockName {
|
||||
case "ca":
|
||||
optCA = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
||||
|
||||
case "cert":
|
||||
clientCertificate = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
||||
|
||||
case "key":
|
||||
let isEncrypted = normalizeEncryptedPEMBlock(block: ¤tBlock)
|
||||
let container = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
||||
if isEncrypted {
|
||||
guard let passphrase = passphrase else {
|
||||
unsupportedError = ParsingError.encryptionPassphrase
|
||||
break
|
||||
}
|
||||
do {
|
||||
clientKey = try container.decrypted(with: passphrase)
|
||||
} catch let e {
|
||||
unsupportedError = ParsingError.unableToDecrypt(error: e)
|
||||
}
|
||||
} else {
|
||||
clientKey = container
|
||||
}
|
||||
|
||||
case "tls-auth":
|
||||
tlsKeyLines = currentBlock.map { Substring($0) }
|
||||
tlsStrategy = .auth
|
||||
|
||||
case "tls-crypt":
|
||||
tlsKeyLines = currentBlock.map { Substring($0) }
|
||||
tlsStrategy = .crypt
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
currentBlockName = nil
|
||||
currentBlock = []
|
||||
}
|
||||
}
|
||||
if let _ = currentBlockName {
|
||||
currentBlock.append(line)
|
||||
continue
|
||||
}
|
||||
|
||||
Regex.eku.enumerateComponents(in: line) { (_) in
|
||||
checksEKU = true
|
||||
}
|
||||
Regex.proto.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
defaultProto = SocketType(protoString: str)
|
||||
if defaultProto == nil {
|
||||
unsupportedError = ParsingError.unsupportedConfiguration(option: "proto \(str)")
|
||||
}
|
||||
}
|
||||
Regex.port.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
defaultPort = UInt16(str)
|
||||
}
|
||||
Regex.remote.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let hostname = $0.first else {
|
||||
return
|
||||
}
|
||||
var port: UInt16?
|
||||
var proto: SocketType?
|
||||
var strippedComponents = ["remote", "<hostname>"]
|
||||
if $0.count > 1 {
|
||||
port = UInt16($0[1])
|
||||
strippedComponents.append($0[1])
|
||||
}
|
||||
if $0.count > 2 {
|
||||
proto = SocketType(protoString: $0[2])
|
||||
strippedComponents.append($0[2])
|
||||
}
|
||||
remotes.append((hostname, port, proto))
|
||||
|
||||
// replace private data
|
||||
strippedLine = strippedComponents.joined(separator: " ")
|
||||
}
|
||||
Regex.cipher.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let rawValue = $0.first else {
|
||||
return
|
||||
}
|
||||
cipher = SessionProxy.Cipher(rawValue: rawValue.uppercased())
|
||||
if cipher == nil {
|
||||
unsupportedError = ParsingError.unsupportedConfiguration(option: "cipher \(rawValue)")
|
||||
}
|
||||
}
|
||||
Regex.auth.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let rawValue = $0.first else {
|
||||
return
|
||||
}
|
||||
digest = SessionProxy.Digest(rawValue: rawValue.uppercased())
|
||||
if digest == nil {
|
||||
unsupportedError = ParsingError.unsupportedConfiguration(option: "auth \(rawValue)")
|
||||
}
|
||||
}
|
||||
Regex.compLZO.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
compressionFraming = .compLZO
|
||||
|
||||
if !LZOIsSupported() {
|
||||
guard let arg = $0.first else {
|
||||
warning = warning ?? .unsupportedConfiguration(option: line)
|
||||
return
|
||||
}
|
||||
guard arg == "no" else {
|
||||
unsupportedError = .unsupportedConfiguration(option: line)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
let arg = $0.first
|
||||
compressionAlgorithm = (arg == "no") ? .disabled : .LZO
|
||||
}
|
||||
}
|
||||
Regex.compress.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
compressionFraming = .compress
|
||||
|
||||
if !LZOIsSupported() {
|
||||
guard $0.isEmpty else {
|
||||
unsupportedError = .unsupportedConfiguration(option: line)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if let arg = $0.first {
|
||||
compressionAlgorithm = (arg == "lzo") ? .LZO : .other
|
||||
} else {
|
||||
compressionAlgorithm = .disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
Regex.keyDirection.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first, let value = Int(arg) else {
|
||||
return
|
||||
}
|
||||
keyDirection = StaticKey.Direction(rawValue: value)
|
||||
}
|
||||
Regex.ping.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first else {
|
||||
return
|
||||
}
|
||||
keepAliveSeconds = TimeInterval(arg)
|
||||
}
|
||||
Regex.renegSec.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first else {
|
||||
return
|
||||
}
|
||||
renegotiateAfterSeconds = TimeInterval(arg)
|
||||
}
|
||||
Regex.dns.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard $0.count == 2 else {
|
||||
return
|
||||
}
|
||||
if dnsServers == nil {
|
||||
dnsServers = []
|
||||
}
|
||||
dnsServers?.append($0[1])
|
||||
}
|
||||
Regex.remoteRandom.enumerateComponents(in: line) { (_) in
|
||||
randomizeEndpoint = true
|
||||
}
|
||||
Regex.fragment.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = ParsingError.unsupportedConfiguration(option: "fragment")
|
||||
}
|
||||
Regex.proxy.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = ParsingError.unsupportedConfiguration(option: "proxy: \"\(line)\"")
|
||||
}
|
||||
Regex.externalFiles.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = ParsingError.unsupportedConfiguration(option: "external file: \"\(line)\"")
|
||||
}
|
||||
if line.contains("mtu") || line.contains("mssfix") {
|
||||
isHandled = true
|
||||
}
|
||||
|
||||
if let error = unsupportedError {
|
||||
throw error
|
||||
}
|
||||
guard let ca = options.ca else {
|
||||
throw OptionsError.missingConfiguration(option: "ca")
|
||||
}
|
||||
|
||||
guard let ca = optCA else {
|
||||
throw ParsingError.missingConfiguration(option: "ca")
|
||||
guard let hostname = options.hostname, !options.remotes.isEmpty else {
|
||||
throw OptionsError.missingConfiguration(option: "remote")
|
||||
}
|
||||
let endpointProtocols = options.remotes.map { EndpointProtocol($0.2, $0.1) }
|
||||
|
||||
// XXX: only reads first remote
|
||||
// hostnames = remotes.map { $0.0 }
|
||||
guard !remotes.isEmpty else {
|
||||
throw ParsingError.missingConfiguration(option: "remote")
|
||||
}
|
||||
let hostname = remotes[0].0
|
||||
|
||||
defaultProto = defaultProto ?? .udp
|
||||
defaultPort = defaultPort ?? 1194
|
||||
|
||||
// XXX: reads endpoints from remotes with matching hostname
|
||||
var endpointProtocols: [EndpointProtocol] = []
|
||||
remotes.forEach {
|
||||
guard $0.0 == hostname else {
|
||||
return
|
||||
var optClientKey: CryptoContainer?
|
||||
if let clientKey = options.clientKey, clientKey.isEncrypted {
|
||||
guard let passphrase = passphrase else {
|
||||
throw OptionsError.encryptionPassphrase
|
||||
}
|
||||
guard let port = $0.1 ?? defaultPort else {
|
||||
return
|
||||
}
|
||||
guard let socketType = $0.2 ?? defaultProto else {
|
||||
return
|
||||
}
|
||||
endpointProtocols.append(EndpointProtocol(socketType, port))
|
||||
}
|
||||
|
||||
assert(!endpointProtocols.isEmpty, "Must define an endpoint protocol")
|
||||
|
||||
if let keyLines = tlsKeyLines, let strategy = tlsStrategy {
|
||||
let optKey: StaticKey?
|
||||
switch strategy {
|
||||
case .auth:
|
||||
optKey = StaticKey(lines: keyLines, direction: keyDirection)
|
||||
|
||||
case .crypt:
|
||||
optKey = StaticKey(lines: keyLines, direction: .client)
|
||||
}
|
||||
if let key = optKey {
|
||||
tlsWrap = SessionProxy.TLSWrap(strategy: strategy, key: key)
|
||||
do {
|
||||
optClientKey = try clientKey.decrypted(with: passphrase)
|
||||
} catch let e {
|
||||
throw OptionsError.unableToDecrypt(error: e)
|
||||
}
|
||||
} else {
|
||||
optClientKey = options.clientKey
|
||||
}
|
||||
|
||||
var sessionBuilder = SessionProxy.ConfigurationBuilder(ca: ca)
|
||||
sessionBuilder.cipher = cipher ?? .aes128cbc
|
||||
sessionBuilder.digest = digest ?? .sha1
|
||||
sessionBuilder.compressionFraming = compressionFraming
|
||||
sessionBuilder.compressionAlgorithm = compressionAlgorithm
|
||||
sessionBuilder.tlsWrap = tlsWrap
|
||||
sessionBuilder.clientCertificate = clientCertificate
|
||||
sessionBuilder.clientKey = clientKey
|
||||
sessionBuilder.checksEKU = checksEKU
|
||||
sessionBuilder.keepAliveInterval = keepAliveSeconds
|
||||
sessionBuilder.renegotiatesAfter = renegotiateAfterSeconds
|
||||
sessionBuilder.dnsServers = dnsServers
|
||||
sessionBuilder.randomizeEndpoint = randomizeEndpoint
|
||||
sessionBuilder.cipher = options.cipher ?? .aes128cbc
|
||||
sessionBuilder.digest = options.digest ?? .sha1
|
||||
sessionBuilder.compressionFraming = options.compressionFraming ?? .disabled
|
||||
sessionBuilder.compressionAlgorithm = options.compressionAlgorithm ?? .disabled
|
||||
sessionBuilder.tlsWrap = options.tlsWrap
|
||||
sessionBuilder.clientCertificate = options.clientCertificate
|
||||
sessionBuilder.clientKey = optClientKey
|
||||
sessionBuilder.checksEKU = options.checksEKU
|
||||
sessionBuilder.keepAliveInterval = options.keepAliveSeconds
|
||||
sessionBuilder.renegotiatesAfter = options.renegotiateAfterSeconds
|
||||
sessionBuilder.dnsServers = options.dnsServers
|
||||
sessionBuilder.randomizeEndpoint = options.randomizeEndpoint
|
||||
|
||||
return ParsingResult(
|
||||
url: originalURL,
|
||||
hostname: hostname,
|
||||
protocols: endpointProtocols,
|
||||
configuration: sessionBuilder.build(),
|
||||
strippedLines: strippedLines,
|
||||
warning: warning
|
||||
strippedLines: options.strippedLines,
|
||||
warning: options.warning
|
||||
)
|
||||
}
|
||||
|
||||
private static func normalizeEncryptedPEMBlock(block: inout [String]) -> Bool {
|
||||
if block.count >= 1 && block[0].contains("ENCRYPTED") {
|
||||
return true
|
||||
}
|
||||
|
||||
// XXX: restore blank line after encryption header (easier than tweaking trimmedLines)
|
||||
if block.count >= 3 && block[1].contains("Proc-Type") {
|
||||
block.insert("", at: 3)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private extension SocketType {
|
||||
init?(protoString: String) {
|
||||
var str = protoString
|
||||
if str.hasSuffix("6") {
|
||||
str.removeLast()
|
||||
}
|
||||
self.init(rawValue: str.uppercased())
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
|
|
@ -82,7 +82,12 @@ extension CryptoContainer: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
extension CryptoContainer {
|
||||
/// :nodoc:
|
||||
public extension CryptoContainer {
|
||||
var isEncrypted: Bool {
|
||||
return pem.contains("ENCRYPTED")
|
||||
}
|
||||
|
||||
func decrypted(with passphrase: String) throws -> CryptoContainer {
|
||||
let decryptedPEM = try TLSBox.decryptedPrivateKey(fromPEM: pem, passphrase: passphrase)
|
||||
return CryptoContainer(pem: decryptedPEM)
|
||||
|
|
|
@ -0,0 +1,464 @@
|
|||
//
|
||||
// OptionsBundle.swift
|
||||
// TunnelKit
|
||||
//
|
||||
// Created by Davide De Rosa on 4/3/19.
|
||||
// Copyright (c) 2019 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/keeshux
|
||||
//
|
||||
// This file is part of TunnelKit.
|
||||
//
|
||||
// TunnelKit is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// TunnelKit is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with TunnelKit. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyBeaver
|
||||
import __TunnelKitNative
|
||||
|
||||
private let log = SwiftyBeaver.self
|
||||
|
||||
public struct OptionsBundle {
|
||||
private struct Regex {
|
||||
|
||||
// shared
|
||||
|
||||
static let cipher = NSRegularExpression("^cipher +[^,\\s]+")
|
||||
|
||||
static let auth = NSRegularExpression("^auth +[\\w\\-]+")
|
||||
|
||||
static let compLZO = NSRegularExpression("^comp-lzo.*")
|
||||
|
||||
static let compress = NSRegularExpression("^compress.*")
|
||||
|
||||
static let ping = NSRegularExpression("^ping +\\d+")
|
||||
|
||||
static let renegSec = NSRegularExpression("^reneg-sec +\\d+")
|
||||
|
||||
static let blockBegin = NSRegularExpression("^<[\\w\\-]+>")
|
||||
|
||||
static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>")
|
||||
|
||||
static let keyDirection = NSRegularExpression("^key-direction +\\d")
|
||||
|
||||
static let gateway = NSRegularExpression("route-gateway [\\d\\.]+")
|
||||
|
||||
static let route = NSRegularExpression("route [\\d\\.]+( [\\d\\.]+){0,2}")
|
||||
|
||||
static let route6 = NSRegularExpression("route-ipv6 [\\da-fA-F:]+/\\d+( [\\da-fA-F:]+){0,2}")
|
||||
|
||||
static let dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+")
|
||||
|
||||
static let remoteRandom = NSRegularExpression("^remote-random")
|
||||
|
||||
// client
|
||||
|
||||
static let proto = NSRegularExpression("^proto +(udp6?|tcp6?)")
|
||||
|
||||
static let port = NSRegularExpression("^port +\\d+")
|
||||
|
||||
static let remote = NSRegularExpression("^remote +[^ ]+( +\\d+)?( +(udp6?|tcp6?))?")
|
||||
|
||||
static let eku = NSRegularExpression("^remote-cert-tls +server")
|
||||
|
||||
// server
|
||||
|
||||
static let topology = NSRegularExpression("topology (net30|p2p|subnet)")
|
||||
|
||||
static let ifconfig = NSRegularExpression("ifconfig [\\d\\.]+ [\\d\\.]+")
|
||||
|
||||
static let ifconfig6 = NSRegularExpression("ifconfig-ipv6 [\\da-fA-F:]+/\\d+ [\\da-fA-F:]+")
|
||||
|
||||
static let authToken = NSRegularExpression("auth-token [a-zA-Z0-9/=+]+")
|
||||
|
||||
static let peerId = NSRegularExpression("peer-id [0-9]+")
|
||||
|
||||
// unsupported
|
||||
|
||||
// static let fragment = NSRegularExpression("^fragment +\\d+")
|
||||
static let fragment = NSRegularExpression("^fragment")
|
||||
|
||||
static let proxy = NSRegularExpression("^\\w+-proxy")
|
||||
|
||||
static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ")
|
||||
|
||||
static let connection = NSRegularExpression("^<connection>")
|
||||
}
|
||||
|
||||
public let strippedLines: [String]?
|
||||
|
||||
public let warning: OptionsError?
|
||||
|
||||
//
|
||||
|
||||
public let hostname: String?
|
||||
|
||||
public let remotes: [(String, UInt16, SocketType)]
|
||||
|
||||
public let cipher: SessionProxy.Cipher?
|
||||
|
||||
public let digest: SessionProxy.Digest?
|
||||
|
||||
public let compressionFraming: SessionProxy.CompressionFraming?
|
||||
|
||||
public let compressionAlgorithm: SessionProxy.CompressionAlgorithm?
|
||||
|
||||
public let ca: CryptoContainer?
|
||||
|
||||
public let clientCertificate: CryptoContainer?
|
||||
|
||||
public let clientKey: CryptoContainer?
|
||||
|
||||
public let checksEKU: Bool
|
||||
|
||||
public let keepAliveSeconds: TimeInterval?
|
||||
|
||||
public let renegotiateAfterSeconds: TimeInterval?
|
||||
|
||||
public let tlsWrap: SessionProxy.TLSWrap?
|
||||
|
||||
public let dnsServers: [String]
|
||||
|
||||
public let randomizeEndpoint: Bool
|
||||
|
||||
public init(from lines: [String], returnsStripped: Bool = false) throws {
|
||||
var optStrippedLines: [String]? = returnsStripped ? [] : nil
|
||||
var optWarning: OptionsError?
|
||||
var unsupportedError: OptionsError?
|
||||
|
||||
var optHostname: String?
|
||||
var optDefaultProto: SocketType?
|
||||
var optDefaultPort: UInt16?
|
||||
var optRemotes: [(String, UInt16?, SocketType?)] = []
|
||||
var optCipher: SessionProxy.Cipher?
|
||||
var optDigest: SessionProxy.Digest?
|
||||
var optCompressionFraming: SessionProxy.CompressionFraming?
|
||||
var optCompressionAlgorithm: SessionProxy.CompressionAlgorithm?
|
||||
var optCA: CryptoContainer?
|
||||
var optClientCertificate: CryptoContainer?
|
||||
var optClientKey: CryptoContainer?
|
||||
var optChecksEKU: Bool?
|
||||
var optKeepAliveSeconds: TimeInterval?
|
||||
var optRenegotiateAfterSeconds: TimeInterval?
|
||||
var optKeyDirection: StaticKey.Direction?
|
||||
var optTLSKeyLines: [Substring]?
|
||||
var optTLSStrategy: SessionProxy.TLSWrap.Strategy?
|
||||
var optDnsServers: [String] = []
|
||||
var optRandomizeEndpoint: Bool?
|
||||
var currentBlockName: String?
|
||||
var currentBlock: [String] = []
|
||||
|
||||
log.verbose("Configuration file:")
|
||||
for line in lines {
|
||||
log.verbose(line)
|
||||
|
||||
var isHandled = false
|
||||
var strippedLine = line
|
||||
defer {
|
||||
if isHandled {
|
||||
optStrippedLines?.append(strippedLine)
|
||||
}
|
||||
}
|
||||
|
||||
// check blocks first
|
||||
Regex.connection.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = OptionsError.unsupportedConfiguration(option: "<connection> blocks")
|
||||
}
|
||||
|
||||
if unsupportedError == nil {
|
||||
if currentBlockName == nil {
|
||||
Regex.blockBegin.enumerateComponents(in: line) {
|
||||
isHandled = true
|
||||
let tag = $0.first!
|
||||
let from = tag.index(after: tag.startIndex)
|
||||
let to = tag.index(before: tag.endIndex)
|
||||
|
||||
currentBlockName = String(tag[from..<to])
|
||||
currentBlock = []
|
||||
}
|
||||
}
|
||||
Regex.blockEnd.enumerateComponents(in: line) {
|
||||
isHandled = true
|
||||
let tag = $0.first!
|
||||
let from = tag.index(tag.startIndex, offsetBy: 2)
|
||||
let to = tag.index(before: tag.endIndex)
|
||||
|
||||
let blockName = String(tag[from..<to])
|
||||
guard blockName == currentBlockName else {
|
||||
return
|
||||
}
|
||||
|
||||
// first is opening tag
|
||||
currentBlock.removeFirst()
|
||||
switch blockName {
|
||||
case "ca":
|
||||
optCA = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
||||
|
||||
case "cert":
|
||||
optClientCertificate = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
||||
|
||||
case "key":
|
||||
OptionsBundle.normalizeEncryptedPEMBlock(block: ¤tBlock)
|
||||
optClientKey = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
||||
|
||||
case "tls-auth":
|
||||
optTLSKeyLines = currentBlock.map { Substring($0) }
|
||||
optTLSStrategy = .auth
|
||||
|
||||
case "tls-crypt":
|
||||
optTLSKeyLines = currentBlock.map { Substring($0) }
|
||||
optTLSStrategy = .crypt
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
currentBlockName = nil
|
||||
currentBlock = []
|
||||
}
|
||||
}
|
||||
if let _ = currentBlockName {
|
||||
currentBlock.append(line)
|
||||
continue
|
||||
}
|
||||
|
||||
Regex.eku.enumerateComponents(in: line) { (_) in
|
||||
optChecksEKU = true
|
||||
}
|
||||
Regex.proto.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
optDefaultProto = SocketType(protoString: str)
|
||||
if optDefaultProto == nil {
|
||||
unsupportedError = OptionsError.unsupportedConfiguration(option: "proto \(str)")
|
||||
}
|
||||
}
|
||||
Regex.port.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
optDefaultPort = UInt16(str)
|
||||
}
|
||||
Regex.remote.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let hostname = $0.first else {
|
||||
return
|
||||
}
|
||||
var port: UInt16?
|
||||
var proto: SocketType?
|
||||
var strippedComponents = ["remote", "<hostname>"]
|
||||
if $0.count > 1 {
|
||||
port = UInt16($0[1])
|
||||
strippedComponents.append($0[1])
|
||||
}
|
||||
if $0.count > 2 {
|
||||
proto = SocketType(protoString: $0[2])
|
||||
strippedComponents.append($0[2])
|
||||
}
|
||||
optRemotes.append((hostname, port, proto))
|
||||
|
||||
// replace private data
|
||||
strippedLine = strippedComponents.joined(separator: " ")
|
||||
}
|
||||
Regex.cipher.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let rawValue = $0.first else {
|
||||
return
|
||||
}
|
||||
optCipher = SessionProxy.Cipher(rawValue: rawValue.uppercased())
|
||||
if optCipher == nil {
|
||||
unsupportedError = OptionsError.unsupportedConfiguration(option: "cipher \(rawValue)")
|
||||
}
|
||||
}
|
||||
Regex.auth.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let rawValue = $0.first else {
|
||||
return
|
||||
}
|
||||
optDigest = SessionProxy.Digest(rawValue: rawValue.uppercased())
|
||||
if optDigest == nil {
|
||||
unsupportedError = OptionsError.unsupportedConfiguration(option: "auth \(rawValue)")
|
||||
}
|
||||
}
|
||||
Regex.compLZO.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
optCompressionFraming = .compLZO
|
||||
|
||||
if !LZOIsSupported() {
|
||||
guard let arg = $0.first else {
|
||||
optWarning = optWarning ?? .unsupportedConfiguration(option: line)
|
||||
return
|
||||
}
|
||||
guard arg == "no" else {
|
||||
unsupportedError = .unsupportedConfiguration(option: line)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
let arg = $0.first
|
||||
optCompressionAlgorithm = (arg == "no") ? .disabled : .LZO
|
||||
}
|
||||
}
|
||||
Regex.compress.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
optCompressionFraming = .compress
|
||||
|
||||
if !LZOIsSupported() {
|
||||
guard $0.isEmpty else {
|
||||
unsupportedError = .unsupportedConfiguration(option: line)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if let arg = $0.first {
|
||||
optCompressionAlgorithm = (arg == "lzo") ? .LZO : .other
|
||||
} else {
|
||||
optCompressionAlgorithm = .disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
Regex.keyDirection.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first, let value = Int(arg) else {
|
||||
return
|
||||
}
|
||||
optKeyDirection = StaticKey.Direction(rawValue: value)
|
||||
}
|
||||
Regex.ping.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first else {
|
||||
return
|
||||
}
|
||||
optKeepAliveSeconds = TimeInterval(arg)
|
||||
}
|
||||
Regex.renegSec.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first else {
|
||||
return
|
||||
}
|
||||
optRenegotiateAfterSeconds = TimeInterval(arg)
|
||||
}
|
||||
Regex.dns.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard $0.count == 2 else {
|
||||
return
|
||||
}
|
||||
optDnsServers.append($0[1])
|
||||
}
|
||||
Regex.remoteRandom.enumerateComponents(in: line) { (_) in
|
||||
optRandomizeEndpoint = true
|
||||
}
|
||||
Regex.fragment.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = OptionsError.unsupportedConfiguration(option: "fragment")
|
||||
}
|
||||
Regex.proxy.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = OptionsError.unsupportedConfiguration(option: "proxy: \"\(line)\"")
|
||||
}
|
||||
Regex.externalFiles.enumerateComponents(in: line) { (_) in
|
||||
unsupportedError = OptionsError.unsupportedConfiguration(option: "external file: \"\(line)\"")
|
||||
}
|
||||
if line.contains("mtu") || line.contains("mssfix") {
|
||||
isHandled = true
|
||||
}
|
||||
|
||||
if let error = unsupportedError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
strippedLines = optStrippedLines
|
||||
warning = optWarning
|
||||
|
||||
optDefaultProto = optDefaultProto ?? .udp
|
||||
optDefaultPort = optDefaultPort ?? 1194
|
||||
if !optRemotes.isEmpty {
|
||||
hostname = optRemotes[0].0
|
||||
|
||||
var fullRemotes: [(String, UInt16, SocketType)] = []
|
||||
let hostname = optRemotes[0].0
|
||||
optRemotes.forEach {
|
||||
guard $0.0 == hostname else {
|
||||
return
|
||||
}
|
||||
guard let port = $0.1 ?? optDefaultPort else {
|
||||
return
|
||||
}
|
||||
guard let socketType = $0.2 ?? optDefaultProto else {
|
||||
return
|
||||
}
|
||||
fullRemotes.append((hostname, port, socketType))
|
||||
}
|
||||
remotes = fullRemotes
|
||||
} else {
|
||||
hostname = nil
|
||||
remotes = []
|
||||
}
|
||||
|
||||
cipher = optCipher
|
||||
digest = optDigest
|
||||
compressionFraming = optCompressionFraming
|
||||
compressionAlgorithm = optCompressionAlgorithm
|
||||
ca = optCA
|
||||
clientCertificate = optClientCertificate
|
||||
clientKey = optClientKey
|
||||
checksEKU = optChecksEKU ?? false
|
||||
keepAliveSeconds = optKeepAliveSeconds
|
||||
renegotiateAfterSeconds = optRenegotiateAfterSeconds
|
||||
dnsServers = optDnsServers
|
||||
randomizeEndpoint = optRandomizeEndpoint ?? false
|
||||
|
||||
if let keyLines = optTLSKeyLines, let strategy = optTLSStrategy {
|
||||
let optKey: StaticKey?
|
||||
switch strategy {
|
||||
case .auth:
|
||||
optKey = StaticKey(lines: keyLines, direction: optKeyDirection)
|
||||
|
||||
case .crypt:
|
||||
optKey = StaticKey(lines: keyLines, direction: .client)
|
||||
}
|
||||
if let key = optKey {
|
||||
tlsWrap = SessionProxy.TLSWrap(strategy: strategy, key: key)
|
||||
} else {
|
||||
tlsWrap = nil
|
||||
}
|
||||
} else {
|
||||
tlsWrap = nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalizeEncryptedPEMBlock(block: inout [String]) {
|
||||
// if block.count >= 1 && block[0].contains("ENCRYPTED") {
|
||||
// return true
|
||||
// }
|
||||
|
||||
// XXX: restore blank line after encryption header (easier than tweaking trimmedLines)
|
||||
if block.count >= 3 && block[1].contains("Proc-Type") {
|
||||
block.insert("", at: 3)
|
||||
// return true
|
||||
}
|
||||
// return false
|
||||
}
|
||||
}
|
||||
|
||||
private extension SocketType {
|
||||
init?(protoString: String) {
|
||||
var str = protoString
|
||||
if str.hasSuffix("6") {
|
||||
str.removeLast()
|
||||
}
|
||||
self.init(rawValue: str.uppercased())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// OptionsError.swift
|
||||
// TunnelKit
|
||||
//
|
||||
// Created by Davide De Rosa on 4/3/19.
|
||||
// Copyright (c) 2019 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/keeshux
|
||||
//
|
||||
// This file is part of TunnelKit.
|
||||
//
|
||||
// TunnelKit is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// TunnelKit is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with TunnelKit. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Error raised by the options parser, with details about the line that triggered it.
|
||||
public enum OptionsError: Error {
|
||||
|
||||
/// The file misses a required option.
|
||||
case missingConfiguration(option: String)
|
||||
|
||||
/// The file includes an unsupported option.
|
||||
case unsupportedConfiguration(option: String)
|
||||
|
||||
/// Passphrase required to decrypt private keys.
|
||||
case encryptionPassphrase
|
||||
|
||||
/// Encryption passphrase is incorrect or key is corrupt.
|
||||
case unableToDecrypt(error: Error)
|
||||
}
|
Loading…
Reference in New Issue