Move all source files to Sources/ and rename WireGuardKit targets
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class ButtonRow: NSView {
|
||||
let button: NSButton = {
|
||||
let button = NSButton()
|
||||
button.title = ""
|
||||
button.setButtonType(.momentaryPushIn)
|
||||
button.bezelStyle = .rounded
|
||||
return button
|
||||
}()
|
||||
|
||||
var buttonTitle: String {
|
||||
get { return button.title }
|
||||
set(value) { button.title = value }
|
||||
}
|
||||
|
||||
var isButtonEnabled: Bool {
|
||||
get { return button.isEnabled }
|
||||
set(value) { button.isEnabled = value }
|
||||
}
|
||||
|
||||
var buttonToolTip: String {
|
||||
get { return button.toolTip ?? "" }
|
||||
set(value) { button.toolTip = value }
|
||||
}
|
||||
|
||||
var onButtonClicked: (() -> Void)?
|
||||
var observationToken: AnyObject?
|
||||
|
||||
override var intrinsicContentSize: NSSize {
|
||||
return NSSize(width: NSView.noIntrinsicMetric, height: button.intrinsicContentSize.height)
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
button.target = self
|
||||
button.action = #selector(buttonClicked)
|
||||
|
||||
addSubview(button)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 155),
|
||||
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder decoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func buttonClicked() {
|
||||
onButtonClicked?()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
buttonTitle = ""
|
||||
buttonToolTip = ""
|
||||
onButtonClicked = nil
|
||||
observationToken = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
protocol ConfTextColorTheme {
|
||||
static var defaultColor: NSColor { get }
|
||||
static var colorMap: [UInt32: NSColor] { get }
|
||||
}
|
||||
|
||||
struct ConfTextAquaColorTheme: ConfTextColorTheme {
|
||||
static let defaultColor = NSColor(hex: "#000000")
|
||||
static let colorMap: [UInt32: NSColor] = [
|
||||
HighlightSection.rawValue: NSColor(hex: "#326D74"), // Class name in Xcode
|
||||
HighlightField.rawValue: NSColor(hex: "#9B2393"), // Keywords in Xcode
|
||||
HighlightPublicKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
|
||||
HighlightPrivateKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
|
||||
HighlightPresharedKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
|
||||
HighlightIP.rawValue: NSColor(hex: "#0E0EFF"), // URLs in Xcode
|
||||
HighlightHost.rawValue: NSColor(hex: "#0E0EFF"), // URLs in Xcode
|
||||
HighlightCidr.rawValue: NSColor(hex: "#815F03"), // Attributes in Xcode
|
||||
HighlightPort.rawValue: NSColor(hex: "#815F03"), // Attributes in Xcode
|
||||
HighlightMTU.rawValue: NSColor(hex: "#1C00CF"), // Numbers in Xcode
|
||||
HighlightKeepalive.rawValue: NSColor(hex: "#1C00CF"), // Numbers in Xcode
|
||||
HighlightComment.rawValue: NSColor(hex: "#536579"), // Comments in Xcode
|
||||
HighlightError.rawValue: NSColor(hex: "#C41A16") // Strings in Xcode
|
||||
]
|
||||
}
|
||||
|
||||
struct ConfTextDarkAquaColorTheme: ConfTextColorTheme {
|
||||
static let defaultColor = NSColor(hex: "#FFFFFF") // Plain text in Xcode
|
||||
static let colorMap: [UInt32: NSColor] = [
|
||||
HighlightSection.rawValue: NSColor(hex: "#91D462"), // Class name in Xcode
|
||||
HighlightField.rawValue: NSColor(hex: "#FC5FA3"), // Keywords in Xcode
|
||||
HighlightPublicKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
|
||||
HighlightPrivateKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
|
||||
HighlightPresharedKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
|
||||
HighlightIP.rawValue: NSColor(hex: "#53A5FB"), // URLs in Xcode
|
||||
HighlightHost.rawValue: NSColor(hex: "#53A5FB"), // URLs in Xcode
|
||||
HighlightCidr.rawValue: NSColor(hex: "#75B492"), // Attributes in Xcode
|
||||
HighlightPort.rawValue: NSColor(hex: "#75B492"), // Attributes in Xcode
|
||||
HighlightMTU.rawValue: NSColor(hex: "#9686F5"), // Numbers in Xcode
|
||||
HighlightKeepalive.rawValue: NSColor(hex: "#9686F5"), // Numbers in Xcode
|
||||
HighlightComment.rawValue: NSColor(hex: "#6C7986"), // Comments in Xcode
|
||||
HighlightError.rawValue: NSColor(hex: "#FF4C4C") // Strings in Xcode
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
private let fontSize: CGFloat = 15
|
||||
|
||||
class ConfTextStorage: NSTextStorage {
|
||||
let defaultFont = NSFontManager.shared.convertWeight(true, of: NSFont.systemFont(ofSize: fontSize))
|
||||
private let boldFont = NSFont.boldSystemFont(ofSize: fontSize)
|
||||
private lazy var italicFont = NSFontManager.shared.convert(defaultFont, toHaveTrait: .italicFontMask)
|
||||
|
||||
private var textColorTheme: ConfTextColorTheme.Type?
|
||||
|
||||
private let backingStore: NSMutableAttributedString
|
||||
private(set) var hasError = false
|
||||
private(set) var privateKeyString: String?
|
||||
|
||||
private(set) var hasOnePeer: Bool = false
|
||||
private(set) var lastOnePeerAllowedIPs = [String]()
|
||||
private(set) var lastOnePeerDNSServers = [String]()
|
||||
private(set) var lastOnePeerHasPublicKey = false
|
||||
|
||||
override init() {
|
||||
backingStore = NSMutableAttributedString(string: "")
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
|
||||
fatalError("init(pasteboardPropertyList:ofType:) has not been implemented")
|
||||
}
|
||||
|
||||
func nonColorAttributes(for highlightType: highlight_type) -> [NSAttributedString.Key: Any] {
|
||||
switch highlightType.rawValue {
|
||||
case HighlightSection.rawValue, HighlightField.rawValue:
|
||||
return [.font: boldFont]
|
||||
case HighlightPublicKey.rawValue, HighlightPrivateKey.rawValue, HighlightPresharedKey.rawValue,
|
||||
HighlightIP.rawValue, HighlightCidr.rawValue, HighlightHost.rawValue, HighlightPort.rawValue,
|
||||
HighlightMTU.rawValue, HighlightKeepalive.rawValue, HighlightDelimiter.rawValue:
|
||||
return [.font: defaultFont]
|
||||
case HighlightComment.rawValue:
|
||||
return [.font: italicFont]
|
||||
case HighlightError.rawValue:
|
||||
return [.font: defaultFont, .underlineStyle: 1]
|
||||
default:
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
|
||||
func updateAttributes(for textColorTheme: ConfTextColorTheme.Type) {
|
||||
self.textColorTheme = textColorTheme
|
||||
highlightSyntax()
|
||||
}
|
||||
|
||||
override var string: String {
|
||||
return backingStore.string
|
||||
}
|
||||
|
||||
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
|
||||
return backingStore.attributes(at: location, effectiveRange: range)
|
||||
}
|
||||
|
||||
override func replaceCharacters(in range: NSRange, with str: String) {
|
||||
beginEditing()
|
||||
backingStore.replaceCharacters(in: range, with: str)
|
||||
edited(.editedCharacters, range: range, changeInLength: str.count - range.length)
|
||||
endEditing()
|
||||
}
|
||||
|
||||
override func replaceCharacters(in range: NSRange, with attrString: NSAttributedString) {
|
||||
beginEditing()
|
||||
backingStore.replaceCharacters(in: range, with: attrString)
|
||||
edited(.editedCharacters, range: range, changeInLength: attrString.length - range.length)
|
||||
endEditing()
|
||||
}
|
||||
|
||||
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
|
||||
beginEditing()
|
||||
backingStore.setAttributes(attrs, range: range)
|
||||
edited(.editedAttributes, range: range, changeInLength: 0)
|
||||
endEditing()
|
||||
}
|
||||
|
||||
func resetLastPeer() {
|
||||
hasOnePeer = false
|
||||
lastOnePeerAllowedIPs = []
|
||||
lastOnePeerDNSServers = []
|
||||
lastOnePeerHasPublicKey = false
|
||||
}
|
||||
|
||||
func evaluateExcludePrivateIPs(highlightSpans: UnsafePointer<highlight_span>) {
|
||||
var spans = highlightSpans
|
||||
enum FieldType: String {
|
||||
case dns
|
||||
case allowedips
|
||||
}
|
||||
var fieldType: FieldType?
|
||||
resetLastPeer()
|
||||
while spans.pointee.type != HighlightEnd {
|
||||
let span = spans.pointee
|
||||
var substring = backingStore.attributedSubstring(from: NSRange(location: span.start, length: span.len)).string.lowercased()
|
||||
|
||||
if span.type == HighlightError {
|
||||
resetLastPeer()
|
||||
return
|
||||
} else if span.type == HighlightSection {
|
||||
if substring == "[peer]" {
|
||||
if hasOnePeer {
|
||||
resetLastPeer()
|
||||
return
|
||||
}
|
||||
hasOnePeer = true
|
||||
}
|
||||
} else if span.type == HighlightField {
|
||||
fieldType = FieldType(rawValue: substring)
|
||||
} else if span.type == HighlightIP && fieldType == .dns {
|
||||
lastOnePeerDNSServers.append(substring)
|
||||
} else if span.type == HighlightIP && fieldType == .allowedips {
|
||||
let next = spans.successor()
|
||||
let nextnext = next.successor()
|
||||
if next.pointee.type == HighlightDelimiter && nextnext.pointee.type == HighlightCidr {
|
||||
substring += backingStore.attributedSubstring(from: NSRange(location: next.pointee.start, length: next.pointee.len)).string +
|
||||
backingStore.attributedSubstring(from: NSRange(location: nextnext.pointee.start, length: nextnext.pointee.len)).string
|
||||
}
|
||||
lastOnePeerAllowedIPs.append(substring)
|
||||
} else if span.type == HighlightPublicKey {
|
||||
lastOnePeerHasPublicKey = true
|
||||
}
|
||||
spans = spans.successor()
|
||||
}
|
||||
}
|
||||
|
||||
func highlightSyntax() {
|
||||
guard let textColorTheme = textColorTheme else { return }
|
||||
hasError = false
|
||||
privateKeyString = nil
|
||||
|
||||
let fullTextRange = NSRange(location: 0, length: (backingStore.string as NSString).length)
|
||||
|
||||
backingStore.beginEditing()
|
||||
let defaultAttributes: [NSAttributedString.Key: Any] = [
|
||||
.foregroundColor: textColorTheme.defaultColor,
|
||||
.font: defaultFont
|
||||
]
|
||||
backingStore.setAttributes(defaultAttributes, range: fullTextRange)
|
||||
var spans = highlight_config(backingStore.string)!
|
||||
evaluateExcludePrivateIPs(highlightSpans: spans)
|
||||
|
||||
let spansStart = spans
|
||||
while spans.pointee.type != HighlightEnd {
|
||||
let span = spans.pointee
|
||||
|
||||
let range = NSRange(location: span.start, length: span.len)
|
||||
backingStore.setAttributes(nonColorAttributes(for: span.type), range: range)
|
||||
let color = textColorTheme.colorMap[span.type.rawValue, default: textColorTheme.defaultColor]
|
||||
backingStore.addAttribute(.foregroundColor, value: color, range: range)
|
||||
|
||||
if span.type == HighlightError {
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if span.type == HighlightPrivateKey {
|
||||
privateKeyString = backingStore.attributedSubstring(from: NSRange(location: span.start, length: span.len)).string
|
||||
}
|
||||
|
||||
spans = spans.successor()
|
||||
}
|
||||
backingStore.endEditing()
|
||||
free(spansStart)
|
||||
|
||||
beginEditing()
|
||||
edited(.editedAttributes, range: fullTextRange, changeInLength: 0)
|
||||
endEditing()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class ConfTextView: NSTextView {
|
||||
|
||||
private let confTextStorage = ConfTextStorage()
|
||||
|
||||
@objc dynamic var hasError: Bool = false
|
||||
@objc dynamic var privateKeyString: String?
|
||||
@objc dynamic var singlePeerAllowedIPs: [String]?
|
||||
|
||||
override var string: String {
|
||||
didSet {
|
||||
confTextStorage.highlightSyntax()
|
||||
updateConfigData()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
let textContainer = NSTextContainer()
|
||||
let layoutManager = NSLayoutManager()
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
confTextStorage.addLayoutManager(layoutManager)
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 60), textContainer: textContainer)
|
||||
font = confTextStorage.defaultFont
|
||||
allowsUndo = true
|
||||
isAutomaticSpellingCorrectionEnabled = false
|
||||
isAutomaticDataDetectionEnabled = false
|
||||
isAutomaticLinkDetectionEnabled = false
|
||||
isAutomaticTextCompletionEnabled = false
|
||||
isAutomaticTextReplacementEnabled = false
|
||||
isAutomaticDashSubstitutionEnabled = false
|
||||
isAutomaticQuoteSubstitutionEnabled = false
|
||||
updateTheme()
|
||||
delegate = self
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidChangeEffectiveAppearance() {
|
||||
updateTheme()
|
||||
}
|
||||
|
||||
private func updateTheme() {
|
||||
switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) ?? .aqua {
|
||||
case .darkAqua:
|
||||
confTextStorage.updateAttributes(for: ConfTextDarkAquaColorTheme.self)
|
||||
default:
|
||||
confTextStorage.updateAttributes(for: ConfTextAquaColorTheme.self)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateConfigData() {
|
||||
if hasError != confTextStorage.hasError {
|
||||
hasError = confTextStorage.hasError
|
||||
}
|
||||
if privateKeyString != confTextStorage.privateKeyString {
|
||||
privateKeyString = confTextStorage.privateKeyString
|
||||
}
|
||||
let hasSyntaxError = confTextStorage.hasError
|
||||
let hasSemanticError = confTextStorage.privateKeyString == nil || !confTextStorage.lastOnePeerHasPublicKey
|
||||
let updatedSinglePeerAllowedIPs = confTextStorage.hasOnePeer && !hasSyntaxError && !hasSemanticError ? confTextStorage.lastOnePeerAllowedIPs : nil
|
||||
if singlePeerAllowedIPs != updatedSinglePeerAllowedIPs {
|
||||
singlePeerAllowedIPs = updatedSinglePeerAllowedIPs
|
||||
}
|
||||
}
|
||||
|
||||
func setConfText(_ text: String) {
|
||||
let fullTextRange = NSRange(location: 0, length: (string as NSString).length)
|
||||
if shouldChangeText(in: fullTextRange, replacementString: text) {
|
||||
replaceCharacters(in: fullTextRange, with: text)
|
||||
didChangeText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConfTextView: NSTextViewDelegate {
|
||||
|
||||
func textDidChange(_ notification: Notification) {
|
||||
confTextStorage.highlightSyntax()
|
||||
updateConfigData()
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class DeleteTunnelsConfirmationAlert: NSAlert {
|
||||
var alertDeleteButton: NSButton?
|
||||
var alertCancelButton: NSButton?
|
||||
|
||||
var onDeleteClicked: ((_ completionHandler: @escaping () -> Void) -> Void)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
let alertDeleteButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleDelete"))
|
||||
alertDeleteButton.target = self
|
||||
alertDeleteButton.action = #selector(removeTunnelAlertDeleteClicked)
|
||||
self.alertDeleteButton = alertDeleteButton
|
||||
self.alertCancelButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleCancel"))
|
||||
}
|
||||
|
||||
@objc func removeTunnelAlertDeleteClicked() {
|
||||
alertDeleteButton?.title = tr("macDeleteTunnelConfirmationAlertButtonTitleDeleting")
|
||||
alertDeleteButton?.isEnabled = false
|
||||
alertCancelButton?.isEnabled = false
|
||||
if let onDeleteClicked = onDeleteClicked {
|
||||
onDeleteClicked { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.window.sheetParent?.endSheet(self.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func beginSheetModal(for sheetWindow: NSWindow) {
|
||||
beginSheetModal(for: sheetWindow) { _ in }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class EditableKeyValueRow: NSView {
|
||||
let keyLabel: NSTextField = {
|
||||
let keyLabel = NSTextField()
|
||||
keyLabel.isEditable = false
|
||||
keyLabel.isSelectable = false
|
||||
keyLabel.isBordered = false
|
||||
keyLabel.alignment = .right
|
||||
keyLabel.maximumNumberOfLines = 1
|
||||
keyLabel.lineBreakMode = .byTruncatingTail
|
||||
keyLabel.backgroundColor = .clear
|
||||
return keyLabel
|
||||
}()
|
||||
|
||||
let valueLabel: NSTextField = {
|
||||
let valueLabel = NSTextField()
|
||||
valueLabel.isSelectable = true
|
||||
valueLabel.maximumNumberOfLines = 1
|
||||
valueLabel.lineBreakMode = .byTruncatingTail
|
||||
return valueLabel
|
||||
}()
|
||||
|
||||
let valueImageView: NSImageView?
|
||||
|
||||
var key: String {
|
||||
get { return keyLabel.stringValue }
|
||||
set(value) { keyLabel.stringValue = value }
|
||||
}
|
||||
var value: String {
|
||||
get { return valueLabel.stringValue }
|
||||
set(value) { valueLabel.stringValue = value }
|
||||
}
|
||||
var isKeyInBold: Bool {
|
||||
get { return keyLabel.font == NSFont.boldSystemFont(ofSize: 0) }
|
||||
set(value) {
|
||||
if value {
|
||||
keyLabel.font = NSFont.boldSystemFont(ofSize: 0)
|
||||
} else {
|
||||
keyLabel.font = NSFont.systemFont(ofSize: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
var valueImage: NSImage? {
|
||||
get { return valueImageView?.image }
|
||||
set(value) { valueImageView?.image = value }
|
||||
}
|
||||
|
||||
var observationToken: AnyObject?
|
||||
|
||||
override var intrinsicContentSize: NSSize {
|
||||
let height = max(keyLabel.intrinsicContentSize.height, valueLabel.intrinsicContentSize.height)
|
||||
return NSSize(width: NSView.noIntrinsicMetric, height: height)
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(hasValueImage: false)
|
||||
}
|
||||
|
||||
fileprivate init(hasValueImage: Bool) {
|
||||
valueImageView = hasValueImage ? NSImageView() : nil
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
addSubview(keyLabel)
|
||||
addSubview(valueLabel)
|
||||
keyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
valueLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
keyLabel.firstBaselineAnchor.constraint(equalTo: valueLabel.firstBaselineAnchor),
|
||||
self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
|
||||
valueLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)
|
||||
])
|
||||
|
||||
let spacing: CGFloat = 5
|
||||
if let valueImageView = valueImageView {
|
||||
addSubview(valueImageView)
|
||||
valueImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
valueImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
valueImageView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing),
|
||||
valueLabel.leadingAnchor.constraint(equalTo: valueImageView.trailingAnchor)
|
||||
])
|
||||
} else {
|
||||
NSLayoutConstraint.activate([
|
||||
valueLabel.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing)
|
||||
])
|
||||
}
|
||||
|
||||
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
|
||||
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
|
||||
let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
|
||||
widthConstraint.priority = .defaultHigh + 1
|
||||
widthConstraint.isActive = true
|
||||
}
|
||||
|
||||
required init?(coder decoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
key = ""
|
||||
value = ""
|
||||
isKeyInBold = false
|
||||
observationToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
class KeyValueRow: EditableKeyValueRow {
|
||||
init() {
|
||||
super.init(hasValueImage: false)
|
||||
valueLabel.isEditable = false
|
||||
valueLabel.isBordered = false
|
||||
valueLabel.backgroundColor = .clear
|
||||
}
|
||||
|
||||
required init?(coder decoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class KeyValueImageRow: EditableKeyValueRow {
|
||||
init() {
|
||||
super.init(hasValueImage: true)
|
||||
valueLabel.isEditable = false
|
||||
valueLabel.isBordered = false
|
||||
valueLabel.backgroundColor = .clear
|
||||
}
|
||||
|
||||
required init?(coder decoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class LogViewCell: NSTableCellView {
|
||||
var text: String = "" {
|
||||
didSet { textField?.stringValue = text }
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let textField = NSTextField(wrappingLabelWithString: "")
|
||||
addSubview(textField)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
textField.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
textField.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
textField.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
textField.bottomAnchor.constraint(equalTo: self.bottomAnchor)
|
||||
])
|
||||
|
||||
self.textField = textField
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
textField?.stringValue = ""
|
||||
}
|
||||
}
|
||||
|
||||
class LogViewTimestampCell: LogViewCell {
|
||||
override init() {
|
||||
super.init()
|
||||
if let textField = textField {
|
||||
textField.maximumNumberOfLines = 1
|
||||
textField.lineBreakMode = .byClipping
|
||||
textField.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
||||
textField.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class LogViewMessageCell: LogViewCell {
|
||||
override init() {
|
||||
super.init()
|
||||
if let textField = textField {
|
||||
textField.maximumNumberOfLines = 0
|
||||
textField.lineBreakMode = .byWordWrapping
|
||||
textField.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
textField.setContentHuggingPriority(.required, for: .vertical)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
import CoreWLAN
|
||||
|
||||
class OnDemandControlsRow: NSView {
|
||||
let keyLabel: NSTextField = {
|
||||
let keyLabel = NSTextField()
|
||||
keyLabel.stringValue = tr("macFieldOnDemand")
|
||||
keyLabel.isEditable = false
|
||||
keyLabel.isSelectable = false
|
||||
keyLabel.isBordered = false
|
||||
keyLabel.alignment = .right
|
||||
keyLabel.maximumNumberOfLines = 1
|
||||
keyLabel.lineBreakMode = .byTruncatingTail
|
||||
keyLabel.backgroundColor = .clear
|
||||
return keyLabel
|
||||
}()
|
||||
|
||||
let onDemandEthernetCheckbox: NSButton = {
|
||||
let checkbox = NSButton()
|
||||
checkbox.title = tr("tunnelOnDemandEthernet")
|
||||
checkbox.setButtonType(.switch)
|
||||
checkbox.state = .off
|
||||
return checkbox
|
||||
}()
|
||||
|
||||
let onDemandWiFiCheckbox: NSButton = {
|
||||
let checkbox = NSButton()
|
||||
checkbox.title = tr("tunnelOnDemandWiFi")
|
||||
checkbox.setButtonType(.switch)
|
||||
checkbox.state = .off
|
||||
return checkbox
|
||||
}()
|
||||
|
||||
static let onDemandSSIDOptions: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
|
||||
.anySSID, .onlySpecificSSIDs, .exceptSpecificSSIDs
|
||||
]
|
||||
|
||||
let onDemandSSIDOptionsPopup = NSPopUpButton()
|
||||
|
||||
let onDemandSSIDsField: NSTokenField = {
|
||||
let tokenField = NSTokenField()
|
||||
tokenField.tokenizingCharacterSet = CharacterSet([])
|
||||
tokenField.tokenStyle = .squared
|
||||
NSLayoutConstraint.activate([
|
||||
tokenField.widthAnchor.constraint(greaterThanOrEqualToConstant: 180)
|
||||
])
|
||||
return tokenField
|
||||
}()
|
||||
|
||||
override var intrinsicContentSize: NSSize {
|
||||
let minHeight: CGFloat = 22
|
||||
let height = max(minHeight, keyLabel.intrinsicContentSize.height,
|
||||
onDemandEthernetCheckbox.intrinsicContentSize.height, onDemandWiFiCheckbox.intrinsicContentSize.height,
|
||||
onDemandSSIDOptionsPopup.intrinsicContentSize.height, onDemandSSIDsField.intrinsicContentSize.height)
|
||||
return NSSize(width: NSView.noIntrinsicMetric, height: height)
|
||||
}
|
||||
|
||||
var onDemandViewModel: ActivateOnDemandViewModel? {
|
||||
didSet { updateControls() }
|
||||
}
|
||||
|
||||
var currentSSIDs: [String]
|
||||
|
||||
init() {
|
||||
currentSSIDs = getCurrentSSIDs()
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
onDemandSSIDOptionsPopup.addItems(withTitles: OnDemandControlsRow.onDemandSSIDOptions.map { $0.localizedUIString })
|
||||
|
||||
let stackView = NSStackView()
|
||||
stackView.setViews([onDemandEthernetCheckbox, onDemandWiFiCheckbox, onDemandSSIDOptionsPopup, onDemandSSIDsField], in: .leading)
|
||||
stackView.orientation = .horizontal
|
||||
|
||||
addSubview(keyLabel)
|
||||
addSubview(stackView)
|
||||
keyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
|
||||
stackView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: 5),
|
||||
stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
|
||||
])
|
||||
|
||||
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
|
||||
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
|
||||
let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
|
||||
widthConstraint.priority = .defaultHigh + 1
|
||||
widthConstraint.isActive = true
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
onDemandEthernetCheckbox.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),
|
||||
onDemandWiFiCheckbox.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
|
||||
onDemandSSIDOptionsPopup.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
|
||||
onDemandSSIDsField.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor)
|
||||
])
|
||||
|
||||
onDemandSSIDsField.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
|
||||
onDemandEthernetCheckbox.target = self
|
||||
onDemandEthernetCheckbox.action = #selector(ethernetCheckboxToggled)
|
||||
|
||||
onDemandWiFiCheckbox.target = self
|
||||
onDemandWiFiCheckbox.action = #selector(wiFiCheckboxToggled)
|
||||
|
||||
onDemandSSIDOptionsPopup.target = self
|
||||
onDemandSSIDOptionsPopup.action = #selector(ssidOptionsPopupValueChanged)
|
||||
|
||||
onDemandSSIDsField.delegate = self
|
||||
|
||||
updateControls()
|
||||
}
|
||||
|
||||
required init?(coder decoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func saveToViewModel() {
|
||||
guard let onDemandViewModel = onDemandViewModel else { return }
|
||||
onDemandViewModel.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
|
||||
onDemandViewModel.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
|
||||
onDemandViewModel.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[onDemandSSIDOptionsPopup.indexOfSelectedItem]
|
||||
onDemandViewModel.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
|
||||
}
|
||||
|
||||
func updateControls() {
|
||||
guard let onDemandViewModel = onDemandViewModel else { return }
|
||||
onDemandEthernetCheckbox.state = onDemandViewModel.isNonWiFiInterfaceEnabled ? .on : .off
|
||||
onDemandWiFiCheckbox.state = onDemandViewModel.isWiFiInterfaceEnabled ? .on : .off
|
||||
let optionIndex = OnDemandControlsRow.onDemandSSIDOptions.firstIndex(of: onDemandViewModel.ssidOption)
|
||||
onDemandSSIDOptionsPopup.selectItem(at: optionIndex ?? 0)
|
||||
onDemandSSIDsField.objectValue = onDemandViewModel.selectedSSIDs
|
||||
onDemandSSIDOptionsPopup.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled
|
||||
onDemandSSIDsField.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled || onDemandViewModel.ssidOption == .anySSID
|
||||
}
|
||||
|
||||
@objc func ethernetCheckboxToggled() {
|
||||
onDemandViewModel?.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
|
||||
}
|
||||
|
||||
@objc func wiFiCheckboxToggled() {
|
||||
onDemandViewModel?.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
|
||||
updateControls()
|
||||
}
|
||||
|
||||
@objc func ssidOptionsPopupValueChanged() {
|
||||
let selectedIndex = onDemandSSIDOptionsPopup.indexOfSelectedItem
|
||||
onDemandViewModel?.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[selectedIndex]
|
||||
onDemandViewModel?.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
|
||||
updateControls()
|
||||
if !onDemandSSIDsField.isHidden {
|
||||
onDemandSSIDsField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OnDemandControlsRow: NSTokenFieldDelegate {
|
||||
func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
|
||||
return currentSSIDs.filter { $0.hasPrefix(substring) }
|
||||
}
|
||||
}
|
||||
|
||||
private func getCurrentSSIDs() -> [String] {
|
||||
return CWWiFiClient.shared().interfaces()?.compactMap { $0.ssid() } ?? []
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class TunnelListRow: NSView {
|
||||
var tunnel: TunnelContainer? {
|
||||
didSet(value) {
|
||||
// Bind to the tunnel's name
|
||||
nameLabel.stringValue = tunnel?.name ?? ""
|
||||
nameObservationToken = tunnel?.observe(\TunnelContainer.name) { [weak self] tunnel, _ in
|
||||
self?.nameLabel.stringValue = tunnel.name
|
||||
}
|
||||
// Bind to the tunnel's status
|
||||
statusImageView.image = TunnelListRow.image(for: tunnel?.status)
|
||||
statusObservationToken = tunnel?.observe(\TunnelContainer.status) { [weak self] tunnel, _ in
|
||||
self?.statusImageView.image = TunnelListRow.image(for: tunnel.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nameLabel: NSTextField = {
|
||||
let nameLabel = NSTextField()
|
||||
nameLabel.isEditable = false
|
||||
nameLabel.isSelectable = false
|
||||
nameLabel.isBordered = false
|
||||
nameLabel.maximumNumberOfLines = 1
|
||||
nameLabel.lineBreakMode = .byTruncatingTail
|
||||
return nameLabel
|
||||
}()
|
||||
|
||||
let statusImageView = NSImageView()
|
||||
|
||||
private var statusObservationToken: AnyObject?
|
||||
private var nameObservationToken: AnyObject?
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
addSubview(statusImageView)
|
||||
addSubview(nameLabel)
|
||||
statusImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.backgroundColor = .clear
|
||||
NSLayoutConstraint.activate([
|
||||
self.leadingAnchor.constraint(equalTo: statusImageView.leadingAnchor),
|
||||
statusImageView.trailingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
|
||||
statusImageView.widthAnchor.constraint(equalToConstant: 20),
|
||||
nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
statusImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder decoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
static func image(for status: TunnelStatus?) -> NSImage? {
|
||||
guard let status = status else { return nil }
|
||||
switch status {
|
||||
case .active, .restarting, .reasserting:
|
||||
return NSImage(named: NSImage.statusAvailableName)
|
||||
case .activating, .waiting, .deactivating:
|
||||
return NSImage(named: NSImage.statusPartiallyAvailableName)
|
||||
case .inactive:
|
||||
return NSImage(named: NSImage.statusNoneName)
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
nameLabel.stringValue = ""
|
||||
statusImageView.image = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include "highlighter.h"
|
||||
|
||||
typedef struct {
|
||||
const char *s;
|
||||
size_t len;
|
||||
} string_span_t;
|
||||
|
||||
static bool is_decimal(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
static bool is_hexadecimal(char c)
|
||||
{
|
||||
return is_decimal(c) || ((c | 32) >= 'a' && (c | 32) <= 'f');
|
||||
}
|
||||
|
||||
static bool is_alphabet(char c)
|
||||
{
|
||||
return (c | 32) >= 'a' && (c | 32) <= 'z';
|
||||
}
|
||||
|
||||
static bool is_same(string_span_t s, const char *c)
|
||||
{
|
||||
size_t len = strlen(c);
|
||||
|
||||
if (len != s.len)
|
||||
return false;
|
||||
return !memcmp(s.s, c, len);
|
||||
}
|
||||
|
||||
static bool is_caseless_same(string_span_t s, const char *c)
|
||||
{
|
||||
size_t len = strlen(c);
|
||||
|
||||
if (len != s.len)
|
||||
return false;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
char a = c[i], b = s.s[i];
|
||||
if ((unsigned)a - 'a' < 26)
|
||||
a &= 95;
|
||||
if ((unsigned)b - 'a' < 26)
|
||||
b &= 95;
|
||||
if (a != b)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_valid_key(string_span_t s)
|
||||
{
|
||||
if (s.len != 44 || s.s[43] != '=')
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < 42; ++i) {
|
||||
if (!is_decimal(s.s[i]) && !is_alphabet(s.s[i]) &&
|
||||
s.s[i] != '/' && s.s[i] != '+')
|
||||
return false;
|
||||
}
|
||||
switch (s.s[42]) {
|
||||
case 'A':
|
||||
case 'E':
|
||||
case 'I':
|
||||
case 'M':
|
||||
case 'Q':
|
||||
case 'U':
|
||||
case 'Y':
|
||||
case 'c':
|
||||
case 'g':
|
||||
case 'k':
|
||||
case 'o':
|
||||
case 's':
|
||||
case 'w':
|
||||
case '4':
|
||||
case '8':
|
||||
case '0':
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_valid_hostname(string_span_t s)
|
||||
{
|
||||
size_t num_digit = 0, num_entity = s.len;
|
||||
|
||||
if (s.len > 63 || !s.len)
|
||||
return false;
|
||||
if (s.s[0] == '-' || s.s[s.len - 1] == '-')
|
||||
return false;
|
||||
if (s.s[0] == '.' || s.s[s.len - 1] == '.')
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < s.len; ++i) {
|
||||
if (is_decimal(s.s[i])) {
|
||||
++num_digit;
|
||||
continue;
|
||||
}
|
||||
if (s.s[i] == '.') {
|
||||
--num_entity;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_alphabet(s.s[i]) && s.s[i] != '-')
|
||||
return false;
|
||||
|
||||
if (i && s.s[i] == '.' && s.s[i - 1] == '.')
|
||||
return false;
|
||||
}
|
||||
return num_digit != num_entity;
|
||||
}
|
||||
|
||||
static bool is_valid_ipv4(string_span_t s)
|
||||
{
|
||||
for (size_t j, i = 0, pos = 0; i < 4 && pos < s.len; ++i) {
|
||||
uint32_t val = 0;
|
||||
|
||||
for (j = 0; j < 3 && pos + j < s.len && is_decimal(s.s[pos + j]); ++j)
|
||||
val = 10 * val + s.s[pos + j] - '0';
|
||||
if (j == 0 || (j > 1 && s.s[pos] == '0') || val > 255)
|
||||
return false;
|
||||
if (pos + j == s.len && i == 3)
|
||||
return true;
|
||||
if (s.s[pos + j] != '.')
|
||||
return false;
|
||||
pos += j + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_valid_ipv6(string_span_t s)
|
||||
{
|
||||
size_t pos = 0;
|
||||
bool seen_colon = false;
|
||||
|
||||
if (s.len < 2)
|
||||
return false;
|
||||
if (s.s[pos] == ':' && s.s[++pos] != ':')
|
||||
return false;
|
||||
if (s.s[s.len - 1] == ':' && s.s[s.len - 2] != ':')
|
||||
return false;
|
||||
|
||||
for (size_t j, i = 0; pos < s.len; ++i) {
|
||||
if (s.s[pos] == ':' && !seen_colon) {
|
||||
seen_colon = true;
|
||||
if (++pos == s.len)
|
||||
break;
|
||||
if (i == 7)
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
for (j = 0; j < 4 && pos + j < s.len && is_hexadecimal(s.s[pos + j]); ++j);
|
||||
if (j == 0)
|
||||
return false;
|
||||
if (pos + j == s.len && (seen_colon || i == 7))
|
||||
break;
|
||||
if (i == 7)
|
||||
return false;
|
||||
if (s.s[pos + j] != ':') {
|
||||
if (s.s[pos + j] != '.' || (i < 6 && !seen_colon))
|
||||
return false;
|
||||
return is_valid_ipv4((string_span_t){ s.s + pos, s.len - pos });
|
||||
}
|
||||
pos += j + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_valid_uint(string_span_t s, bool support_hex, uint64_t min, uint64_t max)
|
||||
{
|
||||
uint64_t val = 0;
|
||||
|
||||
/* Bound this around 32 bits, so that we don't have to write overflow logic. */
|
||||
if (s.len > 10 || !s.len)
|
||||
return false;
|
||||
|
||||
if (support_hex && s.len > 2 && s.s[0] == '0' && s.s[1] == 'x') {
|
||||
for (size_t i = 2; i < s.len; ++i) {
|
||||
if ((unsigned)s.s[i] - '0' < 10)
|
||||
val = 16 * val + (s.s[i] - '0');
|
||||
else if (((unsigned)s.s[i] | 32) - 'a' < 6)
|
||||
val = 16 * val + (s.s[i] | 32) - 'a' + 10;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < s.len; ++i) {
|
||||
if (!is_decimal(s.s[i]))
|
||||
return false;
|
||||
val = 10 * val + s.s[i] - '0';
|
||||
}
|
||||
}
|
||||
return val <= max && val >= min;
|
||||
}
|
||||
|
||||
static bool is_valid_port(string_span_t s)
|
||||
{
|
||||
return is_valid_uint(s, false, 0, 65535);
|
||||
}
|
||||
|
||||
static bool is_valid_mtu(string_span_t s)
|
||||
{
|
||||
return is_valid_uint(s, false, 576, 65535);
|
||||
}
|
||||
|
||||
static bool is_valid_persistentkeepalive(string_span_t s)
|
||||
{
|
||||
if (is_same(s, "off"))
|
||||
return true;
|
||||
return is_valid_uint(s, false, 0, 65535);
|
||||
}
|
||||
|
||||
#ifndef MOBILE_WGQUICK_SUBSET
|
||||
|
||||
static bool is_valid_fwmark(string_span_t s)
|
||||
{
|
||||
if (is_same(s, "off"))
|
||||
return true;
|
||||
return is_valid_uint(s, true, 0, 4294967295);
|
||||
}
|
||||
|
||||
static bool is_valid_table(string_span_t s)
|
||||
{
|
||||
if (is_same(s, "auto"))
|
||||
return true;
|
||||
if (is_same(s, "off"))
|
||||
return true;
|
||||
/* This pretty much invalidates the other checks, but rt_names.c's
|
||||
* fread_id_name does no validation aside from this. */
|
||||
if (s.len < 512)
|
||||
return true;
|
||||
return is_valid_uint(s, false, 0, 4294967295);
|
||||
}
|
||||
|
||||
static bool is_valid_saveconfig(string_span_t s)
|
||||
{
|
||||
return is_same(s, "true") || is_same(s, "false");
|
||||
}
|
||||
|
||||
static bool is_valid_prepostupdown(string_span_t s)
|
||||
{
|
||||
/* It's probably not worthwhile to try to validate a bash expression.
|
||||
* So instead we just demand non-zero length. */
|
||||
return s.len;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool is_valid_scope(string_span_t s)
|
||||
{
|
||||
if (s.len > 64 || !s.len)
|
||||
return false;
|
||||
for (size_t i = 0; i < s.len; ++i) {
|
||||
if (!is_alphabet(s.s[i]) && !is_decimal(s.s[i]) &&
|
||||
s.s[i] != '_' && s.s[i] != '=' && s.s[i] != '+' &&
|
||||
s.s[i] != '.' && s.s[i] != '-')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_valid_endpoint(string_span_t s)
|
||||
{
|
||||
|
||||
if (!s.len)
|
||||
return false;
|
||||
|
||||
if (s.s[0] == '[') {
|
||||
bool seen_scope = false;
|
||||
string_span_t hostspan = { s.s + 1, 0 };
|
||||
|
||||
for (size_t i = 1; i < s.len; ++i) {
|
||||
if (s.s[i] == '%') {
|
||||
if (seen_scope)
|
||||
return false;
|
||||
seen_scope = true;
|
||||
if (!is_valid_ipv6(hostspan))
|
||||
return false;
|
||||
hostspan = (string_span_t){ s.s + i + 1, 0 };
|
||||
} else if (s.s[i] == ']') {
|
||||
if (seen_scope) {
|
||||
if (!is_valid_scope(hostspan))
|
||||
return false;
|
||||
} else if (!is_valid_ipv6(hostspan)) {
|
||||
return false;
|
||||
}
|
||||
if (i == s.len - 1 || s.s[i + 1] != ':')
|
||||
return false;
|
||||
return is_valid_port((string_span_t){ s.s + i + 2, s.len - i - 2 });
|
||||
} else {
|
||||
++hostspan.len;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < s.len; ++i) {
|
||||
if (s.s[i] == ':') {
|
||||
string_span_t host = { s.s, i }, port = { s.s + i + 1, s.len - i - 1};
|
||||
return is_valid_port(port) && (is_valid_ipv4(host) || is_valid_hostname(host));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_valid_network(string_span_t s)
|
||||
{
|
||||
for (size_t i = 0; i < s.len; ++i) {
|
||||
if (s.s[i] == '/') {
|
||||
string_span_t ip = { s.s, i }, cidr = { s.s + i + 1, s.len - i - 1};
|
||||
uint16_t cidrval = 0;
|
||||
|
||||
if (cidr.len > 3 || !cidr.len)
|
||||
return false;
|
||||
|
||||
for (size_t j = 0; j < cidr.len; ++j) {
|
||||
if (!is_decimal(cidr.s[j]))
|
||||
return false;
|
||||
cidrval = 10 * cidrval + cidr.s[j] - '0';
|
||||
}
|
||||
if (is_valid_ipv4(ip))
|
||||
return cidrval <= 32;
|
||||
else if (is_valid_ipv6(ip))
|
||||
return cidrval <= 128;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return is_valid_ipv4(s) || is_valid_ipv6(s);
|
||||
}
|
||||
|
||||
static bool is_valid_dns(string_span_t s)
|
||||
{
|
||||
return is_valid_ipv4(s) || is_valid_ipv6(s);
|
||||
}
|
||||
|
||||
enum field {
|
||||
InterfaceSection,
|
||||
PrivateKey,
|
||||
ListenPort,
|
||||
Address,
|
||||
DNS,
|
||||
MTU,
|
||||
#ifndef MOBILE_WGQUICK_SUBSET
|
||||
FwMark,
|
||||
Table,
|
||||
PreUp, PostUp, PreDown, PostDown,
|
||||
SaveConfig,
|
||||
#endif
|
||||
|
||||
PeerSection,
|
||||
PublicKey,
|
||||
PresharedKey,
|
||||
AllowedIPs,
|
||||
Endpoint,
|
||||
PersistentKeepalive,
|
||||
|
||||
Invalid
|
||||
};
|
||||
|
||||
static enum field section_for_field(enum field t)
|
||||
{
|
||||
if (t > InterfaceSection && t < PeerSection)
|
||||
return InterfaceSection;
|
||||
if (t > PeerSection && t < Invalid)
|
||||
return PeerSection;
|
||||
return Invalid;
|
||||
}
|
||||
|
||||
static enum field get_field(string_span_t s)
|
||||
{
|
||||
#define check_enum(t) do { if (is_caseless_same(s, #t)) return t; } while (0)
|
||||
check_enum(PrivateKey);
|
||||
check_enum(ListenPort);
|
||||
check_enum(Address);
|
||||
check_enum(DNS);
|
||||
check_enum(MTU);
|
||||
check_enum(PublicKey);
|
||||
check_enum(PresharedKey);
|
||||
check_enum(AllowedIPs);
|
||||
check_enum(Endpoint);
|
||||
check_enum(PersistentKeepalive);
|
||||
#ifndef MOBILE_WGQUICK_SUBSET
|
||||
check_enum(FwMark);
|
||||
check_enum(Table);
|
||||
check_enum(PreUp);
|
||||
check_enum(PostUp);
|
||||
check_enum(PreDown);
|
||||
check_enum(PostDown);
|
||||
check_enum(SaveConfig);
|
||||
#endif
|
||||
return Invalid;
|
||||
#undef check_enum
|
||||
}
|
||||
|
||||
static enum field get_sectiontype(string_span_t s)
|
||||
{
|
||||
if (is_caseless_same(s, "[Peer]"))
|
||||
return PeerSection;
|
||||
if (is_caseless_same(s, "[Interface]"))
|
||||
return InterfaceSection;
|
||||
return Invalid;
|
||||
}
|
||||
|
||||
struct highlight_span_array {
|
||||
size_t len, capacity;
|
||||
struct highlight_span *spans;
|
||||
};
|
||||
|
||||
/* A useful OpenBSD-ism. */
|
||||
static void *realloc_array(void *optr, size_t nmemb, size_t size)
|
||||
{
|
||||
if ((nmemb >= (size_t)1 << (sizeof(size_t) * 4) ||
|
||||
size >= (size_t)1 << (sizeof(size_t) * 4)) &&
|
||||
nmemb > 0 && SIZE_MAX / nmemb < size) {
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
return realloc(optr, size * nmemb);
|
||||
}
|
||||
|
||||
static bool append_highlight_span(struct highlight_span_array *a, const char *o, string_span_t s, enum highlight_type t)
|
||||
{
|
||||
if (!s.len)
|
||||
return true;
|
||||
if (a->len >= a->capacity) {
|
||||
struct highlight_span *resized;
|
||||
|
||||
a->capacity = a->capacity ? a->capacity * 2 : 64;
|
||||
resized = realloc_array(a->spans, a->capacity, sizeof(*resized));
|
||||
if (!resized) {
|
||||
free(a->spans);
|
||||
memset(a, 0, sizeof(*a));
|
||||
return false;
|
||||
}
|
||||
a->spans = resized;
|
||||
}
|
||||
a->spans[a->len++] = (struct highlight_span){ t, s.s - o, s.len };
|
||||
return true;
|
||||
}
|
||||
|
||||
static void highlight_multivalue_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
|
||||
{
|
||||
switch (section) {
|
||||
case DNS:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_dns(s) ? HighlightIP : HighlightError);
|
||||
break;
|
||||
case Address:
|
||||
case AllowedIPs: {
|
||||
size_t slash;
|
||||
|
||||
if (!is_valid_network(s)) {
|
||||
append_highlight_span(ret, parent.s, s, HighlightError);
|
||||
break;
|
||||
}
|
||||
for (slash = 0; slash < s.len; ++slash) {
|
||||
if (s.s[slash] == '/')
|
||||
break;
|
||||
}
|
||||
if (slash == s.len) {
|
||||
append_highlight_span(ret, parent.s, s, HighlightIP);
|
||||
} else {
|
||||
append_highlight_span(ret, parent.s, (string_span_t){ s.s, slash }, HighlightIP);
|
||||
append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash, 1 }, HighlightDelimiter);
|
||||
append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash + 1, s.len - slash - 1 }, HighlightCidr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
append_highlight_span(ret, parent.s, s, HighlightError);
|
||||
}
|
||||
}
|
||||
|
||||
static void highlight_multivalue(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
|
||||
{
|
||||
string_span_t current_span = { s.s, 0 };
|
||||
size_t len_at_last_space = 0;
|
||||
|
||||
for (size_t i = 0; i < s.len; ++i) {
|
||||
if (s.s[i] == ',') {
|
||||
current_span.len = len_at_last_space;
|
||||
highlight_multivalue_value(ret, parent, current_span, section);
|
||||
append_highlight_span(ret, parent.s, (string_span_t){ s.s + i, 1 }, HighlightDelimiter);
|
||||
len_at_last_space = 0;
|
||||
current_span = (string_span_t){ s.s + i + 1, 0 };
|
||||
} else if (s.s[i] == ' ' || s.s[i] == '\t') {
|
||||
if (&s.s[i] == current_span.s && !current_span.len)
|
||||
++current_span.s;
|
||||
else
|
||||
++current_span.len;
|
||||
} else {
|
||||
len_at_last_space = ++current_span.len;
|
||||
}
|
||||
}
|
||||
current_span.len = len_at_last_space;
|
||||
if (current_span.len)
|
||||
highlight_multivalue_value(ret, parent, current_span, section);
|
||||
else if (ret->spans[ret->len - 1].type == HighlightDelimiter)
|
||||
ret->spans[ret->len - 1].type = HighlightError;
|
||||
}
|
||||
|
||||
static void highlight_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
|
||||
{
|
||||
switch (section) {
|
||||
case PrivateKey:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPrivateKey : HighlightError);
|
||||
break;
|
||||
case PublicKey:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPublicKey : HighlightError);
|
||||
break;
|
||||
case PresharedKey:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPresharedKey : HighlightError);
|
||||
break;
|
||||
case MTU:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_mtu(s) ? HighlightMTU : HighlightError);
|
||||
break;
|
||||
#ifndef MOBILE_WGQUICK_SUBSET
|
||||
case SaveConfig:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? HighlightSaveConfig : HighlightError);
|
||||
break;
|
||||
case FwMark:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_fwmark(s) ? HighlightFwMark : HighlightError);
|
||||
break;
|
||||
case Table:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_table(s) ? HighlightTable : HighlightError);
|
||||
break;
|
||||
case PreUp:
|
||||
case PostUp:
|
||||
case PreDown:
|
||||
case PostDown:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_prepostupdown(s) ? HighlightCmd : HighlightError);
|
||||
break;
|
||||
#endif
|
||||
case ListenPort:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_port(s) ? HighlightPort : HighlightError);
|
||||
break;
|
||||
case PersistentKeepalive:
|
||||
append_highlight_span(ret, parent.s, s, is_valid_persistentkeepalive(s) ? HighlightKeepalive : HighlightError);
|
||||
break;
|
||||
case Endpoint: {
|
||||
size_t colon;
|
||||
|
||||
if (!is_valid_endpoint(s)) {
|
||||
append_highlight_span(ret, parent.s, s, HighlightError);
|
||||
break;
|
||||
}
|
||||
for (colon = s.len; colon --> 0;) {
|
||||
if (s.s[colon] == ':')
|
||||
break;
|
||||
}
|
||||
append_highlight_span(ret, parent.s, (string_span_t){ s.s, colon }, HighlightHost);
|
||||
append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon, 1 }, HighlightDelimiter);
|
||||
append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon + 1, s.len - colon - 1 }, HighlightPort);
|
||||
break;
|
||||
}
|
||||
case Address:
|
||||
case DNS:
|
||||
case AllowedIPs:
|
||||
highlight_multivalue(ret, parent, s, section);
|
||||
break;
|
||||
default:
|
||||
append_highlight_span(ret, parent.s, s, HighlightError);
|
||||
}
|
||||
}
|
||||
|
||||
struct highlight_span *highlight_config(const char *config)
|
||||
{
|
||||
struct highlight_span_array ret = { 0 };
|
||||
const string_span_t s = { config, strlen(config) };
|
||||
string_span_t current_span = { s.s, 0 };
|
||||
enum field current_section = Invalid, current_field = Invalid;
|
||||
enum { OnNone, OnKey, OnValue, OnComment, OnSection } state = OnNone;
|
||||
size_t len_at_last_space = 0, equals_location = 0;
|
||||
|
||||
for (size_t i = 0; i <= s.len; ++i) {
|
||||
if (i == s.len || s.s[i] == '\n' || (state != OnComment && s.s[i] == '#')) {
|
||||
if (state == OnKey) {
|
||||
current_span.len = len_at_last_space;
|
||||
append_highlight_span(&ret, s.s, current_span, HighlightError);
|
||||
} else if (state == OnValue) {
|
||||
if (current_span.len) {
|
||||
append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightDelimiter);
|
||||
current_span.len = len_at_last_space;
|
||||
highlight_value(&ret, s, current_span, current_field);
|
||||
} else {
|
||||
append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightError);
|
||||
}
|
||||
} else if (state == OnSection) {
|
||||
current_span.len = len_at_last_space;
|
||||
current_section = get_sectiontype(current_span);
|
||||
append_highlight_span(&ret, s.s, current_span, current_section == Invalid ? HighlightError : HighlightSection);
|
||||
} else if (state == OnComment) {
|
||||
append_highlight_span(&ret, s.s, current_span, HighlightComment);
|
||||
}
|
||||
if (i == s.len)
|
||||
break;
|
||||
len_at_last_space = 0;
|
||||
current_field = Invalid;
|
||||
if (s.s[i] == '#') {
|
||||
current_span = (string_span_t){ s.s + i, 1 };
|
||||
state = OnComment;
|
||||
} else {
|
||||
current_span = (string_span_t){ s.s + i + 1, 0 };
|
||||
state = OnNone;
|
||||
}
|
||||
} else if (state == OnComment) {
|
||||
++current_span.len;
|
||||
} else if (s.s[i] == ' ' || s.s[i] == '\t') {
|
||||
if (&s.s[i] == current_span.s && !current_span.len)
|
||||
++current_span.s;
|
||||
else
|
||||
++current_span.len;
|
||||
} else if (s.s[i] == '=' && state == OnKey) {
|
||||
current_span.len = len_at_last_space;
|
||||
current_field = get_field(current_span);
|
||||
enum field section = section_for_field(current_field);
|
||||
if (section == Invalid || current_field == Invalid || section != current_section)
|
||||
append_highlight_span(&ret, s.s, current_span, HighlightError);
|
||||
else
|
||||
append_highlight_span(&ret, s.s, current_span, HighlightField);
|
||||
equals_location = i;
|
||||
current_span = (string_span_t){ s.s + i + 1, 0 };
|
||||
state = OnValue;
|
||||
} else {
|
||||
if (state == OnNone)
|
||||
state = s.s[i] == '[' ? OnSection : OnKey;
|
||||
len_at_last_space = ++current_span.len;
|
||||
}
|
||||
}
|
||||
|
||||
append_highlight_span(&ret, s.s, (string_span_t){ s.s, -1 }, HighlightEnd);
|
||||
return ret.spans;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <sys/types.h>
|
||||
#define MOBILE_WGQUICK_SUBSET
|
||||
|
||||
enum highlight_type {
|
||||
HighlightSection,
|
||||
HighlightField,
|
||||
HighlightPrivateKey,
|
||||
HighlightPublicKey,
|
||||
HighlightPresharedKey,
|
||||
HighlightIP,
|
||||
HighlightCidr,
|
||||
HighlightHost,
|
||||
HighlightPort,
|
||||
HighlightMTU,
|
||||
HighlightKeepalive,
|
||||
HighlightComment,
|
||||
HighlightDelimiter,
|
||||
#ifndef MOBILE_WGQUICK_SUBSET
|
||||
HighlightTable,
|
||||
HighlightFwMark,
|
||||
HighlightSaveConfig,
|
||||
HighlightCmd,
|
||||
#endif
|
||||
HighlightError,
|
||||
HighlightEnd
|
||||
};
|
||||
|
||||
struct highlight_span {
|
||||
enum highlight_type type;
|
||||
size_t start, len;
|
||||
};
|
||||
|
||||
struct highlight_span *highlight_config(const char *config);
|
||||
Reference in New Issue
Block a user