@cappitolian/http-local-server-swifter 0.0.19 → 0.0.20
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.
|
@@ -9,10 +9,12 @@ import UIKit
|
|
|
9
9
|
/// server.start() will always succeed regardless of permission status.
|
|
10
10
|
///
|
|
11
11
|
/// Detection strategy:
|
|
12
|
-
/// -
|
|
13
|
-
/// -
|
|
14
|
-
///
|
|
15
|
-
///
|
|
12
|
+
/// - Send a UDP probe to the mDNS multicast address (224.0.0.251:5353).
|
|
13
|
+
/// - This triggers the system permission dialog without requiring any
|
|
14
|
+
/// NSBonjourServices entry in Info.plist.
|
|
15
|
+
/// - NWBrowser was avoided because it requires the browsed service type
|
|
16
|
+
/// to be declared in NSBonjourServices, causing false NoAuth(-65555)
|
|
17
|
+
/// failures even when the permission is actually granted.
|
|
16
18
|
@objc public class LocalNetworkPermission: NSObject {
|
|
17
19
|
|
|
18
20
|
// MARK: - Types
|
|
@@ -25,76 +27,83 @@ import UIKit
|
|
|
25
27
|
|
|
26
28
|
// MARK: - Private state
|
|
27
29
|
|
|
28
|
-
private var
|
|
30
|
+
private var connection: NWConnection?
|
|
29
31
|
|
|
30
32
|
// MARK: - Permission check
|
|
31
33
|
|
|
32
|
-
///
|
|
33
|
-
///
|
|
34
|
+
/// Probes the local network to trigger the permission dialog and detect the result.
|
|
35
|
+
///
|
|
36
|
+
/// Sends a UDP packet to the mDNS multicast address. iOS intercepts this at the
|
|
37
|
+
/// network layer and either allows it (granted) or blocks it with a policy error (denied).
|
|
34
38
|
///
|
|
35
39
|
/// - Parameter completion: Called on the main thread with the permission status.
|
|
36
|
-
///
|
|
37
|
-
///
|
|
40
|
+
/// `.granted` / `.denied` once iOS responds, or `.unknown` on timeout
|
|
41
|
+
/// (simulator, permission already granted and cached by the OS, etc.).
|
|
38
42
|
///
|
|
39
|
-
/// - Note: iOS shows the dialog only once.
|
|
40
|
-
/// will return the previously stored decision immediately (no dialog shown).
|
|
43
|
+
/// - Note: iOS shows the dialog only once. Subsequent calls return the cached decision.
|
|
41
44
|
public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// mDNS multicast address — probing this triggers the Local Network permission dialog.
|
|
46
|
+
// This approach does NOT require NSBonjourServices in Info.plist.
|
|
47
|
+
let host = NWEndpoint.Host("224.0.0.251")
|
|
48
|
+
let port = NWEndpoint.Port(rawValue: 5353)!
|
|
44
49
|
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
using: parameters
|
|
48
|
-
)
|
|
49
|
-
self.browser = browser
|
|
50
|
+
let conn = NWConnection(host: host, port: port, using: .udp)
|
|
51
|
+
self.connection = conn
|
|
50
52
|
|
|
51
|
-
// Timeout fallback — if iOS doesn't respond in 3 seconds, assume unknown
|
|
52
|
-
// (e.g. on simulator or when the user has already granted permission)
|
|
53
53
|
var completed = false
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
// Timeout fallback — treat unresolved state as unknown and allow server startup.
|
|
56
|
+
// This covers: simulator, permission already granted (OS may skip the callback),
|
|
57
|
+
// and edge cases where iOS doesn't fire stateUpdateHandler promptly.
|
|
58
|
+
let timeout = DispatchWorkItem { [weak self] in
|
|
55
59
|
guard !completed else { return }
|
|
56
60
|
completed = true
|
|
57
|
-
|
|
61
|
+
self?.connection?.cancel()
|
|
62
|
+
self?.connection = nil
|
|
58
63
|
DispatchQueue.main.async { completion(.unknown) }
|
|
59
64
|
}
|
|
60
65
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: timeout)
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
conn.stateUpdateHandler = { [weak self] state in
|
|
63
68
|
guard !completed else { return }
|
|
64
69
|
|
|
65
70
|
switch state {
|
|
66
71
|
case .ready:
|
|
67
|
-
//
|
|
72
|
+
// UDP connection reached the network layer — permission is granted.
|
|
68
73
|
completed = true
|
|
69
74
|
timeout.cancel()
|
|
70
|
-
|
|
75
|
+
self?.connection?.cancel()
|
|
76
|
+
self?.connection = nil
|
|
71
77
|
DispatchQueue.main.async { completion(.granted) }
|
|
72
78
|
|
|
73
|
-
case .
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
case .waiting(let error):
|
|
80
|
+
// .waiting with a policy error = permission denied.
|
|
81
|
+
// Other .waiting errors (no route, offline) are transient — keep waiting.
|
|
82
|
+
if Self.isPolicyDenied(error) {
|
|
83
|
+
completed = true
|
|
84
|
+
timeout.cancel()
|
|
85
|
+
self?.connection?.cancel()
|
|
86
|
+
self?.connection = nil
|
|
87
|
+
DispatchQueue.main.async { completion(.denied) }
|
|
88
|
+
}
|
|
81
89
|
|
|
90
|
+
case .failed(let error):
|
|
91
|
+
let status: PermissionStatus = Self.isPolicyDenied(error) ? .denied : .unknown
|
|
82
92
|
completed = true
|
|
83
93
|
timeout.cancel()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
94
|
+
self?.connection?.cancel()
|
|
95
|
+
self?.connection = nil
|
|
96
|
+
DispatchQueue.main.async { completion(status) }
|
|
88
97
|
|
|
89
98
|
case .cancelled:
|
|
90
|
-
break // Expected
|
|
99
|
+
break // Expected — we cancelled it ourselves above
|
|
91
100
|
|
|
92
101
|
default:
|
|
93
|
-
break // .setup, .
|
|
102
|
+
break // .setup, .preparing — keep waiting
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
conn.start(queue: .main)
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
// MARK: - Recovery
|
|
@@ -111,4 +120,19 @@ import UIKit
|
|
|
111
120
|
UIApplication.shared.open(url)
|
|
112
121
|
}
|
|
113
122
|
}
|
|
123
|
+
|
|
124
|
+
// MARK: - Private helpers
|
|
125
|
+
|
|
126
|
+
/// Returns true if the NWError indicates a Local Network policy denial.
|
|
127
|
+
///
|
|
128
|
+
/// Known denial error codes:
|
|
129
|
+
/// - kDNSServiceErr_PolicyDenied = -65570
|
|
130
|
+
/// - kDNSServiceErr_NoAuth = -65555
|
|
131
|
+
private static func isPolicyDenied(_ error: NWError) -> Bool {
|
|
132
|
+
let nsError = error as NSError
|
|
133
|
+
if nsError.code == -65570 || nsError.code == -65555 { return true }
|
|
134
|
+
|
|
135
|
+
let desc = error.localizedDescription.lowercased()
|
|
136
|
+
return desc.contains("policy") || desc.contains("denied") || desc.contains("noauth")
|
|
137
|
+
}
|
|
114
138
|
}
|
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.20",
|
|
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",
|