@cappitolian/http-local-server-swifter 0.0.18 → 0.0.19
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.
|
@@ -25,45 +25,49 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
@objc public func connect(_ call: CAPPluginCall) {
|
|
28
|
-
//
|
|
29
|
-
// iOS only shows the dialog once
|
|
30
|
-
// We
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
28
|
+
// Check Local Network permission first via NWBrowser callback.
|
|
29
|
+
// iOS only shows the system dialog once — subsequent calls return the stored decision.
|
|
30
|
+
// We must do this BEFORE starting the server because Swifter uses plain TCP sockets
|
|
31
|
+
// which iOS never blocks — the denial only surfaces through Bonjour/mDNS errors,
|
|
32
|
+
// not through server.start() failures.
|
|
33
|
+
networkPermission.checkPermission { [weak self] status in
|
|
35
34
|
guard let self = self else { return }
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Use middleware to catch ALL requests and avoid route misses.
|
|
42
|
-
server.middleware.append { [weak self] request in
|
|
43
|
-
if request.method == "OPTIONS" {
|
|
44
|
-
return self?.corsResponse() ?? .raw(204, "No Content", nil, nil)
|
|
45
|
-
}
|
|
46
|
-
return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
|
|
36
|
+
if status == .denied {
|
|
37
|
+
print("❌ SWIFTER: Local Network permission denied by user")
|
|
38
|
+
call.reject("LOCAL_NETWORK_PERMISSION_DENIED")
|
|
39
|
+
return
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
// .granted or .unknown — proceed with server startup.
|
|
43
|
+
// .unknown means iOS didn't respond within the timeout (simulator, already granted, etc.)
|
|
44
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
45
|
+
guard let self = self else { return }
|
|
46
|
+
|
|
47
|
+
self.disconnect()
|
|
48
|
+
let server = HttpServer()
|
|
49
|
+
self.webServer = server
|
|
50
|
+
|
|
51
|
+
// Use middleware to catch ALL requests and avoid route misses.
|
|
52
|
+
server.middleware.append { [weak self] request in
|
|
53
|
+
if request.method == "OPTIONS" {
|
|
54
|
+
return self?.corsResponse() ?? .raw(204, "No Content", nil, nil)
|
|
55
|
+
}
|
|
56
|
+
return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
|
|
57
|
+
}
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
do {
|
|
60
|
+
try server.start(self.defaultPort, forceIPv4: true)
|
|
61
|
+
let ip = Self.getWiFiAddress() ?? "127.0.0.1"
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
"ip": ip,
|
|
57
|
-
"port": Int(self.defaultPort)
|
|
58
|
-
])
|
|
59
|
-
} catch {
|
|
60
|
-
print("❌ SWIFTER ERROR: \(error)")
|
|
63
|
+
print("🚀 SWIFTER: Server running on http://\(ip):\(self.defaultPort)")
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
65
|
+
call.resolve([
|
|
66
|
+
"ip": ip,
|
|
67
|
+
"port": Int(self.defaultPort)
|
|
68
|
+
])
|
|
69
|
+
} catch {
|
|
70
|
+
print("❌ SWIFTER ERROR: \(error)")
|
|
67
71
|
call.reject("Could not start server")
|
|
68
72
|
}
|
|
69
73
|
}
|
|
@@ -4,26 +4,41 @@ import UIKit
|
|
|
4
4
|
|
|
5
5
|
/// Handles Local Network permission lifecycle on iOS.
|
|
6
6
|
///
|
|
7
|
-
/// iOS
|
|
8
|
-
///
|
|
9
|
-
///
|
|
10
|
-
///
|
|
11
|
-
///
|
|
7
|
+
/// IMPORTANT: iOS Local Network permission does NOT block TCP sockets.
|
|
8
|
+
/// It only blocks mDNS/Bonjour service discovery. This means Swifter's
|
|
9
|
+
/// server.start() will always succeed regardless of permission status.
|
|
10
|
+
///
|
|
11
|
+
/// Detection strategy:
|
|
12
|
+
/// - Trigger the system dialog by starting a NWBrowser.
|
|
13
|
+
/// - Listen to the browser's state change callback to detect denial.
|
|
14
|
+
/// - Surface the result via a completion handler so the plugin can
|
|
15
|
+
/// reject the Capacitor call with LOCAL_NETWORK_PERMISSION_DENIED.
|
|
12
16
|
@objc public class LocalNetworkPermission: NSObject {
|
|
13
17
|
|
|
14
|
-
// MARK: -
|
|
18
|
+
// MARK: - Types
|
|
19
|
+
|
|
20
|
+
public enum PermissionStatus {
|
|
21
|
+
case granted
|
|
22
|
+
case denied
|
|
23
|
+
case unknown
|
|
24
|
+
}
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
private static let policyDeniedErrorCode = -65570
|
|
26
|
+
// MARK: - Private state
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
private var browser: NWBrowser?
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
// MARK: - Permission check
|
|
31
|
+
|
|
32
|
+
/// Triggers the Local Network permission dialog and returns the result
|
|
33
|
+
/// asynchronously via the completion handler.
|
|
34
|
+
///
|
|
35
|
+
/// - Parameter completion: Called on the main thread with the permission status.
|
|
36
|
+
/// Will be called with `.granted` or `.denied` once iOS responds,
|
|
37
|
+
/// or `.unknown` if the result could not be determined within the timeout.
|
|
22
38
|
///
|
|
23
|
-
/// -
|
|
24
|
-
///
|
|
25
|
-
|
|
26
|
-
public func requestPermissionIfNeeded() {
|
|
39
|
+
/// - Note: iOS shows the dialog only once. On subsequent calls this method
|
|
40
|
+
/// will return the previously stored decision immediately (no dialog shown).
|
|
41
|
+
public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
|
|
27
42
|
let parameters = NWParameters()
|
|
28
43
|
parameters.includePeerToPeer = true
|
|
29
44
|
|
|
@@ -31,42 +46,62 @@ import UIKit
|
|
|
31
46
|
for: .bonjour(type: "_http._tcp", domain: "local."),
|
|
32
47
|
using: parameters
|
|
33
48
|
)
|
|
49
|
+
self.browser = browser
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
var completed = false
|
|
54
|
+
let timeout = DispatchWorkItem {
|
|
55
|
+
guard !completed else { return }
|
|
56
|
+
completed = true
|
|
39
57
|
browser.cancel()
|
|
58
|
+
DispatchQueue.main.async { completion(.unknown) }
|
|
40
59
|
}
|
|
41
|
-
|
|
60
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: timeout)
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
browser.stateUpdateHandler = { state in
|
|
63
|
+
guard !completed else { return }
|
|
44
64
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
switch state {
|
|
66
|
+
case .ready:
|
|
67
|
+
// Browser started successfully — permission is granted
|
|
68
|
+
completed = true
|
|
69
|
+
timeout.cancel()
|
|
70
|
+
browser.cancel()
|
|
71
|
+
DispatchQueue.main.async { completion(.granted) }
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
case .failed(let error):
|
|
74
|
+
// Check for policy denial errors:
|
|
75
|
+
// kDNSServiceErr_PolicyDenied = -65570
|
|
76
|
+
// kDNSServiceErr_NoAuth = -65555
|
|
77
|
+
let nsError = error as NSError
|
|
78
|
+
let isDenied = nsError.code == -65570
|
|
79
|
+
|| nsError.code == -65555
|
|
80
|
+
|| error.localizedDescription.lowercased().contains("policy")
|
|
81
|
+
|
|
82
|
+
completed = true
|
|
83
|
+
timeout.cancel()
|
|
84
|
+
browser.cancel()
|
|
85
|
+
DispatchQueue.main.async {
|
|
86
|
+
completion(isDenied ? .denied : .unknown)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case .cancelled:
|
|
90
|
+
break // Expected when we cancel it ourselves
|
|
91
|
+
|
|
92
|
+
default:
|
|
93
|
+
break // .setup, .waiting — keep waiting
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
browser.start(queue: .main)
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
// MARK: - Recovery
|
|
65
101
|
|
|
66
102
|
/// Opens the app's page in iOS Settings so the user can manually grant Local Network access.
|
|
67
103
|
///
|
|
68
|
-
/// - Note: This is the only recovery path available
|
|
69
|
-
/// after the user denies the permission.
|
|
104
|
+
/// - Note: This is the only recovery path available after the user denies the permission.
|
|
70
105
|
/// - Warning: On iOS 17, the user may need to restart the device after granting
|
|
71
106
|
/// the permission for it to take effect. This is a known iOS 17 bug.
|
|
72
107
|
public func openAppSettings() {
|
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.19",
|
|
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",
|