diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-07-26 15:39:28 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-07-28 11:37:10 +0200 |
| commit | 66a72c5f96c6297ac5911bcee87aa666c2c2f181 (patch) | |
| tree | 6256ea1c9d7da33985d6e7a5fc9f627ee48758ac | |
| parent | 91a37150bd623c8edf521620bbab27b9a272bfe0 (diff) | |
| download | mullvadvpn-66a72c5f96c6297ac5911bcee87aa666c2c2f181.tar.xz mullvadvpn-66a72c5f96c6297ac5911bcee87aa666c2c2f181.zip | |
PacketTunnel: extract types into separate files and re-arrange files
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 34 | ||||
| -rw-r--r-- | ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift | 22 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelConfiguration.swift | 66 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 106 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitor/Pinger.swift (renamed from ios/PacketTunnel/Pinger.swift) | 0 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift (renamed from ios/PacketTunnel/TunnelMonitor.swift) | 11 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitor/TunnelMonitorConfiguration.swift (renamed from ios/PacketTunnel/TunnelMonitorConfiguration.swift) | 0 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift | 23 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireGuardAdapterError+Localization.swift | 40 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireGuardLogLevel+Logging.swift | 22 |
10 files changed, 204 insertions, 120 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 4922b11220..bb617b48f5 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -264,6 +264,11 @@ 58DF5B7C28521A9F00E92647 /* ResultOperation+Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDF2846823E002B1049 /* ResultOperation+Output.swift */; }; 58DF5B7D28521AAC00E92647 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDD28468158002B1049 /* OutputOperation.swift */; }; 58DF5B7F2852778600E92647 /* OperationSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */; }; + 58E07299288031D5008902F8 /* WireGuardAdapterError+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */; }; + 58E0729D28814AAE008902F8 /* PacketTunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */; }; + 58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */; }; + 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */; }; + 58E072A528814C28008902F8 /* TunnelMonitorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E072A428814C28008902F8 /* TunnelMonitorDelegate.swift */; }; 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; }; 58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E20770274672CA00DE5D77 /* LaunchViewController.swift */; }; 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */; }; @@ -546,6 +551,11 @@ 58DF5B752852108E00E92647 /* InputInjectionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputInjectionBuilder.swift; sourceTree = "<group>"; }; 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationInputInjectionTests.swift; sourceTree = "<group>"; }; 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSmokeTests.swift; sourceTree = "<group>"; }; + 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapterError+Localization.swift"; sourceTree = "<group>"; }; + 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelConfiguration.swift; sourceTree = "<group>"; }; + 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardLogLevel+Logging.swift"; sourceTree = "<group>"; }; + 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MullvadEndpoint+WgEndpoint.swift"; sourceTree = "<group>"; }; + 58E072A428814C28008902F8 /* TunnelMonitorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorDelegate.swift; sourceTree = "<group>"; }; 58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; }; 58E20770274672CA00DE5D77 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; }; 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; @@ -974,11 +984,13 @@ isa = PBXGroup; children = ( 58CE5E7D224146470008646E /* Info.plist */, + 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */, 58CE5E7E224146470008646E /* PacketTunnel.entitlements */, + 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */, 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */, - 5838318A27C40A3900000571 /* Pinger.swift */, - 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */, - 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */, + 58E072A228814B96008902F8 /* TunnelMonitor */, + 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */, + 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */, ); path = PacketTunnel; sourceTree = "<group>"; @@ -993,6 +1005,17 @@ path = MullvadVPNScreenshots; sourceTree = "<group>"; }; + 58E072A228814B96008902F8 /* TunnelMonitor */ = { + isa = PBXGroup; + children = ( + 5838318A27C40A3900000571 /* Pinger.swift */, + 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */, + 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */, + 58E072A428814C28008902F8 /* TunnelMonitorDelegate.swift */, + ); + path = TunnelMonitor; + sourceTree = "<group>"; + }; 58ECD29023F178FD004298B6 /* Configurations */ = { isa = PBXGroup; children = ( @@ -1489,6 +1512,7 @@ 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */, 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */, 585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */, + 58E072A528814C28008902F8 /* TunnelMonitorDelegate.swift in Sources */, 587C575426D2615F005EF767 /* PacketTunnelOptions.swift in Sources */, 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */, @@ -1499,6 +1523,7 @@ 585DA89426B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */, 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */, 5875960B26F3723000BF6711 /* TunnelIPC.swift in Sources */, + 58E07299288031D5008902F8 /* WireGuardAdapterError+Localization.swift in Sources */, 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */, 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, @@ -1508,6 +1533,8 @@ 585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */, 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */, + 58E0729D28814AAE008902F8 /* PacketTunnelConfiguration.swift in Sources */, + 58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */, 580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */, 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, @@ -1517,6 +1544,7 @@ 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */, + 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */, 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, 58F840B32464491D0044E708 /* ChainedError.swift in Sources */, 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */, diff --git a/ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift b/ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift new file mode 100644 index 0000000000..c39af86ea0 --- /dev/null +++ b/ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift @@ -0,0 +1,22 @@ +// +// MullvadEndpoint+WgEndpoint.swift +// PacketTunnel +// +// Created by pronebird on 15/07/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import WireGuardKit + +extension MullvadEndpoint { + var ipv4RelayEndpoint: Endpoint { + return Endpoint(host: .ipv4(ipv4Relay.ip), port: .init(integerLiteral: ipv4Relay.port)) + } + + var ipv6RelayEndpoint: Endpoint? { + guard let ipv6Relay = ipv6Relay else { return nil } + + return Endpoint(host: .ipv6(ipv6Relay.ip), port: .init(integerLiteral: ipv6Relay.port)) + } +} diff --git a/ios/PacketTunnel/PacketTunnelConfiguration.swift b/ios/PacketTunnel/PacketTunnelConfiguration.swift new file mode 100644 index 0000000000..91afd5363b --- /dev/null +++ b/ios/PacketTunnel/PacketTunnelConfiguration.swift @@ -0,0 +1,66 @@ +// +// PacketTunnelConfiguration.swift +// PacketTunnel +// +// Created by pronebird on 15/07/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import WireGuardKit +import protocol Network.IPAddress + +struct PacketTunnelConfiguration { + var tunnelSettings: TunnelSettingsV2 + var selectorResult: RelaySelectorResult +} + +extension PacketTunnelConfiguration { + var wgTunnelConfig: TunnelConfiguration { + let mullvadEndpoint = selectorResult.endpoint + var peers = [mullvadEndpoint.ipv4RelayEndpoint] + if let ipv6RelayEndpoint = mullvadEndpoint.ipv6RelayEndpoint { + peers.append(ipv6RelayEndpoint) + } + + let peerConfigs = peers.compactMap { (endpoint) -> PeerConfiguration in + let pubKey = PublicKey(rawValue: selectorResult.endpoint.publicKey)! + var peerConfig = PeerConfiguration(publicKey: pubKey) + peerConfig.endpoint = endpoint + peerConfig.allowedIPs = [ + IPAddressRange(from: "0.0.0.0/0")!, + IPAddressRange(from: "::/0")! + ] + return peerConfig + } + + var interfaceConfig = InterfaceConfiguration( + privateKey: tunnelSettings.device.wgKeyData.privateKey + ) + interfaceConfig.listenPort = 0 + interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) } + interfaceConfig.addresses = [ + tunnelSettings.device.ipv4Address, + tunnelSettings.device.ipv6Address + ] + + return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs) + } + + var dnsServers: [IPAddress] { + let mullvadEndpoint = selectorResult.endpoint + let dnsSettings = tunnelSettings.dnsSettings + + if dnsSettings.effectiveEnableCustomDNS { + let dnsServers = dnsSettings.customDNSDomains + .prefix(DNSSettings.maxAllowedCustomDNSDomains) + return Array(dnsServers) + } else { + if let serverAddress = dnsSettings.blockingOptions.serverAddress { + return [serverAddress] + } else { + return [mullvadEndpoint.ipv4Gateway, mullvadEndpoint.ipv6Gateway] + } + } + } +} diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index e8d51b5ad1..f057e47592 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -415,109 +415,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { ) } } - -struct PacketTunnelConfiguration { - var tunnelSettings: TunnelSettingsV2 - var selectorResult: RelaySelectorResult -} - -extension PacketTunnelConfiguration { - var wgTunnelConfig: TunnelConfiguration { - let mullvadEndpoint = selectorResult.endpoint - var peers = [mullvadEndpoint.ipv4RelayEndpoint] - if let ipv6RelayEndpoint = mullvadEndpoint.ipv6RelayEndpoint { - peers.append(ipv6RelayEndpoint) - } - - let peerConfigs = peers.compactMap { (endpoint) -> PeerConfiguration in - let pubKey = PublicKey(rawValue: selectorResult.endpoint.publicKey)! - var peerConfig = PeerConfiguration(publicKey: pubKey) - peerConfig.endpoint = endpoint - peerConfig.allowedIPs = [ - IPAddressRange(from: "0.0.0.0/0")!, - IPAddressRange(from: "::/0")! - ] - return peerConfig - } - - var interfaceConfig = InterfaceConfiguration( - privateKey: tunnelSettings.device.wgKeyData.privateKey - ) - interfaceConfig.listenPort = 0 - interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) } - interfaceConfig.addresses = [ - tunnelSettings.device.ipv4Address, - tunnelSettings.device.ipv6Address - ] - - return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs) - } - - var dnsServers: [IPAddress] { - let mullvadEndpoint = selectorResult.endpoint - let dnsSettings = tunnelSettings.dnsSettings - - if dnsSettings.effectiveEnableCustomDNS { - let dnsServers = dnsSettings.customDNSDomains - .prefix(DNSSettings.maxAllowedCustomDNSDomains) - return Array(dnsServers) - } else { - if let serverAddress = dnsSettings.blockingOptions.serverAddress { - return [serverAddress] - } else { - return [mullvadEndpoint.ipv4Gateway, mullvadEndpoint.ipv6Gateway] - } - } - } -} - -extension WireGuardLogLevel { - var loggerLevel: Logger.Level { - switch self { - case .verbose: - return .debug - case .error: - return .error - } - } -} - -extension WireGuardAdapterError: LocalizedError { - public var errorDescription: String? { - switch self { - case .cannotLocateTunnelFileDescriptor: - return "Failure to locate tunnel file descriptor." - - case .invalidState: - return "Failure to perform an operation in such state." - - case .dnsResolution(let resolutionErrors): - let detailedErrorDescription = resolutionErrors - .enumerated() - .map { index, dnsResolutionError in - return "\(index): \(dnsResolutionError.address) \(dnsResolutionError.errorDescription ?? "???")" - } - .joined(separator: "\n") - - return "Failure to resolve endpoints:\n\(detailedErrorDescription)" - - case .setNetworkSettings: - return "Failure to set network settings." - - case .startWireGuardBackend(let code): - return "Failure to start WireGuard backend (error code: \(code))." - } - } -} - -extension MullvadEndpoint { - var ipv4RelayEndpoint: Endpoint { - return Endpoint(host: .ipv4(ipv4Relay.ip), port: .init(integerLiteral: ipv4Relay.port)) - } - - var ipv6RelayEndpoint: Endpoint? { - guard let ipv6Relay = ipv6Relay else { return nil } - - return Endpoint(host: .ipv6(ipv6Relay.ip), port: .init(integerLiteral: ipv6Relay.port)) - } -} diff --git a/ios/PacketTunnel/Pinger.swift b/ios/PacketTunnel/TunnelMonitor/Pinger.swift index feb0fd678a..feb0fd678a 100644 --- a/ios/PacketTunnel/Pinger.swift +++ b/ios/PacketTunnel/TunnelMonitor/Pinger.swift diff --git a/ios/PacketTunnel/TunnelMonitor.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift index b39a26d4d5..fd589e4aa1 100644 --- a/ios/PacketTunnel/TunnelMonitor.swift +++ b/ios/PacketTunnel/TunnelMonitor/TunnelMonitor.swift @@ -11,17 +11,6 @@ import NetworkExtension import WireGuardKit import Logging -protocol TunnelMonitorDelegate: AnyObject { - /// Invoked when tunnel monitor determined that connection is established. - func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) - - /// Invoked when tunnel monitor determined that connection attempt has failed. - func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) - - /// Invoked when network reachability status changes. - func tunnelMonitor(_ tunnelMonitor: TunnelMonitor, networkReachabilityStatusDidChange isNetworkReachable: Bool) -} - final class TunnelMonitor { private let adapter: WireGuardAdapter private let internalQueue = DispatchQueue(label: "TunnelMonitor") diff --git a/ios/PacketTunnel/TunnelMonitorConfiguration.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorConfiguration.swift index 13f4c4908d..13f4c4908d 100644 --- a/ios/PacketTunnel/TunnelMonitorConfiguration.swift +++ b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorConfiguration.swift diff --git a/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift new file mode 100644 index 0000000000..9839faf3e5 --- /dev/null +++ b/ios/PacketTunnel/TunnelMonitor/TunnelMonitorDelegate.swift @@ -0,0 +1,23 @@ +// +// TunnelMonitorDelegate.swift +// PacketTunnel +// +// Created by pronebird on 15/07/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol TunnelMonitorDelegate: AnyObject { + /// Invoked when tunnel monitor determined that connection is established. + func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) + + /// Invoked when tunnel monitor determined that connection attempt has failed. + func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) + + /// Invoked when network reachability status changes. + func tunnelMonitor( + _ tunnelMonitor: TunnelMonitor, + networkReachabilityStatusDidChange isNetworkReachable: Bool + ) +} diff --git a/ios/PacketTunnel/WireGuardAdapterError+Localization.swift b/ios/PacketTunnel/WireGuardAdapterError+Localization.swift new file mode 100644 index 0000000000..7568c65b87 --- /dev/null +++ b/ios/PacketTunnel/WireGuardAdapterError+Localization.swift @@ -0,0 +1,40 @@ +// +// WireGuardAdapterError+Localization.swift +// PacketTunnel +// +// Created by pronebird on 14/07/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import WireGuardKit + +extension WireGuardAdapterError: LocalizedError { + public var errorDescription: String? { + switch self { + case .cannotLocateTunnelFileDescriptor: + return "Failure to locate tunnel file descriptor." + + case .invalidState: + return "Failure to perform an operation in such state." + + case .dnsResolution(let resolutionErrors): + let detailedErrorDescription = resolutionErrors + .enumerated() + .map { index, dnsResolutionError in + let errorDescription = dnsResolutionError.errorDescription ?? "???" + + return "\(index): \(dnsResolutionError.address) \(errorDescription)" + } + .joined(separator: "\n") + + return "Failure to resolve endpoints:\n\(detailedErrorDescription)" + + case .setNetworkSettings: + return "Failure to set network settings." + + case .startWireGuardBackend(let code): + return "Failure to start WireGuard backend (error code: \(code))." + } + } +} diff --git a/ios/PacketTunnel/WireGuardLogLevel+Logging.swift b/ios/PacketTunnel/WireGuardLogLevel+Logging.swift new file mode 100644 index 0000000000..443941c74e --- /dev/null +++ b/ios/PacketTunnel/WireGuardLogLevel+Logging.swift @@ -0,0 +1,22 @@ +// +// WireGuardLogLevel+Logging.swift +// PacketTunnel +// +// Created by pronebird on 15/07/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import WireGuardKit +import Logging + +extension WireGuardLogLevel { + var loggerLevel: Logger.Level { + switch self { + case .verbose: + return .debug + case .error: + return .error + } + } +} |
