@cappitolian/http-local-server-swifter 0.0.22 → 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,116 +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
|
-
// 6s gives iOS enough time to evaluate a previously-revoked permission,
|
|
69
|
-
// which can take longer than a first-time denial.
|
|
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.
|
|
70
64
|
let timeout = DispatchWorkItem {
|
|
71
|
-
print("⚠️ LocalNetworkPermission: timeout
|
|
65
|
+
print("⚠️ LocalNetworkPermission: timeout — resolving as .unknown")
|
|
72
66
|
finish(.unknown)
|
|
73
67
|
}
|
|
74
|
-
DispatchQueue.main.asyncAfter(deadline: .now() +
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
82
|
+
|
|
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
|
|
87
|
+
|
|
88
|
+
listener.stateUpdateHandler = { state in
|
|
89
|
+
print("🔍 LocalNetworkPermission NWListener state: \(state)")
|
|
90
90
|
switch state {
|
|
91
91
|
case .ready:
|
|
92
|
+
// Bonjour advertisement succeeded — permission is granted.
|
|
92
93
|
timeout.cancel()
|
|
93
94
|
finish(.granted)
|
|
95
|
+
|
|
94
96
|
case .failed(let error):
|
|
95
|
-
|
|
97
|
+
let code = (error as NSError).code
|
|
98
|
+
print("🔍 LocalNetworkPermission NWListener error code: \(code) — \(error.localizedDescription)")
|
|
99
|
+
|
|
96
100
|
if Self.isPolicyDenied(error) {
|
|
101
|
+
// PolicyDenied or NoAuth — user has denied Local Network permission.
|
|
97
102
|
timeout.cancel()
|
|
98
103
|
finish(.denied)
|
|
99
104
|
} else {
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
case .cancelled:
|
|
104
|
-
break
|
|
105
|
-
default:
|
|
106
|
-
break
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
browser.start(queue: .main)
|
|
110
|
-
|
|
111
|
-
// --- Probe 2: UDP mDNS multicast (dialog trigger + secondary signal) ---
|
|
112
|
-
// Sending to 224.0.0.251:5353 triggers the system dialog on first launch.
|
|
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)")
|
|
121
|
-
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) {
|
|
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)")
|
|
125
108
|
timeout.cancel()
|
|
126
|
-
finish(.
|
|
109
|
+
finish(.unknown)
|
|
127
110
|
}
|
|
128
|
-
|
|
129
|
-
case .failed(let error):
|
|
130
|
-
print("🔍 LocalNetworkPermission UDP failed error code: \((error as NSError).code) — \(error.localizedDescription)")
|
|
131
|
-
if Self.isPolicyDenied(error) {
|
|
132
|
-
timeout.cancel()
|
|
133
|
-
finish(.denied)
|
|
134
|
-
}
|
|
135
|
-
// Non-policy UDP failure — let NWBrowser or timeout decide
|
|
111
|
+
|
|
136
112
|
case .cancelled:
|
|
137
|
-
break
|
|
113
|
+
break // Expected — we cancelled it ourselves in finish()
|
|
114
|
+
|
|
138
115
|
default:
|
|
139
|
-
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.
|
|
116
|
+
break // .setup, .waiting — keep waiting
|
|
142
117
|
}
|
|
143
118
|
}
|
|
144
|
-
|
|
119
|
+
|
|
120
|
+
listener.start(queue: .main)
|
|
145
121
|
}
|
|
146
122
|
|
|
147
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",
|