diff --git a/.github/actions/archive/action.yml b/.github/actions/archive/action.yml index e49eb0d..37282e1 100644 --- a/.github/actions/archive/action.yml +++ b/.github/actions/archive/action.yml @@ -35,6 +35,7 @@ runs: -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ + -onlyUsePackageVersionsFromResolvedFile \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ -archivePath '${{ inputs.archive-path }}' \ diff --git a/.github/actions/notarize/action.yml b/.github/actions/notarize/action.yml index c38a633..290ed86 100644 --- a/.github/actions/notarize/action.yml +++ b/.github/actions/notarize/action.yml @@ -15,6 +15,10 @@ inputs: export-path: description: The path to export the archive to required: true +outputs: + notarized-app: + description: The compressed and notarized app + value: ${{ steps.notarize.outputs.notarized-app }} runs: using: composite steps: @@ -24,26 +28,31 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - echo '{"destination":"export","method":"developer-id"}' \ + echo '{"destination":"upload","method":"developer-id"}' \ | plutil -convert xml1 -o ExportOptions.plist - - xcodebuild -exportArchive \ + xcodebuild \ + -exportArchive \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ - -skipPackagePluginValidation \ - -skipMacroValidation \ - -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -archivePath Wallet.xcarchive \ - -exportPath Release \ + -archivePath '${{ inputs.archive-path }}' \ -exportOptionsPlist ExportOptions.plist - ditto -c -k --keepParent Release/Wallet.app Upload.zip - xcrun notarytool submit --wait --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 - xcrun stapler staple Release/Wallet.app + until xcodebuild \ + -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 - 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 + rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist diff --git a/Apple/App/App-iOS.entitlements b/Apple/App/App-iOS.entitlements index 53fcbb7..02ee960 100644 --- a/Apple/App/App-iOS.entitlements +++ b/Apple/App/App-iOS.entitlements @@ -2,11 +2,6 @@ - com.apple.developer.associated-domains - - applinks:burrow.rs?mode=developer - webcredentials:burrow.rs?mode=developer - com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/App/App-macOS.entitlements b/Apple/App/App-macOS.entitlements index 53fcbb7..02ee960 100644 --- a/Apple/App/App-macOS.entitlements +++ b/Apple/App/App-macOS.entitlements @@ -2,11 +2,6 @@ - com.apple.developer.associated-domains - - applinks:burrow.rs?mode=developer - webcredentials:burrow.rs?mode=developer - com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/App/BurrowView.swift b/Apple/App/BurrowView.swift index 8447592..b78b1e1 100644 --- a/Apple/App/BurrowView.swift +++ b/Apple/App/BurrowView.swift @@ -1,29 +1,9 @@ -import AuthenticationServices import SwiftUI -#if !os(macOS) struct BurrowView: View { - @Environment(\.webAuthenticationSession) - private var webAuthenticationSession - var body: some View { NavigationStack { 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() Spacer() TunnelStatusView() @@ -31,35 +11,9 @@ struct BurrowView: View { .padding(.bottom) } .padding() - .handleOAuth2Callback() + .navigationTitle("Networks") } } - - private func addHackClubNetwork() { - Task { - try await authenticateWithSlack() - } - } - - private func addWireGuardNetwork() { - - } - - private func authenticateWithSlack() async throws { - guard - let authorizationEndpoint = URL(string: "https://slack.com/openid/connect/authorize"), - let tokenEndpoint = URL(string: "https://slack.com/api/openid.connect.token"), - let redirectURI = URL(string: "https://burrow.rs/callback/oauth2") else { return } - let session = OAuth2.Session( - authorizationEndpoint: authorizationEndpoint, - tokenEndpoint: tokenEndpoint, - redirectURI: redirectURI, - scopes: ["openid", "profile"], - clientID: "2210535565.6884042183125", - clientSecret: "2793c8a5255cae38830934c664eeb62d" - ) - let response = try await session.authorize(webAuthenticationSession) - } } #if DEBUG @@ -70,4 +24,3 @@ struct NetworkView_Previews: PreviewProvider { } } #endif -#endif diff --git a/Apple/App/NetworkCarouselView.swift b/Apple/App/NetworkCarouselView.swift deleted file mode 100644 index b120c60..0000000 --- a/Apple/App/NetworkCarouselView.swift +++ /dev/null @@ -1,39 +0,0 @@ -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 diff --git a/Apple/App/NetworkView.swift b/Apple/App/NetworkView.swift index b839d65..290254c 100644 --- a/Apple/App/NetworkView.swift +++ b/Apple/App/NetworkView.swift @@ -30,9 +30,59 @@ struct NetworkView: 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 { init(network: any Network) { color = network.backgroundColor 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 diff --git a/Apple/App/OAuth2.swift b/Apple/App/OAuth2.swift deleted file mode 100644 index dc8c62b..0000000 --- a/Apple/App/OAuth2.swift +++ /dev/null @@ -1,280 +0,0 @@ -import AuthenticationServices -import SwiftUI -import Foundation - -enum OAuth2 { - enum Error: Swift.Error { - case unknown - case invalidAuthorizationURL - case invalidCallbackURL - case invalidRedirectURI - } - - struct Credential { - var accessToken: String - var refreshToken: String? - var expirationDate: Date? - } - - struct Session { - var authorizationEndpoint: URL - var tokenEndpoint: URL - var redirectURI: URL - var responseType = OAuth2.ResponseType.code - var scopes: Set - var clientID: String - var clientSecret: String - - fileprivate static var queue: [Int: CheckedContinuation] = [:] - - fileprivate static func handle(url: URL) { - let continuations = queue - queue.removeAll() - for (_, continuation) in continuations { - continuation.resume(returning: url) - } - } - - public init( - authorizationEndpoint: URL, - tokenEndpoint: URL, - redirectURI: URL, - scopes: Set, - clientID: String, - clientSecret: String - ) { - self.authorizationEndpoint = authorizationEndpoint - self.tokenEndpoint = tokenEndpoint - self.redirectURI = redirectURI - self.scopes = scopes - self.clientID = clientID - self.clientSecret = clientSecret - } - - private var authorizationURL: URL { - get throws { - var queryItems: [URLQueryItem] = [ - .init(name: "client_id", value: clientID), - .init(name: "response_type", value: responseType.rawValue), - .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 { - var components = URLComponents() - components.queryItems = [ - .init(name: "client_id", value: clientID), - .init(name: "client_secret", value: clientSecret), - .init(name: "grant_type", value: GrantType.authorizationCode.rawValue), - .init(name: "code", value: response.code), - .init(name: "redirect_uri", value: redirectURI.absoluteString) - ] - let httpBody = Data(components.percentEncodedQuery!.utf8) - - var request = URLRequest(url: tokenEndpoint) - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" - request.httpBody = httpBody - - let session = URLSession(configuration: .ephemeral) - let (data, _) = try await session.data(for: request) - return try OAuth2.decoder.decode(OAuth2.AccessTokenResponse.self, from: data) - } - - func authorize(_ session: WebAuthenticationSession) async throws -> Credential { - let authorizationURL = try authorizationURL - let callbackURL = try await session.start( - url: authorizationURL, - redirectURI: redirectURI - ) - return try await handle(callbackURL: callbackURL).credential - } - } - - private struct CodeResponse: Codable { - var code: String - var state: String? - } - - private struct AccessTokenResponse: Codable { - var accessToken: String - var tokenType: TokenType - var expiresIn: Double? - var refreshToken: String? - - var credential: Credential { - .init(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expiresIn.map { Date.init(timeIntervalSinceNow: $0) }) - } - } - - enum TokenType: Codable, RawRepresentable { - case bearer - case unknown(String) - - init(rawValue: String) { - self = switch rawValue.lowercased() { - case "bearer": .bearer - default: .unknown(rawValue) - } - } - - var rawValue: String { - switch self { - case .bearer: "bearer" - case .unknown(let type): type - } - } - } - - enum GrantType: Codable, RawRepresentable { - case authorizationCode - case unknown(String) - - init(rawValue: String) { - self = switch rawValue.lowercased() { - case "authorization_code": .authorizationCode - default: .unknown(rawValue) - } - } - - var rawValue: String { - switch self { - case .authorizationCode: "authorization_code" - case .unknown(let type): type - } - } - } - - enum ResponseType: Codable, RawRepresentable { - case code - case idToken - case unknown(String) - - init(rawValue: String) { - self = switch rawValue.lowercased() { - case "code": .code - case "id_token": .idToken - default: .unknown(rawValue) - } - } - - var rawValue: String { - switch self { - case .code: "code" - case .idToken: "id_token" - case .unknown(let type): type - } - } - } - - 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 - } -} - -extension WebAuthenticationSession { - func start(url: URL, redirectURI: URL) async throws -> URL { - #if canImport(BrowserEngineKit) - if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) { - return try await authenticate( - using: url, - callback: try Self.callback(for: redirectURI), - additionalHeaderFields: [:] - ) - } - #endif - - return try await withThrowingTaskGroup(of: URL.self) { group in - group.addTask { - return try await authenticate(using: url, callbackURLScheme: redirectURI.scheme ?? "") - } - - let id = Int.random(in: 0.. ASWebAuthenticationSession.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 - } - } - #endif -} - -extension View { - func handleOAuth2Callback() -> some View { - onOpenURL { url in OAuth2.Session.handle(url: url) } - } -} - -extension URLComponents { - fileprivate func decode(_ type: T.Type) throws -> T { - guard let queryItems else { - throw DecodingError.valueNotFound( - T.self, - .init(codingPath: [], debugDescription: "Missing query items") - ) - } - let data = try OAuth2.encoder.encode(try queryItems.values) - return try OAuth2.decoder.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")) - } - } - } -} diff --git a/Apple/App/TunnelButton.swift b/Apple/App/TunnelButton.swift index 1f5693e..df8d7e6 100644 --- a/Apple/App/TunnelButton.swift +++ b/Apple/App/TunnelButton.swift @@ -4,19 +4,16 @@ struct TunnelButton: View { @Environment(\.tunnel) var tunnel: any Tunnel - private var action: Action? { tunnel.action } - var body: some View { - Button { - if let action { + if let action = tunnel.action { + Button { tunnel.perform(action) + } label: { + Text(action.description) } - } label: { - Text(action.description) + .padding(.horizontal) + .buttonStyle(.floating) } - .disabled(action.isDisabled) - .padding(.horizontal) - .buttonStyle(.floating) } } @@ -43,21 +40,12 @@ extension TunnelButton { } } -extension TunnelButton.Action? { +extension TunnelButton.Action { var description: LocalizedStringKey { switch self { case .enable: "Enable" case .start: "Start" case .stop: "Stop" - case .none: "Start" - } - } - - var isDisabled: Bool { - if case .none = self { - true - } else { - false } } } diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 3ea58b3..b08c4b0 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; - D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; }; - D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OAuth2.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 */; }; D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; @@ -80,8 +78,6 @@ 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; - D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = ""; }; - D000363E2BB895FB00E582EC /* OAuth2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -251,9 +247,7 @@ D00AA8962A4669BC005C8102 /* AppDelegate.swift */, 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */, D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */, - D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */, D01A79302B81630D0024EC91 /* NetworkView.swift */, - D000363E2BB895FB00E582EC /* OAuth2.swift */, D032E64D2B8A69C90006B8AD /* Networks */, D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */, D0FAB5952B818B2900F6A84B /* TunnelButton.swift */, @@ -482,7 +476,6 @@ 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */, D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */, D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */, - D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */, D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */, D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */, @@ -491,7 +484,6 @@ D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */, D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */, D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, - D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };