-
Notifications
You must be signed in to change notification settings - Fork 3
chore: add dylib downloader and validator #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+1,059
−387
Merged
Changes from 10 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
3563eb4
chore: add dylib downloader and validator
ethanndickson 48b35f0
fmt
ethanndickson f602fa1
http test server -> mocked requests
ethanndickson 8b60368
undo entitlements change
ethanndickson 6398b00
fixup tunnel
ethanndickson fb36b59
fixup tunnel
ethanndickson 9e1a956
fixup tunnel
ethanndickson e6208e8
fixup downloader
ethanndickson ce4f0da
flip recv and send
ethanndickson 8453170
remove hardcoded bundle identifiers
ethanndickson 5fb8cdd
review
ethanndickson bfb98f0
200 resp only
ethanndickson f48b106
sendable downloader
ethanndickson ae5b3e2
receiver logger name
ethanndickson 4eac98b
localizederror -> error
ethanndickson 844df27
download and validator to functions
ethanndickson 4d0b3da
improve tunnelhandle
ethanndickson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
11 changes: 10 additions & 1 deletion
11
...Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import NetworkExtension | ||
import os | ||
import VPNLib | ||
|
||
actor Manager { | ||
let ptp: PacketTunnelProvider | ||
let downloader: Downloader | ||
|
||
var tunnelHandle: TunnelHandle? | ||
var speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>? | ||
// TODO: XPC Speaker | ||
|
||
private let dest = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) | ||
.first!.appending(path: "coder-vpn.dylib") | ||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "manager") | ||
|
||
init(with: PacketTunnelProvider) { | ||
ptp = with | ||
downloader = Downloader() | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import Foundation | ||
import os | ||
|
||
let startSymbol = "OpenTunnel" | ||
|
||
actor TunnelHandle { | ||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "tunnel-handle") | ||
|
||
private let tunnelWritePipe: Pipe | ||
private let tunnelReadPipe: Pipe | ||
private let dylibHandle: UnsafeMutableRawPointer | ||
|
||
var writeHandle: FileHandle { tunnelReadPipe.fileHandleForWriting } | ||
var readHandle: FileHandle { tunnelWritePipe.fileHandleForReading } | ||
|
||
init(dylibPath: URL) throws(TunnelHandleError) { | ||
guard let dylibHandle = dlopen(dylibPath.path, RTLD_NOW | RTLD_LOCAL) else { | ||
var errStr = "UNKNOWN" | ||
let e = dlerror() | ||
if e != nil { | ||
errStr = String(cString: e!) | ||
} | ||
throw .dylib(errStr) | ||
} | ||
self.dylibHandle = dylibHandle | ||
|
||
guard let startSym = dlsym(dylibHandle, startSymbol) else { | ||
var errStr = "UNKNOWN" | ||
let e = dlerror() | ||
if e != nil { | ||
errStr = String(cString: e!) | ||
} | ||
throw .symbol(startSymbol, errStr) | ||
} | ||
let openTunnelFn = unsafeBitCast(startSym, to: OpenTunnel.self) | ||
tunnelReadPipe = Pipe() | ||
tunnelWritePipe = Pipe() | ||
let res = openTunnelFn(tunnelReadPipe.fileHandleForReading.fileDescriptor, | ||
tunnelWritePipe.fileHandleForWriting.fileDescriptor) | ||
guard res == 0 else { | ||
throw .openTunnel(OpenTunnelError(rawValue: res) ?? .unknown) | ||
} | ||
} | ||
|
||
func close() throws { | ||
dlclose(dylibHandle) | ||
} | ||
} | ||
|
||
enum TunnelHandleError: Error { | ||
case dylib(String) | ||
case symbol(String, String) | ||
case openTunnel(OpenTunnelError) | ||
|
||
var description: String { | ||
switch self { | ||
case let .dylib(d): return d | ||
case let .symbol(symbol, message): return "\(symbol): \(message)" | ||
case let .openTunnel(error): return "OpenTunnel: \(error.message)" | ||
} | ||
} | ||
} | ||
|
||
enum OpenTunnelError: Int32 { | ||
case errDupReadFD = -2 | ||
case errDupWriteFD = -3 | ||
case errOpenPipe = -4 | ||
case errNewTunnel = -5 | ||
case unknown = -99 | ||
|
||
var message: String { | ||
switch self { | ||
case .errDupReadFD: return "Failed to duplicate read file descriptor" | ||
case .errDupWriteFD: return "Failed to duplicate write file descriptor" | ||
case .errOpenPipe: return "Failed to open the pipe" | ||
case .errNewTunnel: return "Failed to create a new tunnel" | ||
case .unknown: return "Unknown error code" | ||
} | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
Coder Desktop/VPN/com_coder_Coder_Desktop_VPN-Bridging-Header.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#ifndef CoderPacketTunnelProvider_Bridging_Header_h | ||
#define CoderPacketTunnelProvider_Bridging_Header_h | ||
|
||
// GoInt32 OpenTunnel(GoInt32 cReadFD, GoInt32 cWriteFD); | ||
typedef int(*OpenTunnel)(int, int); | ||
|
||
#endif /* CoderPacketTunnelProvider_Bridging_Header_h */ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import CryptoKit | ||
import Foundation | ||
|
||
public protocol Validator: Sendable { | ||
func validate(path: URL) async throws | ||
} | ||
|
||
public enum ValidationError: LocalizedError { | ||
case fileNotFound | ||
case unableToCreateStaticCode | ||
case invalidSignature | ||
case unableToRetrieveInfo | ||
case invalidIdentifier(identifier: String?) | ||
case invalidTeamIdentifier(identifier: String?) | ||
case missingInfoPList | ||
case invalidVersion(version: String?) | ||
|
||
public var errorDescription: String? { | ||
switch self { | ||
case .fileNotFound: | ||
return "The file does not exist." | ||
case .unableToCreateStaticCode: | ||
return "Unable to create a static code object." | ||
case .invalidSignature: | ||
return "The file's signature is invalid." | ||
case .unableToRetrieveInfo: | ||
return "Unable to retrieve signing information." | ||
case let .invalidIdentifier(identifier): | ||
return "Invalid identifier: \(identifier ?? "unknown")." | ||
case let .invalidVersion(version): | ||
return "Invalid runtime version: \(version ?? "unknown")." | ||
case let .invalidTeamIdentifier(identifier): | ||
return "Invalid team identifier: \(identifier ?? "unknown")." | ||
case .missingInfoPList: | ||
return "Info.plist is not embedded within the dylib." | ||
} | ||
} | ||
} | ||
|
||
public struct SignatureValidator: Validator { | ||
private let expectedName = "CoderVPN" | ||
private let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib" | ||
private let expectedTeamIdentifier = "4399GN35BJ" | ||
private let minDylibVersion = "2.18.1" | ||
|
||
private let infoIdentifierKey = "CFBundleIdentifier" | ||
private let infoNameKey = "CFBundleName" | ||
private let infoShortVersionKey = "CFBundleShortVersionString" | ||
|
||
private let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation) | ||
|
||
public init() {} | ||
|
||
public func validate(path: URL) throws { | ||
guard FileManager.default.fileExists(atPath: path.path) else { | ||
throw ValidationError.fileNotFound | ||
} | ||
|
||
var staticCode: SecStaticCode? | ||
let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode) | ||
guard status == errSecSuccess, let code = staticCode else { | ||
throw ValidationError.unableToCreateStaticCode | ||
} | ||
|
||
let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil) | ||
guard validateStatus == errSecSuccess else { | ||
throw ValidationError.invalidSignature | ||
} | ||
|
||
var information: CFDictionary? | ||
let infoStatus = SecCodeCopySigningInformation(code, signInfoFlags, &information) | ||
guard infoStatus == errSecSuccess, let info = information as? [String: Any] else { | ||
throw ValidationError.unableToRetrieveInfo | ||
} | ||
|
||
guard let identifier = info[kSecCodeInfoIdentifier as String] as? String, | ||
identifier == expectedIdentifier | ||
else { | ||
throw ValidationError.invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String) | ||
} | ||
|
||
guard let teamIdentifier = info[kSecCodeInfoTeamIdentifier as String] as? String, | ||
teamIdentifier == expectedTeamIdentifier | ||
else { | ||
throw ValidationError.invalidTeamIdentifier( | ||
identifier: info[kSecCodeInfoTeamIdentifier as String] as? String | ||
) | ||
} | ||
|
||
guard let infoPlist = info[kSecCodeInfoPList as String] as? [String: AnyObject] else { | ||
throw ValidationError.missingInfoPList | ||
} | ||
|
||
guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else { | ||
throw ValidationError.invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String) | ||
} | ||
|
||
guard let plistName = infoPlist[infoNameKey] as? String, plistName == expectedName else { | ||
throw ValidationError.invalidIdentifier(identifier: infoPlist[infoNameKey] as? String) | ||
} | ||
|
||
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String, | ||
minDylibVersion.compare(dylibVersion, options: .numeric) != .orderedDescending | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else { | ||
throw ValidationError.invalidVersion(version: infoPlist[infoShortVersionKey] as? String) | ||
} | ||
} | ||
} | ||
|
||
public actor Downloader { | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let validator: Validator | ||
public init(validator: Validator = SignatureValidator()) { | ||
self.validator = validator | ||
} | ||
|
||
public func download(src: URL, dest: URL) async throws { | ||
var req = URLRequest(url: src) | ||
if FileManager.default.fileExists(atPath: dest.path) { | ||
if let existingFileData = try? Data(contentsOf: dest) { | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
req.setValue(etag(data: existingFileData), forHTTPHeaderField: "If-None-Match") | ||
} | ||
} | ||
// TODO: Add Content-Length headers to coderd, add download progress delegate | ||
let (tempURL, response) = try await URLSession.shared.download(for: req) | ||
defer { | ||
if FileManager.default.fileExists(atPath: dest.path) { | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
do { try FileManager.default.removeItem(at: tempURL) } catch {} | ||
} | ||
} | ||
|
||
guard let httpResponse = response as? HTTPURLResponse else { | ||
throw DownloadError.invalidResponse | ||
} | ||
guard httpResponse.statusCode != 304 else { | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return | ||
} | ||
guard (200 ..< 300).contains(httpResponse.statusCode) else { | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throw DownloadError.unexpectedStatusCode(httpResponse.statusCode) | ||
} | ||
|
||
if FileManager.default.fileExists(atPath: dest.path) { | ||
try FileManager.default.removeItem(at: dest) | ||
} | ||
try FileManager.default.moveItem(at: tempURL, to: dest) | ||
try await validator.validate(path: dest) | ||
} | ||
} | ||
|
||
func etag(data: Data) -> String { | ||
let sha1Hash = Insecure.SHA1.hash(data: data) | ||
let etag = sha1Hash.map { String(format: "%02x", $0) }.joined() | ||
return "\"\(etag)\"" | ||
} | ||
|
||
enum DownloadError: LocalizedError { | ||
case unexpectedStatusCode(Int) | ||
case invalidResponse | ||
|
||
var localizedDescription: String { | ||
switch self { | ||
case let .unexpectedStatusCode(code): | ||
return "Unexpected status code: \(code)" | ||
case .invalidResponse: | ||
return "Received non-HTTP response" | ||
} | ||
} | ||
} |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
#import <Foundation/Foundation.h> | ||
|
||
//! Project version number for VPNLib. | ||
FOUNDATION_EXPORT double VPNLibVersionNumber; | ||
|
||
//! Project version string for VPNLib. | ||
FOUNDATION_EXPORT const unsigned char VPNLibVersionString[]; | ||
|
||
// In this header, you should import all the public headers of your framework using statements like #import <VPNLib/PublicHeader.h> | ||
|
||
|
516 changes: 258 additions & 258 deletions
516
Coder Desktop/Proto/vpn.pb.swift → Coder Desktop/VPNLib/vpn.pb.swift
Large diffs are not rendered by default.
Oops, something went wrong.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import Foundation | ||
import Mocker | ||
import Testing | ||
@testable import VPNLib | ||
|
||
struct NoopValidator: Validator { | ||
func validate(path _: URL) async throws {} | ||
} | ||
|
||
@Suite | ||
struct DownloaderTests { | ||
let downloader = Downloader(validator: NoopValidator()) | ||
|
||
@Test | ||
func downloadFile() async throws { | ||
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) | ||
let testData = Data("foo".utf8) | ||
|
||
let fileURL = URL(string: "http://example.com/test1.txt")! | ||
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: testData]).register() | ||
|
||
try await downloader.download(src: fileURL, dest: destinationURL) | ||
|
||
try #require(FileManager.default.fileExists(atPath: destinationURL.path)) | ||
defer { try? FileManager.default.removeItem(at: destinationURL) } | ||
|
||
let downloadedData = try Data(contentsOf: destinationURL) | ||
#expect(downloadedData == testData) | ||
} | ||
|
||
@Test | ||
func fileNotModified() async throws { | ||
let testData = Data("foo bar".utf8) | ||
let fileURL = URL(string: "http://example.com/test2.txt")! | ||
|
||
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) | ||
defer { try? FileManager.default.removeItem(at: destinationURL) } | ||
|
||
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: testData]).register() | ||
|
||
try await downloader.download(src: fileURL, dest: destinationURL) | ||
try #require(FileManager.default.fileExists(atPath: destinationURL.path)) | ||
let downloadedData = try Data(contentsOf: destinationURL) | ||
#expect(downloadedData == testData) | ||
|
||
Mock(url: fileURL, contentType: .html, statusCode: 304, data: [.get: Data()]).register() | ||
ethanndickson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
try await downloader.download(src: fileURL, dest: destinationURL) | ||
let unchangedData = try Data(contentsOf: destinationURL) | ||
#expect(unchangedData == testData) | ||
} | ||
|
||
@Test | ||
func fileUpdated() async throws { | ||
let ogData = Data("foo bar".utf8) | ||
let newData = Data("foo bar qux".utf8) | ||
|
||
let fileURL = URL(string: "http://example.com/test3.txt")! | ||
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) | ||
defer { try? FileManager.default.removeItem(at: destinationURL) } | ||
|
||
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: ogData]).register() | ||
|
||
try await downloader.download(src: fileURL, dest: destinationURL) | ||
try #require(FileManager.default.fileExists(atPath: destinationURL.path)) | ||
var downloadedData = try Data(contentsOf: destinationURL) | ||
#expect(downloadedData == ogData) | ||
|
||
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: newData]).register() | ||
|
||
try await downloader.download(src: fileURL, dest: destinationURL) | ||
downloadedData = try Data(contentsOf: destinationURL) | ||
#expect(downloadedData == newData) | ||
} | ||
} |
2 changes: 1 addition & 1 deletion
2
Coder Desktop/ProtoTests/ProtoTests.swift → Coder Desktop/VPNLibTests/ProtoTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
Coder Desktop/ProtoTests/SpeakerTests.swift → Coder Desktop/VPNLibTests/SpeakerTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.