@cappitolian/http-local-server-swifter 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,19 +6,13 @@ import UIKit
|
|
|
6
6
|
///
|
|
7
7
|
/// IMPORTANT: iOS Local Network permission does NOT block TCP sockets.
|
|
8
8
|
/// It only blocks mDNS/Bonjour service discovery. This means Swifter's
|
|
9
|
-
/// server.start() will always succeed regardless of permission status
|
|
10
|
-
/// the denial is only detectable via Bonjour/mDNS probes.
|
|
9
|
+
/// server.start() will always succeed regardless of permission status.
|
|
11
10
|
///
|
|
12
|
-
/// Detection strategy
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
///
|
|
16
|
-
///
|
|
17
|
-
/// 2. UDP probe to 224.0.0.251:5353 (mDNS multicast) as a secondary signal
|
|
18
|
-
/// to trigger the system dialog on first launch.
|
|
19
|
-
///
|
|
20
|
-
/// The first probe that resolves wins. If both time out, we assume .unknown
|
|
21
|
-
/// and allow the server to start (covers simulator and edge cases).
|
|
11
|
+
/// Detection strategy — NWListener with serviceRegistrationUpdateHandler:
|
|
12
|
+
/// The PolicyDenied error for a revoked Local Network permission surfaces
|
|
13
|
+
/// in NWListener.serviceRegistrationUpdateHandler, not in stateUpdateHandler.
|
|
14
|
+
/// A connection handler must also be set or NWListener fails with error 22
|
|
15
|
+
/// before iOS even evaluates the permission.
|
|
22
16
|
@objc public class LocalNetworkPermission: NSObject {
|
|
23
17
|
|
|
24
18
|
// MARK: - Types
|
|
@@ -32,128 +26,112 @@ import UIKit
|
|
|
32
26
|
// MARK: - Constants
|
|
33
27
|
|
|
34
28
|
/// Must match the NSBonjourServices entry in Info.plist exactly.
|
|
35
|
-
/// NWBrowser will return NoAuth(-65555) for any type NOT listed there,
|
|
36
|
-
/// even when the user has granted Local Network permission.
|
|
37
29
|
private static let bonjourServiceType = "_ssspos._tcp"
|
|
38
30
|
|
|
39
31
|
// MARK: - Private state
|
|
40
32
|
|
|
41
|
-
private var
|
|
42
|
-
private var udpConnection: NWConnection?
|
|
33
|
+
private var listener: NWListener?
|
|
43
34
|
|
|
44
35
|
// MARK: - Permission check
|
|
45
36
|
|
|
46
|
-
/// Checks Local Network permission
|
|
37
|
+
/// Checks Local Network permission by advertising a Bonjour service via NWListener.
|
|
47
38
|
///
|
|
48
|
-
///
|
|
49
|
-
///
|
|
39
|
+
/// The permission denial for a revoked permission surfaces in
|
|
40
|
+
/// serviceRegistrationUpdateHandler — not in stateUpdateHandler.
|
|
41
|
+
/// A dummy connection handler is required to prevent error 22 (Invalid argument).
|
|
50
42
|
///
|
|
51
|
-
/// -
|
|
52
|
-
/// the OS returns the cached decision immediately without showing a dialog.
|
|
43
|
+
/// - Parameter completion: Called on the main thread with the permission status.
|
|
53
44
|
public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
|
|
54
45
|
var completed = false
|
|
55
46
|
|
|
56
47
|
let finish: (PermissionStatus) -> Void = { [weak self] status in
|
|
57
48
|
guard !completed else { return }
|
|
58
49
|
completed = true
|
|
59
|
-
self?.
|
|
60
|
-
self?.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
self?.listener?.cancel()
|
|
51
|
+
self?.listener = nil
|
|
52
|
+
DispatchQueue.main.async {
|
|
53
|
+
print("✅ LocalNetworkPermission resolved: \(status)")
|
|
54
|
+
completion(status)
|
|
55
|
+
}
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
// Timeout fallback — if neither probe resolves, allow startup.
|
|
67
|
-
// Covers: simulator, already-granted permission where iOS skips the callback.
|
|
68
|
-
// 6s gives iOS enough time to evaluate a previously-revoked permission,
|
|
69
|
-
// which can take longer than a first-time denial.
|
|
70
58
|
let timeout = DispatchWorkItem {
|
|
71
|
-
print("⚠️ LocalNetworkPermission: timeout
|
|
59
|
+
print("⚠️ LocalNetworkPermission: timeout — resolving as .unknown")
|
|
72
60
|
finish(.unknown)
|
|
73
61
|
}
|
|
74
|
-
DispatchQueue.main.asyncAfter(deadline: .now() +
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
62
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeout)
|
|
63
|
+
|
|
64
|
+
let params = NWParameters.tcp
|
|
65
|
+
params.includePeerToPeer = true
|
|
66
|
+
|
|
67
|
+
guard let listener = try? NWListener(using: params) else {
|
|
68
|
+
print("⚠️ LocalNetworkPermission: could not create NWListener")
|
|
69
|
+
timeout.cancel()
|
|
70
|
+
finish(.unknown)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
listener.service = NWListener.Service(type: Self.bonjourServiceType)
|
|
75
|
+
self.listener = listener
|
|
76
|
+
|
|
77
|
+
// Required: NWListener fails with error 22 (Invalid argument) if no
|
|
78
|
+
// connection handler is set. This dummy handler satisfies the requirement.
|
|
79
|
+
listener.newConnectionHandler = { connection in
|
|
80
|
+
connection.cancel()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// This is where iOS reports PolicyDenied for a revoked Local Network permission.
|
|
84
|
+
// stateUpdateHandler only reports generic listener errors, not Bonjour policy errors.
|
|
85
|
+
listener.serviceRegistrationUpdateHandler = { change in
|
|
86
|
+
print("🔍 LocalNetworkPermission serviceRegistration: \(change)")
|
|
87
|
+
switch change {
|
|
88
|
+
case .add:
|
|
89
|
+
// Service registered successfully — permission is granted.
|
|
92
90
|
timeout.cancel()
|
|
93
91
|
finish(.granted)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
case .cancelled:
|
|
104
|
-
break
|
|
105
|
-
default:
|
|
92
|
+
|
|
93
|
+
case .remove:
|
|
94
|
+
// Service was removed. On permission denial this fires immediately
|
|
95
|
+
// after the _NWAdvertiser PolicyDenied error in the system logs.
|
|
96
|
+
// We treat an immediate remove (before .add) as denied.
|
|
97
|
+
timeout.cancel()
|
|
98
|
+
finish(.denied)
|
|
99
|
+
|
|
100
|
+
@unknown default:
|
|
106
101
|
break
|
|
107
102
|
}
|
|
108
103
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Also catches policy denials that surface at the UDP layer.
|
|
114
|
-
let host = NWEndpoint.Host("224.0.0.251")
|
|
115
|
-
let port = NWEndpoint.Port(rawValue: 5353)!
|
|
116
|
-
let conn = NWConnection(host: host, port: port, using: .udp)
|
|
117
|
-
self.udpConnection = conn
|
|
118
|
-
|
|
119
|
-
conn.stateUpdateHandler = { state in
|
|
120
|
-
print("🔍 LocalNetworkPermission UDP state: \(state)")
|
|
104
|
+
|
|
105
|
+
// stateUpdateHandler still needed to catch non-permission listener failures.
|
|
106
|
+
listener.stateUpdateHandler = { state in
|
|
107
|
+
print("🔍 LocalNetworkPermission NWListener state: \(state)")
|
|
121
108
|
switch state {
|
|
122
|
-
case .waiting(let error):
|
|
123
|
-
print("🔍 LocalNetworkPermission UDP waiting error code: \((error as NSError).code) — \(error.localizedDescription)")
|
|
124
|
-
if Self.isPolicyDenied(error) {
|
|
125
|
-
timeout.cancel()
|
|
126
|
-
finish(.denied)
|
|
127
|
-
}
|
|
128
|
-
// Other .waiting errors (no route, offline) are transient — ignore
|
|
129
109
|
case .failed(let error):
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
let code = (error as NSError).code
|
|
111
|
+
print("🔍 LocalNetworkPermission NWListener error: \(code) — \(error.localizedDescription)")
|
|
112
|
+
// Only treat explicit policy errors as denied; other failures are unknown.
|
|
113
|
+
timeout.cancel()
|
|
114
|
+
finish(Self.isPolicyDenied(error) ? .denied : .unknown)
|
|
115
|
+
|
|
136
116
|
case .cancelled:
|
|
137
117
|
break
|
|
118
|
+
|
|
138
119
|
default:
|
|
139
120
|
break
|
|
140
|
-
// Note: UDP .ready does NOT confirm permission — we rely on NWBrowser for that.
|
|
141
|
-
// A UDP socket can reach .ready even when Bonjour is blocked.
|
|
142
121
|
}
|
|
143
122
|
}
|
|
144
|
-
|
|
123
|
+
|
|
124
|
+
listener.start(queue: .main)
|
|
145
125
|
}
|
|
146
126
|
|
|
147
127
|
// MARK: - Recovery
|
|
148
128
|
|
|
149
129
|
/// Opens the app's page in iOS Settings so the user can manually grant Local Network access.
|
|
150
130
|
///
|
|
151
|
-
/// - Note: This is the only recovery path
|
|
152
|
-
/// - Warning: On iOS 17,
|
|
153
|
-
/// the permission for it to take effect. This is a known iOS 17 bug.
|
|
131
|
+
/// - Note: This is the only recovery path after denial — iOS cannot re-prompt.
|
|
132
|
+
/// - Warning: On iOS 17, a device restart may be needed after granting the permission.
|
|
154
133
|
public func openAppSettings() {
|
|
155
134
|
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
|
156
|
-
|
|
157
135
|
DispatchQueue.main.async {
|
|
158
136
|
UIApplication.shared.open(url)
|
|
159
137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cappitolian/http-local-server-swifter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "Runs a local HTTP server on your device, accessible over LAN. Supports connect, disconnect, GET, and POST methods with IP and port discovery.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|