@cappitolian/http-local-server-swifter 0.0.21 → 0.0.23
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,14 @@ 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
|
-
/// in Info.plist, otherwise iOS returns NoAuth even when permission is granted.
|
|
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.
|
|
11
|
+
/// Detection strategy — NWListener with Bonjour advertising:
|
|
12
|
+
/// NWBrowser returns .ready even when permission is revoked (iOS bug).
|
|
13
|
+
/// NWListener advertising a Bonjour service correctly fails with PolicyDenied
|
|
14
|
+
/// when Local Network permission is OFF, including previously-revoked cases.
|
|
19
15
|
///
|
|
20
|
-
/// The
|
|
21
|
-
/// and allow the server to start (covers simulator and edge cases).
|
|
16
|
+
/// The service type MUST match NSBonjourServices in Info.plist exactly.
|
|
22
17
|
@objc public class LocalNetworkPermission: NSObject {
|
|
23
18
|
|
|
24
19
|
// MARK: - Types
|
|
@@ -32,106 +27,97 @@ import UIKit
|
|
|
32
27
|
// MARK: - Constants
|
|
33
28
|
|
|
34
29
|
/// 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
30
|
private static let bonjourServiceType = "_ssspos._tcp"
|
|
38
31
|
|
|
39
32
|
// MARK: - Private state
|
|
40
33
|
|
|
41
|
-
private var
|
|
42
|
-
private var udpConnection: NWConnection?
|
|
34
|
+
private var listener: NWListener?
|
|
43
35
|
|
|
44
36
|
// MARK: - Permission check
|
|
45
37
|
|
|
46
|
-
/// Checks Local Network permission
|
|
38
|
+
/// Checks Local Network permission by advertising a Bonjour service via NWListener.
|
|
47
39
|
///
|
|
48
|
-
///
|
|
49
|
-
///
|
|
40
|
+
/// NWListener is the only iOS API that correctly detects a revoked Local Network
|
|
41
|
+
/// permission. NWBrowser incorrectly returns .ready in that case (iOS bug).
|
|
50
42
|
///
|
|
51
|
-
/// -
|
|
52
|
-
///
|
|
43
|
+
/// - Parameter completion: Called on the main thread with the permission status.
|
|
44
|
+
/// `.granted` / `.denied` once iOS responds, or `.unknown` on timeout.
|
|
45
|
+
///
|
|
46
|
+
/// - Note: iOS shows the system dialog only on the first call. Subsequent calls
|
|
47
|
+
/// return the cached decision immediately without prompting the user.
|
|
53
48
|
public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
|
|
54
49
|
var completed = false
|
|
55
50
|
|
|
56
51
|
let finish: (PermissionStatus) -> Void = { [weak self] status in
|
|
57
52
|
guard !completed else { return }
|
|
58
53
|
completed = true
|
|
59
|
-
self?.
|
|
60
|
-
self?.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
self?.listener?.cancel()
|
|
55
|
+
self?.listener = nil
|
|
56
|
+
DispatchQueue.main.async {
|
|
57
|
+
print("✅ LocalNetworkPermission resolved: \(status)")
|
|
58
|
+
completion(status)
|
|
59
|
+
}
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
// Timeout fallback — if
|
|
67
|
-
// Covers: simulator,
|
|
68
|
-
let timeout = DispatchWorkItem {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
// Timeout fallback — allow startup if iOS never calls the state handler.
|
|
63
|
+
// Covers: simulator, and rare edge cases where the OS skips the callback.
|
|
64
|
+
let timeout = DispatchWorkItem {
|
|
65
|
+
print("⚠️ LocalNetworkPermission: timeout — resolving as .unknown")
|
|
66
|
+
finish(.unknown)
|
|
67
|
+
}
|
|
68
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeout)
|
|
69
|
+
|
|
70
|
+
// Create a TCP listener and advertise it as a Bonjour service.
|
|
71
|
+
// iOS evaluates the Local Network permission at advertisement time,
|
|
72
|
+
// not at socket bind time — which is why server.start() always succeeds.
|
|
73
|
+
let params = NWParameters.tcp
|
|
74
|
+
params.includePeerToPeer = true
|
|
75
|
+
|
|
76
|
+
guard let listener = try? NWListener(using: params) else {
|
|
77
|
+
print("⚠️ LocalNetworkPermission: could not create NWListener — resolving as .unknown")
|
|
78
|
+
timeout.cancel()
|
|
79
|
+
finish(.unknown)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
self.browser = browser
|
|
83
|
+
// Advertising this service type triggers the system dialog on first launch
|
|
84
|
+
// and also immediately returns PolicyDenied when permission is revoked.
|
|
85
|
+
listener.service = NWListener.Service(type: Self.bonjourServiceType)
|
|
86
|
+
self.listener = listener
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
listener.stateUpdateHandler = { state in
|
|
89
|
+
print("🔍 LocalNetworkPermission NWListener state: \(state)")
|
|
84
90
|
switch state {
|
|
85
91
|
case .ready:
|
|
92
|
+
// Bonjour advertisement succeeded — permission is granted.
|
|
86
93
|
timeout.cancel()
|
|
87
94
|
finish(.granted)
|
|
88
|
-
case .failed(let error):
|
|
89
|
-
if Self.isPolicyDenied(error) {
|
|
90
|
-
timeout.cancel()
|
|
91
|
-
finish(.denied)
|
|
92
|
-
} else {
|
|
93
|
-
// Non-policy failure (e.g. no Wi-Fi) — let UDP probe or timeout decide
|
|
94
|
-
print("⚠️ LocalNetworkPermission: NWBrowser failed (non-policy): \(error)")
|
|
95
|
-
}
|
|
96
|
-
case .cancelled:
|
|
97
|
-
break
|
|
98
|
-
default:
|
|
99
|
-
break
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
browser.start(queue: .main)
|
|
103
95
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let host = NWEndpoint.Host("224.0.0.251")
|
|
108
|
-
let port = NWEndpoint.Port(rawValue: 5353)!
|
|
109
|
-
let conn = NWConnection(host: host, port: port, using: .udp)
|
|
110
|
-
self.udpConnection = conn
|
|
96
|
+
case .failed(let error):
|
|
97
|
+
let code = (error as NSError).code
|
|
98
|
+
print("🔍 LocalNetworkPermission NWListener error code: \(code) — \(error.localizedDescription)")
|
|
111
99
|
|
|
112
|
-
conn.stateUpdateHandler = { state in
|
|
113
|
-
switch state {
|
|
114
|
-
case .waiting(let error):
|
|
115
100
|
if Self.isPolicyDenied(error) {
|
|
101
|
+
// PolicyDenied or NoAuth — user has denied Local Network permission.
|
|
116
102
|
timeout.cancel()
|
|
117
103
|
finish(.denied)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
104
|
+
} else {
|
|
105
|
+
// Port conflict or other non-permission error — treat as unknown
|
|
106
|
+
// and allow the server to start rather than blocking the user.
|
|
107
|
+
print("⚠️ LocalNetworkPermission: NWListener failed (non-policy): \(error)")
|
|
122
108
|
timeout.cancel()
|
|
123
|
-
finish(.
|
|
109
|
+
finish(.unknown)
|
|
124
110
|
}
|
|
125
|
-
|
|
111
|
+
|
|
126
112
|
case .cancelled:
|
|
127
|
-
break
|
|
113
|
+
break // Expected — we cancelled it ourselves in finish()
|
|
114
|
+
|
|
128
115
|
default:
|
|
129
|
-
break
|
|
130
|
-
// Note: UDP .ready does NOT confirm permission — we rely on NWBrowser for that.
|
|
131
|
-
// A UDP socket can reach .ready even when Bonjour is blocked.
|
|
116
|
+
break // .setup, .waiting — keep waiting
|
|
132
117
|
}
|
|
133
118
|
}
|
|
134
|
-
|
|
119
|
+
|
|
120
|
+
listener.start(queue: .main)
|
|
135
121
|
}
|
|
136
122
|
|
|
137
123
|
// MARK: - Recovery
|
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.23",
|
|
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",
|