I did ask same question on Apple dev portal and there is other people with the same problem.
I have created simple reproducible project on GitHub: (follow steps in README)
https://github.com/ChoadPet/NWListenerTest.git
I have screen and on present ConnectionListener is initialized and on dismiss it deinitialized (called stopListening()).
First time when open the screen everything is ok:
Listener stateUpdateHandler: waiting(POSIXErrorCode: Network is down)
Listener stateUpdateHandler: ready
"📞 New connection: 10.0.1.2:50655 establish"
"Connection stateUpdateHandler: preparing"
"Connection stateUpdateHandler: ready"
but for next n-tries only this messages:
[] nw_path_evaluator_evaluate NECP_CLIENT_ACTION_ADD error [48: Address already in use]
[] nw_path_create_evaluator_for_listener nw_path_evaluator_evaluate failed
[] nw_listener_start_locked [L3] nw_path_create_evaluator_for_listener failed
Listener stateUpdateHandler: waiting(POSIXErrorCode: Network is down)
Listener stateUpdateHandler: failed(POSIXErrorCode: Address already in use)
It happens on iPhone 6 iOS 12.4.1, iPhone Xs Max iOS 13.3, iPhone 11 Pro iOS 13.5.1(also iOS 13.6)
but NOT on iPhone 7 Plus iOS 12.1.4, iPhone 11 iOS 13.5.1.
Here is my code for listening inbound connection:
final class ConnectionListener {
var dataReceivedHandler: ((Data) -> Void)?
private let port: UInt16
private let maxLength: Int
private var listener: NWListener!
private var connection: NWConnection!
init(port: UInt16, maxLength: Int) {
self.port = port
self.maxLength = maxLength
}
deinit {
print("❌ Deinitialize \(self)")
}
// MARK: Public API
func startListening() {
let parameters = NWParameters.tcp
parameters.allowLocalEndpointReuse = true
self.listener = try! NWListener(using: parameters, on: NWEndpoint.Port(integerLiteral: port))
self.listener.stateUpdateHandler = { state in print("Listener stateUpdateHandler: \(state)") }
self.listener.newConnectionHandler = { [weak self] in self?.establishNewConnection($0) }
self.listener.start(queue: .main)
}
func stopListening() {
listener.cancel()
connection?.cancel()
}
// MARK: Private API
private func establishNewConnection(_ newConnection: NWConnection) {
connection = newConnection
debugPrint("📞 New connection: \(String(describing: connection.endpoint)) establish")
connection.stateUpdateHandler = { [weak self] state in
guard let self = self else { return }
debugPrint("Connection stateUpdateHandler: \(state)")
switch state {
case .ready:
debugPrint("Connection: start receiving ✅")
self.receive(on: self.connection)
default: break
}
}
self.connection.start(queue: .main)
}
private func receive(on connection: NWConnection) {
connection.receive(minimumIncompleteLength: 1, maximumLength: maxLength, completion: { [weak self] content, context, isCompleted, error in
guard let self = self else { return }
if let frame = content {
self.dataReceivedHandler?(frame)
}
self.receive(on: connection)
})
}
}
If there is more information, which you need, let me know.
Thanks!
After your app "fails", click STOP, wait 2 minutes, and try START again. I assure you, it will work normally again. I tried with your code, which also "failed" initially for me.
What's going on: you're running into a TCP close timeout. Imagine it as a postal service where you are the receiver. If the postal service says "we have stopped deliveries to you", you can remove whole mailbox immediately and there cannot be any delivery failures. This is an equivalent of THE CLIENT closing the TCP connection. But you tell the postal service that you accept deliveries, you actually have one already received, and you decide to move out. This is an equivalent of server closing the TCP connection. The mailbox (your iOS app process) is still there, you just don't want to receive anymore. In real world, you should ensure at least some more time of having your name on the mailbox, before everyone (in reasonable frame) notices. In TCP, this is called TIME_WAIT.
TCP was not constructed primarily for the second scenario, but anyway it is trying to do its best to avoid lost packets, which just were delivered out of band (later than other packets) due to TCP routing. Particular wait length is specified in units of TCP stack parameters of the particular Operation System mark and version. So it may be shorter on some, and longer on others. It should not be more than 2 minutes.
Think over your use case. What are you trying to achieve by closing whole server, while the app is still running?
clients should close, not the server
server can cancel just single connections, not the whole listener
server can reject new connections while it's still running
Related
Problem: I fail subscribing to my publisher implemented in swift as soon as the publisher has published its first message.
Goal: Publish a data stream over ZeroMQ from my swift app. Then connect and disconnect a few subscribers and get messages through.
Background: I use swift5 and SwiftyZeroMQ5 (I have tested SwiftyZeroMQ too), I deploy on iPhone. I try to subscribe from both swift and python3. It only works if I connect my subscriber prior to publishing the first message. It also works if I first connect my subscriber then start the publisher app, then publishes. Corresponding publish and subscribe code on python3 does not require launching in any specific order and represents the behaviour I want.
Since I get the sub/pub to work if I start i specific order, I know that IP-numbers, ports and topic, formatting etc are correct.
Note that the behaviour is the same when I subscribe from both python3 and swift - it is not compatibility issues between swift and python.
Error messages: No error messages, the poller simply does not trigger if the processes are not started in the described order. The poller do trigger and messages and received if the processes are started in the described order.
What I tried: I've tried different combinations of publishers and subscribers regarding code base and devices.
Swift pub to swift sub on same device [only works in described order]
Swift pub to swift sub on different devices [only works in described order]
Swift pub to python3 sub [only works in described order]
Python3 pub to swift sub [works independent of start order]
Python3 pub to python3 sub [works independent of start order]
My conclusion: There is a problem in the swift publisher socket: it fails to recognise new subscribers after it has published its first message.
Swift code for publisher, initPublisher is called in viewDidLoad(). ZeroMQ library version is 4.2.2:
import SwiftyZeroMQ5
var context: SwiftyZeroMQ.Context = try! SwiftyZeroMQ.Context()
var gpsPublisher: SwiftyZeroMQ.Socket?
let gpsPublishEndPoint = "tcp://*:5560"
// Init the publisher socket
func initPublisher()->Bool{
do{
self.gpsPublisher = try context.socket(.publish)
try self.gpsPublisher?.setSendBufferSize(4096)
try self.gpsPublisher?.setLinger(0) // Dont buffer messages
try self.gpsPublisher?.bind(self.gpsPublishEndPoint)
return true
}
catch{
print("Publish setup failed!")
return false
}
}
// ZMQ publish. Publishes string and serialized json-object
func publish(socket: SwiftyZeroMQ.Socket?, topic: String, json: JSON)->Bool{
// Create string with topic and json representation
let publishStr = getJsonStringAndTopic(topic: topic, json: json)
do{
try socket?.send(string: publishStr)
print("publish: Published: " + publishStr)
return true
}
catch{
print("publish: Error, tried to publish, but failed: " + publishStr)
return false
}
}
//The function is repeatedly called in a thread. Only function call shown here below.
_ = self.publish(socket: self.gpsPublisher, topic: "myTopic", json: json)
The python3 subscriber code,
zmq.zmq_version() -> '4.3.2':
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://192.168.1.2:5560")
socket.setsockopt_string(zmq.SUBSCRIBE, 'myTopic')
socket.RCVTIMEO = 1000 # in milliseconds
while socket:
try:
msg = str(socket.recv(), 'utf-8')
(topic, msg) = auxiliaries.zmq.demogrify(msg)
_print((topic, msg))
except zmq.error.Again as error:
_print(str(error))
except KeyboardInterrupt:
auxiliaries.zmq.close_socket_gracefully(socket)
socket = None
Any help, interesting test setups etc is very much appreciated.
I did some more testing and found out a few things and a workaround.
The code works as intended when running on an iPhone simulator (simulator is x86_64 architecture, iPhone8 is armv7)
I don't think it is related, but I did find it interesting. Certain multicast and broadcast protocols require an approval from Apple. You can run without the approval on simulators, but not in devices. Apple networking multicast entitlement. Since it partially works I did rule this out.
The workaround is to bind the socket again prior to each publish. This throws the error "Address already in use", butt seem to not do much harm.
Without unreasonable delays in between publish messages, the pub-sub fails after 500-1000 messages if the publish socket is not binded again when running from the iPhone.
I made a minimal working example app that you can play around with if you want to, or need to dig deeper. It has buttons for "init", "bind", "publish" and "bind and publish". You can send a batch of messages and check timings etc.
I run the app towards a python3 script.
I included SwiftyZeroMQ5 via cocoapods.
Podfile:
platform :ios, '13.0'
target 'ZMQtest' do
use_frameworks!
pod 'SwiftyZeroMQ5'
end
SwiftCode (set up the buttons your self..)
import UIKit
import SwiftyZeroMQ5
class ViewController: UIViewController {
let context = try! SwiftyZeroMQ.Context()
var publisher: SwiftyZeroMQ.Socket?
let publishEndPoint = "tcp://*:5560"
var cnt = 0
let quota = 1200
#IBAction func publishButtonPressed(_ sender: Any) {
while cnt < quota {
publish()
//usleep(10000)
}
cnt = 1
}
#IBAction func bindAndPublishButtonPressed(_ sender: Any) {
while cnt < quota {
bindPublisher()
publish()
}
cnt = 1
}
#IBAction func initPubButtonPressed(_ sender: Any) {
initPublisher()
}
#IBAction func bindPublisherButtonPressed(_ sender: Any) {
bindPublisher()
}
#IBOutlet weak var statusLabel: UILabel!
// **************
// Init publisher
func initPublisher(){
do {
self.publisher = try context.socket(.publish)
print("Publisher socket created")
}
catch {
print("initPublisher error: ", error)
}
}
// **************
// Bind publisher
func bindPublisher(){
do {
try self.publisher?.bind(publishEndPoint)
print("Publisher socket binded to :", publishEndPoint)
}
catch {
print("bindPublisher error: ", error)
}
}
// *****************
// Publish a message
func publish(){
// Publish dummy string
do{
cnt += 1
let str = "topic {\"key\": \"" + String(cnt) + "\"}"
try self.publisher?.send(string: str)
statusLabel.text = str
print("Publish message no: ", String(cnt))
}
catch{
print("publisher error: ", error)
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
And the python3 code:
#!/usr/bin/env python3
'''Minimal running example of a ZMQ SUB socket.'''
import json
import zmq
def demogrify(msg: str):
'''inverse of mogrify()'''
try:
(topic, message) = msg.split(maxsplit=1)
except ValueError:
(topic, message) = (msg, '{}')
return topic, json.loads(message)
def close_socket_gracefully(socket):
'''graceful termination'''
socket.setsockopt(zmq.LINGER, 0) # to avoid hanging infinitely
socket.close()
if __name__ == "__main__":
context = zmq.Context()
socket = context.socket(zmq.SUB) #pylint: disable=no-member
socket.connect("tcp://192.168.1.2:5560")
socket.setsockopt_string(zmq.SUBSCRIBE, '') #pylint: disable=no-member
socket.RCVTIMEO = 1000 # in milliseconds
while socket:
try:
msg = str(socket.recv(), 'utf-8')
(topic, msg) = demogrify(msg)
print((topic, msg))
except zmq.error.Again as error:
print(str(error))
except KeyboardInterrupt:
close_socket_gracefully(socket)
socket = None
Should I mark this issue as solved or not?
I'm using the Network Extension framework provided by Apple to build a packet sniffing/monitoring application similar to Charles Proxy and Surge 4 for iOS.
So far, I have the basic structure of the project up and running with the Main Application triggering the PacketTunnelProvider Extension where I can see packets being forwarded via the packetFlow.readPackets(completionHandler:) method. My background isn't in networking so I'm confused on the basic structure of these kinds of apps. Do they host a server on the device that act as the proxy which intercepts network requests? Could anyone provide a diagram of the general flow of the network requests? I.e. what is the relationship between the Packet Tunnel Provider, Proxy Server, Virtual Interface, and Tunnel?
If these apps do use a local on-device server, how do you configure the NEPacketTunnelNetworkSettings to allow for a connection? I have tried incorporating a local on-device server such as GCDWebServer with no luck in establishing a link between the two.
For example, if the GCDWebServer was reachable at 192.168.1.231:8080, how would I change the code below for the client to communicate with the server?
Main App:
let proxyServer = NEProxyServer(address: "192.168.1.231", port: 8080)
let proxySettings = NEProxySettings()
proxySettings.exceptionList = []
proxySettings.httpEnabled = true
proxySettings.httpServer = proxyServer
let providerProtocol = NETunnelProviderProtocol()
providerProtocol.providerBundleIdentifier = self.tunnelBundleId
providerProtocol.serverAddress = "My Server"
providerProtocol.providerConfiguration = [:]
providerProtocol.proxySettings = proxySettings
let newManager = NETunnelProviderManager()
newManager.localizedDescription = "Custom VPN"
newManager.protocolConfiguration = providerProtocol
newManager.isEnabled = true
saveLoadManager()
self.vpnManager = newManager
PacketTunnelProviderExtension:
func startTunnel(options: [String : NSObject]?, completionHandler: #escaping (Error?) -> Void) {
...
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.143")
settings.ipv4Settings = NEIPv4Settings(addresses: ["198.17.203.2"], subnetMasks: ["255.255.255.255"])
settings.ipv4Settings?.includedRoutes = [NEIPv4Route.default()]
settings.ipv4Settings?.excludedRoutes = []
settings.dnsSettings = NEDNSSettings(servers: ["8.8.8.8", "8.8.4.4"])
settings.dnsSettings?.matchDomains = [""]
self.setTunnelNetworkSettings(settings) { error in
if let e = error {
NSLog("Settings error %#", e.localizedDescription)
} else {
completionHandler(error)
self.readPackets()
}
}
...
}
I'm working on the iOS version of Proxyman and my experience can help you:
Do they host a server on the device that acts as the proxy which intercepts network requests?
Yes, you have to start a Listener on the Network Extension (not the main app) to act as a Proxy Server. You can write a simple Proxy Server by using Swift NIO or CocoaAsyncSocket.
To intercept the HTTPS traffic, it's a quite big challenge, but I won't mention here since it's out of the scope.
Could anyone provide a diagram of the general flow of the network requests?
As the Network Extension and the Main app are two different processes, so they couldn't communicate directly like normal apps.
Thus, the flow may look like:
The Internet -> iPhone -> Your Network Extension (VPN) -> Forward to your Local Proxy Server -> Intercept or monitor -> Save to a local database (in Shared Container Group) -> Forward again to the destination server.
From the main app, you can receive the data by reading the local database.
how do you configure the NEPacketTunnelNetworkSettings to allow for a connection?
In the Network extension, let start a Proxy Server at Host:Port, then init the NetworkSetting, like the sample:
private func initTunnelSettings(proxyHost: String, proxyPort: Int) -> NEPacketTunnelNetworkSettings {
let settings: NEPacketTunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
/* proxy settings */
let proxySettings: NEProxySettings = NEProxySettings()
proxySettings.httpServer = NEProxyServer(
address: proxyHost,
port: proxyPort
)
proxySettings.httpsServer = NEProxyServer(
address: proxyHost,
port: proxyPort
)
proxySettings.autoProxyConfigurationEnabled = false
proxySettings.httpEnabled = true
proxySettings.httpsEnabled = true
proxySettings.excludeSimpleHostnames = true
proxySettings.exceptionList = [
"192.168.0.0/16",
"10.0.0.0/8",
"172.16.0.0/12",
"127.0.0.1",
"localhost",
"*.local"
]
settings.proxySettings = proxySettings
/* ipv4 settings */
let ipv4Settings: NEIPv4Settings = NEIPv4Settings(
addresses: [settings.tunnelRemoteAddress],
subnetMasks: ["255.255.255.255"]
)
ipv4Settings.includedRoutes = [NEIPv4Route.default()]
ipv4Settings.excludedRoutes = [
NEIPv4Route(destinationAddress: "192.168.0.0", subnetMask: "255.255.0.0"),
NEIPv4Route(destinationAddress: "10.0.0.0", subnetMask: "255.0.0.0"),
NEIPv4Route(destinationAddress: "172.16.0.0", subnetMask: "255.240.0.0")
]
settings.ipv4Settings = ipv4Settings
/* MTU */
settings.mtu = 1500
return settings
}
Then start a VPN,
let networkSettings = initTunnelSettings(proxyHost: ip, proxyPort: port)
// Start
setTunnelNetworkSettings(networkSettings) { // Handle success }
Then forward the package to your local proxy server:
let endpoint = NWHostEndpoint(hostname: proxyIP, port: proxyPort)
self.connection = self.createTCPConnection(to: endpoint, enableTLS: false, tlsParameters: nil, delegate: nil)
packetFlow.readPackets {[weak self] (packets, protocols) in
guard let strongSelf = self else { return }
for packet in packets {
strongSelf.connection.write(packet, completionHandler: { (error) in
})
}
// Repeat
strongSelf.readPackets()
}
From that, your local server can receive the packages then forwarding to the destination server.
I would like to inspect packets with NEPacketTunnelProvider without a specified proxy. Unfortunately my readPacketObjects completion handler is never being called and I don't understand why. My regular internet connection stops and Wireshark shows nothing on the new interface obviously and I don't see any of my HTTP connection on en0 either.
Is this possible? Apple Developer Forums suggests it might not be... but Charles Proxy for iOS seems to be able to inspect everything.
My code is below:
class TunnelProvider: NEPacketTunnelProvider {
override func startTunnel(options: [String : NSObject]? = nil, completionHandler: #escaping (Error?) -> Void) {
os_log("starting tunnel")
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
settings.tunnelOverheadBytes = 80
settings.mtu = 1200
settings.ipv4Settings = NEIPv4Settings(addresses: ["127.0.0.1", "0.0.0.0"], subnetMasks: ["0.0.0.0", "0.0.0.0"]) // all addresses
settings.ipv4Settings?.includedRoutes = [NEIPv4Route.default()] // all routes
settings.ipv4Settings?.excludedRoutes = [NEIPv4Route(destinationAddress: "127.0.0.1", subnetMask: "255.255.255.255")] // avoid local routes
settings.dnsSettings = NEDNSSettings(servers: ["1.1.1.1", "8.8.8.8", "8.8.4.4"])
settings.dnsSettings?.matchDomains = [""] // if blank don't use this DNS, use system; if "" then use this
NSLog("default/included route %#:%#", NEIPv4Route.default().destinationAddress, NEIPv4Route.default().destinationSubnetMask)
self.setTunnelNetworkSettings(settings) { error in
if let e = error {
NSLog("Settings error %#", e.localizedDescription)
completionHandler(e)
} else {
os_log("Settings set without error")
completionHandler(nil)
}
}
self.readPacketObjects()
}
private func readPacketObjects() {
NSLog("Inside readPacketObjects %#", self.packetFlow)
self.packetFlow.readPacketObjects() { packets in
NSLog("Inside readPacketObjects completionHandler %#", packets)
self.packetFlow.writePacketObjects(packets)
self.readPacketObjects()
}
}
Console output
default 10:31:03.177424 -0400 tunnel starting tunnel
default 10:31:03.177796 -0400 tunnel default/included route 0.0.0.0:0.0.0.0
default 10:31:03.178602 -0400 tunnel Inside readPacketObjects <NEPacketTunnelFlow: 0x7f9a69d135e0>
default 10:31:03.356006 -0400 tunnel Settings set without error
Edit 1
I am able to read packets after modifying settings.ipv4Settings to a local address with a more specific subnet mask. However the internet connection is still down and route get outputs bad address.
Edit 2
I was able to get DNS to work by adding the DNS servers to the excludedRoutes as seen here. Writing to the packetFlow with no modifications still doesn't enable full routing tho.
Issue Description
We have hosted Parse server on Azure App Services. We are able to connect to live Query and able to Subscribe to it. We are getting below messages.
2019-01-01 19:03:26.917094+0530 LiveQueryTest[59625:972922] ParseLiveQuery: Sending message:
{"clientKey":"xxxxxx","op":"connect","sessionToken":"","applicationId":"xxxxxx"}
2019-01-01 19:03:27.285251+0530 LiveQueryTest[59625:972922] ParseLiveQuery: Received message: {"op":"connected","clientId":5}
2019-01-01 19:03:27.388337+0530 LiveQueryTest[59625:972922] ParseLiveQuery: Sending message: {"query":{"className":"PostQuestionMessage","where":{"type":2}},"requestId":1,"op":"subscribe"}
2019-01-01 19:03:27.600455+0530 LiveQueryTest[59625:972813] ParseLiveQuery: Received message: {"op":"subscribed","clientId":5,"requestId":1}
And we are subscribed to Update Event but when we updated any records we are not getting event Back.
On the server:
We tried to listen to Specific port "1337" as below: Every time we do that our Parse Dashboard and API stops working. So Is it necessary to listen to specific port i.e "1337" or var port = process.env.PORT || 1337; will also work for Live Query.
var httpServer = require('http').createServer(app);
var port = 1337;
httpServer.listen(port, function() {
console.log('Parse Server running at ${port}');
console.log("The value of port is " + port);
});
ParseServer.createLiveQueryServer(httpServer);
Expected Results
We should get the event update as per our subscription.
Actual Outcome
Not receiving event updates as per the Query Subscribed,
Environment Setup
Server
parse-server version (Be specific! Don't say 'latest'.) : [2.3.8]
Operating System: [Linux]
[Remote Server - Azure]
Database
MongoDB version: [3.4]
[Remote Server - Azure]
Logs/Trace
Server:
[32minfo�[39m: Parse LiveQuery Server starts running
�[32minfo�[39m: Create new client: 1
iOS client Code:
import UIKit
import Parse
import ParseLiveQuery
class LiveQueryManager {
static let shared = LiveQueryManager()
var liveQueryClient = ParseLiveQuery.Client()
var subscription: Subscription<PostQuestionMessage>?
var messagesQuery: PFQuery<PostQuestionMessage> {
return (PostQuestionMessage.query()?
.whereKey("type", equalTo: 2)) as! PFQuery<PostQuestionMessage>
}
init(){
liveQueryClient.shouldPrintWebSocketLog = true
liveQueryClient.shouldPrintWebSocketTrace = true
}
fileprivate func printMessage(_ message: PostQuestionMessage) {
let createdAt = message.createdAt ?? Date()
print("\(createdAt) : \(message.message ?? "")")
}
func subscribeToUpdates() {
//https://stackoverflow.com/questions/44273455/parse-cloud-livequeries-ios-client-doesnt-work
self.subscription = self.liveQueryClient.subscribe(messagesQuery)
self.subscription = self.subscription?.handleEvent({ (_, event) in
switch event {
case .created(let object):
print("Event Created")
self.printMessage(object)
// do stuff
case .updated(let object):
print("Event Updated")
self.printMessage(object)
default:
break // do other stuff or do nothing
}
})
}
func disconnectFromSubscription() {
if let objSubcription = self.subscription {
self.liveQueryClient.unsubscribe(messagesQuery, handler: objSubcription)
}
}
func printSubscription () {
if let objSubscription = self.subscription {
print ("Subscription:\(objSubscription)")
print ("Client:\(self.liveQueryClient)")
print ("Client userDisconnected:\(self.liveQueryClient.userDisconnected)")
}
}
}
(UPDATE added at end)
I have a native iOS MobileFirst (7.0) client written in Swift. The MobileFirst Server is behind a firewall and accessed though a junction on IBM Security Access Manager for Web (ISAM). ISAM is being used for adapter authentication. I've already tested the app without ISAM in the middle (no authentication), and it works fine.
A custom challenge handler is registered:
let myCH = MyChallengeHandler(vc: self)
WLClient.sharedInstance().registerChallengeHandler(myCH)
MyChallengeHandler sets the realm in its init() function:
init(vc: LoginViewController){
self.vc = vc
super.init(realm: "HeaderAuthRealm")
}
The app first connects to the server using wlConnectWithDelegate:
WLClient.sharedInstance().wlConnectWithDelegate(ConnectListener(vc: self))
And once the connection is made, it should call a adapter method on the server to look up the user info (using invokeProcedure):
let invocationData = WLProcedureInvocationData(adapterName: "login", procedureName: "lookUpRole")
invocationData.parameters = [userid]
WLClient.sharedInstance().invokeProcedure(invocationData, withDelegate: LoginListener(vc: self))
However it's not getting that far.
When ISAM is involved, it is protecting everything, include the connect URL, so the challenge handler is first getting called when the wlConnectWithDelegate() is attempted because ISAM returns a login page.
The challenge handler is properly detecting the login page and the handleChallenge() function is being called. Userid/password is collected from the user if necessary, and then it calls a function that calls submitLoginForm(). The custom onConnect() and onFailure() functions are defined in the challenge handler as well:
override func handleChallenge(response: WLResponse!)
{
handleChallengeISAM(response)
}
func handleChallengeISAM(response: WLResponse!)
{
//HPDIA0200W Authentication failed. You have used an invalid user name, password or client certificate.
let failedLogin = response.responseText.rangeOfString("HPDIA0200W") != nil
if vc.security.authDataSet && !failedLogin
{
println("=========== Sending login data directly")
submitISAMAuthData()
}
else
{
println("=========== A login screen form should appear")
if failedLogin {
needCredentials("Please check your credentials.")
} else {
needCredentials(nil)
}
}
}
func submitISAMAuthData()
{
let loginPostUrl = "https://wstest.clearlake.ibm.com/pkmslogin.form"
let params = ["username" : vc.security.userid , "password" : vc.security.password, "login-form-type" : "pwd"]
println("=========== Sending submitLoginForm request")
self.submitLoginForm( loginPostUrl, requestParameters: params, requestHeaders: nil, requestTimeoutInMilliSeconds: -1, requestMethod: nil)
println("=========== submitLoginForm request Sent")
}
override func onSuccess(response: WLResponse!)
{
println("=========== onSuccess")
let isLoginResponse = isCustomResponse(response)
if isLoginResponse {
println("=========== RE-challenged")
handleChallenge(response)
} else {
println("=========== Challenge success")
submitSuccess(response)
}
}
override func onFailure(response: WLFailResponse!)
{
println("=========== Challenge failure")
println("\(response.errorMsg)")
submitFailure(response)
}
The problem is that is as far as it gets. The request never gets to the ISAM device, and the onSuccess() or onFailure() functions are never called. The iOS simulator log states that the request was made, but that's it. No indication that it actually did anything.
=========== Sending submitLoginForm request
2015-04-09 15:00:12.866 ThirdPartyCompliance[54200:2903010] [DEBUG] [WL_AFHTTPCLIENTWRAPPER_PACKAGE] +[WLAFHTTPClientWrapper requestWithURL:] in WLAFHTTPClientWrapper.m:46 :: Request url is https://wstest.clearlake.ibm.com/pkmslogin.form
2015-04-09 15:00:12.871 ThirdPartyCompliance[54200:2903010] [DEBUG] [WL_REQUEST] -[WLRequest sendRequest:path:withOptions:] in WLRequest.m:141 :: Request timeout is 10.000000
2015-04-09 15:00:12.876 ThirdPartyCompliance[54200:2903010] [DEBUG] [WL_REQUEST] -[WLRequest sendRequest:path:withOptions:] in WLRequest.m:220 :: Sending request (https://wstest.clearlake.ibm.com/pkmslogin.form) with headers:
{
"Accept-Language" = en;
"User-Agent" = "ThirdPartyCompliance/1 (iPad Simulator; iOS 8.2; Scale/2.00)/WLNativeAPI/7.0.0.0";
"X-Requested-With" = XMLHttpRequest;
"x-wl-app-version" = "1.0";
"x-wl-device-id" = "C1CFD648-C648-439C-AC9F-8292FDAC20E6";
"x-wl-platform-version" = "7.0.0.0";
}
You can see the request body in the Analytics platform logs.
=========== submitLoginForm request Sent
The ISAM logs don't show the submitLoginForm request ever being sent despite what the iOS MobileFirst API logs say. Is something wrong with submitLoginForm() in v7.0?
UPDATE:
It seems that WLClient.sharedInstance().wlConnectWithDelegate() has to succeed before ChallengeHandler.submitLoginForm() will work. I verified this by using a proxy server in front of the ISAM device to send adapter service requests through ISAM, and all other MobileFirst server connections bypass ISAM. In this architecture, the submitLoginForm() function works fine, since wlConnectWithDelegate() is succeeding without login.
This is a little confusing since the ChallengeHandler is called when using wlConnectWithDelegate(), but some of the ChallengeHandler's methods don't yet work until after wlConnectWithDelegate() finishes. Also I can't find it documented anywhere that it works this way.
When ISAM is protecting all the resources between the Mobile app and MFP server, the initial call by wlConnectWithDelegate() will fail, as the call will not go up to the MFP Server. The initial connection URL/s have to be left unprotected (unauth in ISAM).
This link http://www-01.ibm.com/support/docview.wss?uid=swg24034222 contains the integration solution between ISAM and worklight/Mobile First 7.x. In v3.6 integration solution, there is a guide for Form Based Login integration (isam_impf_loginform_int_guide.pdf). The URLs given below are the ones that need to be left unprotected, without which the initial connection/certificate based connection/DirectUpdate will be unsuccessful.
/WebSEAL/<webseal-host-name>-<instance-name>/<context-root>/apps/services/api/<application_name>/<platform>/init
/WebSEAL/<webseal-host-name>-<instance-name>/<context-root>/apps/services/api/<application_name>/<platform>/authenticate
/WebSEAL/<webseal-host-name>-<instance-name>/<context-root>/directUpdate/<application_name>/<platform>
/WebSEAL/<webseal-host-name>-<instance-name>/<context-root>/authorization/v1/clients/instance
I hope this helps.