wip
This commit is contained in:
parent
cb1bc1c8aa
commit
86594fb663
10 changed files with 507 additions and 81 deletions
40
.github/actions/notarize/action.yml
vendored
40
.github/actions/notarize/action.yml
vendored
|
|
@ -15,10 +15,6 @@ inputs:
|
||||||
export-path:
|
export-path:
|
||||||
description: The path to export the archive to
|
description: The path to export the archive to
|
||||||
required: true
|
required: true
|
||||||
outputs:
|
|
||||||
notarized-app:
|
|
||||||
description: The compressed and notarized app
|
|
||||||
value: ${{ steps.notarize.outputs.notarized-app }}
|
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -28,31 +24,29 @@ runs:
|
||||||
run: |
|
run: |
|
||||||
echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8
|
echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8
|
||||||
|
|
||||||
echo '{"destination":"upload","method":"developer-id"}' \
|
echo '{"destination":"export","method":"developer-id"}' \
|
||||||
| plutil -convert xml1 -o ExportOptions.plist -
|
| plutil -convert xml1 -o ExportOptions.plist -
|
||||||
|
|
||||||
xcodebuild \
|
xcodebuild \
|
||||||
-exportArchive \
|
-exportArchive \
|
||||||
-allowProvisioningUpdates \
|
-allowProvisioningUpdates \
|
||||||
-allowProvisioningDeviceRegistration \
|
-allowProvisioningDeviceRegistration \
|
||||||
-authenticationKeyID ${{ inputs.app-store-key-id }} \
|
-skipPackagePluginValidation \
|
||||||
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
|
-skipMacroValidation \
|
||||||
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
|
-archivePath Wallet.xcarchive \
|
||||||
-archivePath '${{ inputs.archive-path }}' \
|
-exportPath Release \
|
||||||
-exportOptionsPlist ExportOptions.plist
|
-exportOptionsPlist ExportOptions.plist
|
||||||
|
|
||||||
until xcodebuild \
|
rm ExportOptions.plist
|
||||||
-exportNotarizedApp \
|
|
||||||
-allowProvisioningUpdates \
|
|
||||||
-allowProvisioningDeviceRegistration \
|
|
||||||
-authenticationKeyID ${{ inputs.app-store-key-id }} \
|
|
||||||
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
|
|
||||||
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
|
|
||||||
-archivePath '${{ inputs.archive-path }}' \
|
|
||||||
-exportPath ${{ inputs.export-path }}
|
|
||||||
do
|
|
||||||
echo "Failed to export app, trying again in 10s..."
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
|
|
||||||
rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist
|
NOTARY_AUTH=""
|
||||||
|
|
||||||
|
ditto -c -k --keepParent Release/Wallet.app Upload.zip
|
||||||
|
SUBMISSION_ID=$(xcrun notarytool submit --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip | awk '/ id:/ { print $2; exit }')
|
||||||
|
|
||||||
|
xcrun notarytool wait $SUBMISSION_ID --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8"
|
||||||
|
xcrun stapler staple Release/Wallet.app
|
||||||
|
|
||||||
|
aa archive -a lzma -b 8m -d Release -subdir Wallet.app -o Wallet.app.aar
|
||||||
|
|
||||||
|
rm -rf Upload.zip Release AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array>
|
||||||
|
<string>applinks:burrow.rs?mode=developer</string>
|
||||||
|
<string>webcredentials:burrow.rs?mode=developer</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.developer.networking.networkextension</key>
|
<key>com.apple.developer.networking.networkextension</key>
|
||||||
<array>
|
<array>
|
||||||
<string>packet-tunnel-provider</string>
|
<string>packet-tunnel-provider</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array>
|
||||||
|
<string>applinks:burrow.rs?mode=developer</string>
|
||||||
|
<string>webcredentials:burrow.rs?mode=developer</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.developer.networking.networkextension</key>
|
<key>com.apple.developer.networking.networkextension</key>
|
||||||
<array>
|
<array>
|
||||||
<string>packet-tunnel-provider</string>
|
<string>packet-tunnel-provider</string>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ struct BurrowApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
BurrowView()
|
BurrowView()
|
||||||
|
.onOpenURL { url in
|
||||||
|
print(url)
|
||||||
|
}
|
||||||
|
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
|
||||||
|
print(userActivity.webpageURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,29 @@
|
||||||
|
import AuthenticationServices
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
struct BurrowView: View {
|
struct BurrowView: View {
|
||||||
|
@Environment(\.webAuthenticationSession)
|
||||||
|
private var webAuthenticationSession
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
VStack {
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("Networks")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Spacer()
|
||||||
|
Menu {
|
||||||
|
Button("Hack Club", action: addHackClubNetwork)
|
||||||
|
Button("WireGuard", action: addWireGuardNetwork)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.title)
|
||||||
|
.accessibilityLabel("Add")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
NetworkCarouselView()
|
NetworkCarouselView()
|
||||||
Spacer()
|
Spacer()
|
||||||
TunnelStatusView()
|
TunnelStatusView()
|
||||||
|
|
@ -11,9 +31,31 @@ struct BurrowView: View {
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.navigationTitle("Networks")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addHackClubNetwork() {
|
||||||
|
guard
|
||||||
|
let issuerURL = URL(string: "https://slack.com"),
|
||||||
|
let redirectURI = URL(string: "https://burrow.rs/callback/oauth2") else { return }
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let session = try await OpenID.Session(
|
||||||
|
issuerURL: issuerURL,
|
||||||
|
redirectURI: redirectURI,
|
||||||
|
scopes: ["openid", "profile"],
|
||||||
|
clientID: "2210535565.6884042183125"
|
||||||
|
)
|
||||||
|
let response = try await session.authorize(webAuthenticationSession)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addWireGuardNetwork() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -24,3 +66,4 @@ struct NetworkView_Previews: PreviewProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
|
||||||
39
Apple/App/NetworkCarouselView.swift
Normal file
39
Apple/App/NetworkCarouselView.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NetworkCarouselView: View {
|
||||||
|
var networks: [any Network] = [
|
||||||
|
HackClub(id: "1"),
|
||||||
|
HackClub(id: "2"),
|
||||||
|
WireGuard(id: "4"),
|
||||||
|
HackClub(id: "5"),
|
||||||
|
]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
LazyHStack {
|
||||||
|
ForEach(networks, id: \.id) { network in
|
||||||
|
NetworkView(network: network)
|
||||||
|
.containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center)
|
||||||
|
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
|
||||||
|
content
|
||||||
|
.scaleEffect(1.0 - abs(phase.value) * 0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollTargetLayout()
|
||||||
|
.scrollClipDisabled()
|
||||||
|
.scrollIndicators(.hidden)
|
||||||
|
.defaultScrollAnchor(.center)
|
||||||
|
.scrollTargetBehavior(.viewAligned)
|
||||||
|
.containerRelativeFrame(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct NetworkCarouselView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NetworkCarouselView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -30,59 +30,9 @@ struct NetworkView<Content: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AddNetworkView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("Add Network")
|
|
||||||
.frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.stroke(style: .init(lineWidth: 2, dash: [6]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NetworkView where Content == AnyView {
|
extension NetworkView where Content == AnyView {
|
||||||
init(network: any Network) {
|
init(network: any Network) {
|
||||||
color = network.backgroundColor
|
color = network.backgroundColor
|
||||||
content = { AnyView(network.label) }
|
content = { AnyView(network.label) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NetworkCarouselView: View {
|
|
||||||
var networks: [any Network] = [
|
|
||||||
HackClub(id: "1"),
|
|
||||||
HackClub(id: "2"),
|
|
||||||
WireGuard(id: "4"),
|
|
||||||
HackClub(id: "5"),
|
|
||||||
]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView(.horizontal) {
|
|
||||||
LazyHStack {
|
|
||||||
ForEach(networks, id: \.id) { network in
|
|
||||||
NetworkView(network: network)
|
|
||||||
.containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center)
|
|
||||||
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
|
|
||||||
content
|
|
||||||
.scaleEffect(1.0 - abs(phase.value) * 0.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AddNetworkView()
|
|
||||||
}
|
|
||||||
.scrollTargetLayout()
|
|
||||||
}
|
|
||||||
.scrollClipDisabled()
|
|
||||||
.scrollIndicators(.hidden)
|
|
||||||
.defaultScrollAnchor(.center)
|
|
||||||
.scrollTargetBehavior(.viewAligned)
|
|
||||||
.containerRelativeFrame(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
struct NetworkCarouselView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NetworkCarouselView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
|
||||||
364
Apple/App/OpenID.swift
Normal file
364
Apple/App/OpenID.swift
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
import AuthenticationServices
|
||||||
|
import SwiftUI
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum OAuth2 {
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case unknown
|
||||||
|
case invalidAuthorizationURL
|
||||||
|
case invalidCallbackURL
|
||||||
|
case invalidRedirectURI
|
||||||
|
case invalidScopes(Set<String>)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TokenType: Codable, LosslessStringConvertible {
|
||||||
|
case bearer
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
init?(_ description: String) {
|
||||||
|
self = switch description {
|
||||||
|
case "bearer": .bearer
|
||||||
|
default: .unknown(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .bearer: "bearer"
|
||||||
|
case .unknown(let type): type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GrantType: Codable, LosslessStringConvertible {
|
||||||
|
case authorizationCode
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
init?(_ description: String) {
|
||||||
|
self = switch description {
|
||||||
|
case "authorization_code": .authorizationCode
|
||||||
|
default: .unknown(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .authorizationCode: "authorization_code"
|
||||||
|
case .unknown(let type): type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ResponseType: Codable, LosslessStringConvertible {
|
||||||
|
case code
|
||||||
|
case idToken
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
init?(_ description: String) {
|
||||||
|
self = switch description {
|
||||||
|
case "code": .code
|
||||||
|
case "id_token": .idToken
|
||||||
|
default: .unknown(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .code: "code"
|
||||||
|
case .idToken: "id_token"
|
||||||
|
case .unknown(let type): type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct AccessTokenRequest: Codable {
|
||||||
|
var clientID: String
|
||||||
|
var grantType: GrantType
|
||||||
|
var code: String?
|
||||||
|
var redirectURI: URL?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccessTokenResponse: Codable {
|
||||||
|
var accessToken: String
|
||||||
|
var tokenType: TokenType
|
||||||
|
var expiresIn: Double?
|
||||||
|
var refreshToken: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct CodeResponse: Codable {
|
||||||
|
var code: String
|
||||||
|
var state: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static var decoder: JSONDecoder {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
return decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static var encoder: JSONEncoder {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||||
|
return encoder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OpenID {
|
||||||
|
struct Configuration: Codable {
|
||||||
|
var issuer: URL
|
||||||
|
var authorizationEndpoint: URL
|
||||||
|
var tokenEndpoint: URL
|
||||||
|
var userinfoEndpoint: URL
|
||||||
|
var scopesSupported: Set<String>
|
||||||
|
|
||||||
|
static func load(from issuerURL: URL) async throws -> Self {
|
||||||
|
let configurationURL = issuerURL
|
||||||
|
.appending(component: ".well-known")
|
||||||
|
.appending(component: "openid-configuration")
|
||||||
|
let (data, _) = try await URLSession.shared.data(from: configurationURL)
|
||||||
|
return try OAuth2.decoder.decode(Self.self, from: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Session {
|
||||||
|
var authorizationEndpoint: URL
|
||||||
|
var tokenEndpoint: URL
|
||||||
|
var redirectURI: URL
|
||||||
|
var responseType = OAuth2.ResponseType.code
|
||||||
|
var scopes: Set<String>
|
||||||
|
var clientID: String
|
||||||
|
|
||||||
|
init(issuerURL: URL, redirectURI: URL, scopes: Set<String>, clientID: String) async throws {
|
||||||
|
async let configuration = Configuration.load(from: issuerURL)
|
||||||
|
try await self.init(
|
||||||
|
configuration: configuration,
|
||||||
|
redirectURI: redirectURI,
|
||||||
|
scopes: scopes,
|
||||||
|
clientID: clientID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(configuration: Configuration, redirectURI: URL, scopes: Set<String>, clientID: String) throws {
|
||||||
|
guard scopes.isSubset(of: configuration.scopesSupported) else {
|
||||||
|
throw OAuth2.Error.invalidScopes(scopes.subtracting(configuration.scopesSupported))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.authorizationEndpoint = configuration.authorizationEndpoint
|
||||||
|
self.tokenEndpoint = configuration.tokenEndpoint
|
||||||
|
self.redirectURI = redirectURI
|
||||||
|
self.scopes = scopes
|
||||||
|
self.clientID = clientID
|
||||||
|
}
|
||||||
|
|
||||||
|
private var authorizationURL: URL {
|
||||||
|
get throws {
|
||||||
|
var queryItems: [URLQueryItem] = [
|
||||||
|
.init(name: "client_id", value: clientID),
|
||||||
|
.init(name: "response_type", value: responseType.description),
|
||||||
|
.init(name: "redirect_uri", value: redirectURI.absoluteString),
|
||||||
|
]
|
||||||
|
if !scopes.isEmpty {
|
||||||
|
queryItems.append(.init(name: "scope", value: scopes.joined(separator: ",")))
|
||||||
|
}
|
||||||
|
guard var components = URLComponents(url: authorizationEndpoint, resolvingAgainstBaseURL: false) else {
|
||||||
|
throw OAuth2.Error.invalidAuthorizationURL
|
||||||
|
}
|
||||||
|
components.queryItems = queryItems
|
||||||
|
guard let authorizationURL = components.url else { throw OAuth2.Error.invalidAuthorizationURL }
|
||||||
|
return authorizationURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handle(callbackURL: URL) async throws -> OAuth2.AccessTokenResponse {
|
||||||
|
switch responseType {
|
||||||
|
case .code:
|
||||||
|
guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false) else {
|
||||||
|
throw OAuth2.Error.invalidCallbackURL
|
||||||
|
}
|
||||||
|
return try await handle(response: try components.decode(OAuth2.CodeResponse.self))
|
||||||
|
default:
|
||||||
|
throw OAuth2.Error.invalidCallbackURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handle(response: OAuth2.CodeResponse) async throws -> OAuth2.AccessTokenResponse {
|
||||||
|
let body = OAuth2.AccessTokenRequest(clientID: clientID, grantType: .authorizationCode, code: response.code)
|
||||||
|
|
||||||
|
var request = URLRequest(url: tokenEndpoint)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.httpBody = try OAuth2.encoder.encode(body)
|
||||||
|
|
||||||
|
let session = URLSession(configuration: .ephemeral)
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
let response = try OAuth2.decoder.decode(OAuth2.AccessTokenResponse.self, from: data)
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorize(
|
||||||
|
configure: (ASWebAuthenticationSession) -> Void
|
||||||
|
) async throws -> OAuth2.AccessTokenResponse {
|
||||||
|
let authorizationURL = try authorizationURL
|
||||||
|
let callbackURL = try await ASWebAuthenticationSession.start(
|
||||||
|
url: authorizationURL,
|
||||||
|
redirectURI: redirectURI,
|
||||||
|
configure: configure
|
||||||
|
)
|
||||||
|
return try await handle(callbackURL: callbackURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorize(_ session: WebAuthenticationSession) async throws -> OAuth2.AccessTokenResponse {
|
||||||
|
let authorizationURL = try authorizationURL
|
||||||
|
let callbackURL = try await session.start(
|
||||||
|
url: authorizationURL,
|
||||||
|
redirectURI: redirectURI
|
||||||
|
)
|
||||||
|
return try await handle(callbackURL: callbackURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WebAuthenticationSession {
|
||||||
|
func start(url: URL, redirectURI: URL) async throws -> URL {
|
||||||
|
if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) {
|
||||||
|
return try await authenticate(
|
||||||
|
using: url,
|
||||||
|
callback: try ASWebAuthenticationSession.callback(for: redirectURI),
|
||||||
|
additionalHeaderFields: [:]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let callbackURLScheme = try ASWebAuthenticationSession.callbackURLScheme(for: redirectURI) ?? ""
|
||||||
|
return try await authenticate(using: url, callbackURLScheme: callbackURLScheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ASWebAuthenticationSession {
|
||||||
|
static func start(url: URL, redirectURI: URL, configure: (ASWebAuthenticationSession) -> Void) async throws -> URL {
|
||||||
|
try await withUnsafeThrowingContinuation { continuation in
|
||||||
|
do {
|
||||||
|
let session: ASWebAuthenticationSession
|
||||||
|
if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) {
|
||||||
|
session = ASWebAuthenticationSession(
|
||||||
|
url: url,
|
||||||
|
callback: try callback(for: redirectURI),
|
||||||
|
completionHandler: completionHandler(for: continuation)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
session = ASWebAuthenticationSession(
|
||||||
|
url: url,
|
||||||
|
callbackURLScheme: try callbackURLScheme(for: redirectURI),
|
||||||
|
completionHandler: completionHandler(for: continuation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
configure(session)
|
||||||
|
session.start()
|
||||||
|
} catch {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func completionHandler(for continuation: UnsafeContinuation<URL, Error>) -> CompletionHandler {
|
||||||
|
return { url, error in
|
||||||
|
if let url {
|
||||||
|
continuation.resume(returning: url)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: error ?? OAuth2.Error.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ASWebAuthenticationSession {
|
||||||
|
@available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *)
|
||||||
|
fileprivate static func callback(for redirectURI: URL) throws -> Callback {
|
||||||
|
switch redirectURI.scheme {
|
||||||
|
case "https":
|
||||||
|
guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI }
|
||||||
|
return .https(host: host, path: redirectURI.path)
|
||||||
|
case "http":
|
||||||
|
throw OAuth2.Error.invalidRedirectURI
|
||||||
|
case .some(let scheme):
|
||||||
|
return .customScheme(scheme)
|
||||||
|
case .none:
|
||||||
|
throw OAuth2.Error.invalidRedirectURI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static func callbackURLScheme(for redirectURI: URL) throws -> String? {
|
||||||
|
switch redirectURI.scheme {
|
||||||
|
case "http", .none:
|
||||||
|
return nil
|
||||||
|
case "https":
|
||||||
|
#if os(macOS)
|
||||||
|
if
|
||||||
|
let host = url.host,
|
||||||
|
let associatedDomains = try? Task.current.associatedDomains,
|
||||||
|
!associatedDomains.contains(host) {
|
||||||
|
throw OAuth2.Error.invalidCallbackURL
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return "https"
|
||||||
|
case .some(let scheme):
|
||||||
|
return scheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension URLComponents {
|
||||||
|
fileprivate func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
||||||
|
guard let queryItems else {
|
||||||
|
throw DecodingError.valueNotFound(
|
||||||
|
T.self,
|
||||||
|
.init(codingPath: [], debugDescription: "Missing query items")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let data = try JSONEncoder().encode(try queryItems.values)
|
||||||
|
return try JSONDecoder().decode(T.self, from: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sequence where Element == URLQueryItem {
|
||||||
|
fileprivate var values: [String: String?] {
|
||||||
|
get throws {
|
||||||
|
try Dictionary(map { ($0.name, $0.value) }) { _, _ in
|
||||||
|
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Duplicate query items"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
import Security
|
||||||
|
|
||||||
|
private struct Task {
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
static var current: Self {
|
||||||
|
get throws {
|
||||||
|
guard let task = SecTaskCreateFromSelf(nil) else { throw Error.unknown }
|
||||||
|
return Self(task: task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task: SecTask
|
||||||
|
|
||||||
|
var associatedDomains: [String] {
|
||||||
|
get throws {
|
||||||
|
var error: Unmanaged<CFError>?
|
||||||
|
let value = SecTaskCopyValueForEntitlement(
|
||||||
|
task,
|
||||||
|
"com.apple.developer.associated-domains" as CFString,
|
||||||
|
&error
|
||||||
|
)
|
||||||
|
if let error = error?.takeRetainedValue() {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return value as! [String] // swiftlint:disable:this force_cast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -4,16 +4,19 @@ struct TunnelButton: View {
|
||||||
@Environment(\.tunnel)
|
@Environment(\.tunnel)
|
||||||
var tunnel: any Tunnel
|
var tunnel: any Tunnel
|
||||||
|
|
||||||
|
private var action: Action? { tunnel.action }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let action = tunnel.action {
|
Button {
|
||||||
Button {
|
if let action {
|
||||||
tunnel.perform(action)
|
tunnel.perform(action)
|
||||||
} label: {
|
|
||||||
Text(action.description)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
} label: {
|
||||||
.buttonStyle(.floating)
|
Text(action.description)
|
||||||
}
|
}
|
||||||
|
.disabled(action.isDisabled)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.buttonStyle(.floating)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,12 +43,21 @@ extension TunnelButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TunnelButton.Action {
|
extension TunnelButton.Action? {
|
||||||
var description: LocalizedStringKey {
|
var description: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .enable: "Enable"
|
case .enable: "Enable"
|
||||||
case .start: "Start"
|
case .start: "Start"
|
||||||
case .stop: "Stop"
|
case .stop: "Stop"
|
||||||
|
case .none: "Start"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDisabled: Bool {
|
||||||
|
if case .none = self {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; };
|
0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; };
|
||||||
0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; };
|
0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; };
|
||||||
43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; };
|
43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; };
|
||||||
|
D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; };
|
||||||
|
D000363F2BB895FB00E582EC /* OpenID.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OpenID.swift */; };
|
||||||
D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; };
|
D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; };
|
||||||
D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; };
|
D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; };
|
||||||
D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; };
|
D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; };
|
||||||
|
|
@ -78,6 +80,8 @@
|
||||||
0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = "<group>"; };
|
0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = "<group>"; };
|
||||||
0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
||||||
43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = "<group>"; };
|
43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = "<group>"; };
|
||||||
|
D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = "<group>"; };
|
||||||
|
D000363E2BB895FB00E582EC /* OpenID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenID.swift; sourceTree = "<group>"; };
|
||||||
D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = "<group>"; };
|
D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = "<group>"; };
|
||||||
D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = "<group>"; };
|
D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = "<group>"; };
|
||||||
D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
|
@ -247,7 +251,9 @@
|
||||||
D00AA8962A4669BC005C8102 /* AppDelegate.swift */,
|
D00AA8962A4669BC005C8102 /* AppDelegate.swift */,
|
||||||
43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */,
|
43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */,
|
||||||
D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */,
|
D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */,
|
||||||
|
D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */,
|
||||||
D01A79302B81630D0024EC91 /* NetworkView.swift */,
|
D01A79302B81630D0024EC91 /* NetworkView.swift */,
|
||||||
|
D000363E2BB895FB00E582EC /* OpenID.swift */,
|
||||||
D032E64D2B8A69C90006B8AD /* Networks */,
|
D032E64D2B8A69C90006B8AD /* Networks */,
|
||||||
D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */,
|
D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */,
|
||||||
D0FAB5952B818B2900F6A84B /* TunnelButton.swift */,
|
D0FAB5952B818B2900F6A84B /* TunnelButton.swift */,
|
||||||
|
|
@ -476,6 +482,7 @@
|
||||||
43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */,
|
43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */,
|
||||||
D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */,
|
D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */,
|
||||||
D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */,
|
D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */,
|
||||||
|
D000363F2BB895FB00E582EC /* OpenID.swift in Sources */,
|
||||||
D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */,
|
D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */,
|
||||||
D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */,
|
D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */,
|
||||||
D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */,
|
D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */,
|
||||||
|
|
@ -484,6 +491,7 @@
|
||||||
D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */,
|
D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */,
|
||||||
D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */,
|
D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */,
|
||||||
D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */,
|
D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */,
|
||||||
|
D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue