olofd / react-native-signalr

Use SignalR with React Native
150 stars 61 forks source link

Cannot use websockets on iOS #12

Closed somoso closed 7 years ago

somoso commented 7 years ago

I've been developing with react-native-signalr for both Android and iOS with the requirement that the connection must be websocket based.

Android works wonderfully. iOS gives me the following crash with react-native log-ios:

Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: Running application "SignalRApp" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: 'posting to', 'http://examplesignalr.com/Example/GetToken'
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: 'Example State', { _rowHasChanged: [Function: rowHasChanged],
      _getRowData: [Function: defaultGetRowData],
      _sectionHeaderHasChanged: [Function],
      _getSectionHeaderData: [Function: defaultGetSectionHeaderData],
      _dataBlob: { s1: [] },
      _dirtyRows: [ [] ],
      _dirtySections: [ true ],
      _cachedRowCount: 0,
      rowIdentities: [ [] ],
      sectionIdentities: [ 's1' ] }
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Error>: [] __nwlog_err_simulate_crash simulate crash already simulated "nw_socket_set_common_sockopts setsockopt SO_NOAPNFALLBK failed: [42] Protocol not available"
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Error>: [] nw_socket_set_common_sockopts setsockopt SO_NOAPNFALLBK failed: [42] Protocol not available, dumping backtrace:
            [x86_64] libnetcore-856.20.4
        0   libsystem_network.dylib             0x0000000114219682 __nw_create_backtrace_string + 123
        1   libnetwork.dylib                    0x0000000114a88932 nw_socket_add_input_handler + 3100
        2   libnetwork.dylib                    0x0000000114a664f4 nw_endpoint_flow_attach_protocols + 3768
        3   libnetwork.dylib                    0x0000000114a65511 nw_endpoint_flow_setup_socket + 563
        4   libnetwork.dylib                    0x0000000114a64270 -[NWConcrete_nw_endpoint_flow startWithHandler:] + 2612
        5   libnetwork.dylib                    0x0000000114a7f44d nw_endpoint_handler_path_change + 1261
        6   libnetwork.dylib                    0x0000000114a7ee7c nw_endpoint_handler_start + 570
        7   libdispatch.dylib                   0x0000000113fa1810 _dispatch_call_block_and_release + 12
        8   libdispatch.dylib                   0x0000000113fc
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: Client subscribed to hub 'exampleHub'.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: Negotiating with 'http://examplesignalr.com/signalr/negotiate?clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22exampleHub%22%7D%5D'.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: webSockets transport starting.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: Connecting to websocket endpoint 'ws://examplesignalr.com/signalr/connect?transport=webSockets&clientProtocol=1.5&connectionToken=eXNnRQNAB6mzAEYevdhfupjUyFargeId5n%2FHWLUwH%2FqlNET7OdK3AVR5TT3YaxLn0AHOY9seRfz7tfxM0vRJnR%2BAVGxT8iIuGkoigNj%2FlJoA6k0W81pLCcbAGuPHTW66&connectionData=%5B%7B%22name%22%3A%22exampleHub%22%7D%5D&tid=4'.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Error>: [] __nwlog_err_simulate_crash simulate crash already simulated "nw_socket_set_common_sockopts setsockopt SO_NOAPNFALLBK failed: [42] Protocol not available"
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Error>: [] nw_socket_set_common_sockopts setsockopt SO_NOAPNFALLBK failed: [42] Protocol not available, dumping backtrace:
            [x86_64] libnetcore-856.20.4
        0   libsystem_network.dylib             0x0000000114219682 __nw_create_backtrace_string + 123
        1   libnetwork.dylib                    0x0000000114a88932 nw_socket_add_input_handler + 3100
        2   libnetwork.dylib                    0x0000000114a664f4 nw_endpoint_flow_attach_protocols + 3768
        3   libnetwork.dylib                    0x0000000114a65511 nw_endpoint_flow_setup_socket + 563
        4   libnetwork.dylib                    0x0000000114a64270 -[NWConcrete_nw_endpoint_flow startWithHandler:] + 2612
        5   libnetwork.dylib                    0x0000000114a7f44d nw_endpoint_handler_path_change + 1261
        6   libnetwork.dylib                    0x0000000114a7ee7c nw_endpoint_handler_start + 570
        7   libdispatch.dylib                   0x0000000113fa1810 _dispatch_call_block_and_release + 12
        8   libdispatch.dylib                   0x0000000113fc
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: Websocket closed.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: Closing the Websocket.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: webSockets transport failed to connect. Attempting to fall back.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: No fallback transports were selected.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: SignalR error: Error: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: Failed
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: Stopping connection.
Dec  2 13:18:40 Humboldt-Mac-Mini SignalRApp[89427] <Notice>: [13:18:40 GMT+0000 (GMT)] SignalR: Fired ajax abort async = true.

Here is the snippet of code used to cause this crash:

var connection = signalr.hubConnection(this.getUrl());
connection.logging = true;

exampleProxy = connection.createHubProxy('exampleHub');

connection.start({transport: ['webSockets']}).done(() => { 
    console.log('Now connected, connection ID=' + connection.id); 
    exampleProxy.invoke("SomeMethod").done((r) => {
      console.log("SomeMethod result",r);
    });
}).fail(() => {
    console.log('Failed'); 
});

Unfortunately, falling back to long-polling isn't an option for us.

olofd commented 7 years ago

Hmm.. weird. And I guess it's not the same as #11 ? Must be something wrong in the setup/rn-version or something. Hard to guess without repro. Websockets works in iOS on my end of course. Can you somehow debug on the server and see if the connection is received?

olofd commented 7 years ago

@somoso Did you have any success in tracking down this issue? Let me know if I can help in any way. But I will need more info/better debug-scenario to be able to help efficiently.

Will close in a couple of days if I do not hear from you.

somoso commented 7 years ago

Thanks for checking this out - I'll give #11 a go and if that fails, I'll see if I can have access to the actual server and debug it from there.

somoso commented 7 years ago

Just a quick update, I've added NSAllowsArbitraryLoads in my Info.plists file and I couldn't get it to behave. For now I'm going to close this issue, but I'll reopen if I have any questions or issues with react-native-signalr.

Thanks for the help though!

somoso commented 7 years ago

OK, so I've managed to understand what is going on with a little help with Wireshark.

Before we interact with signalr, I go through a login process (mocked for now) which sets the cookie on our end. With Android, it passed those cookies in the header when doing a GET /signalr/negotiate call and when doing a GET /signalr/connect call. With the iOS app, it passes the cookies in the header in the GET /signalr/negotiate call, but doesn't pass the cookies when doing a GET /signalr/connect call!

Looking around on the internet, I though adding withCredentials: true would fix things as below:

connection.start({withCredentials:true, transport: ['webSockets']}).done(() => { 
    console.log('Now connected, connection ID=' + connection.id); 
    exampleProxy.invoke("SomeMethod").done((r) => {
      console.log("SomeMethod result",r);
    });
}).fail(() => {
    console.log('Failed'); 
});

but that still doesn't fix things. Using react-native-cookies, I've found that all my cookies have the path parameter set to "/", so that's not the issue (plus, Android works fine).

I'm still using the same trick as addressed in #11 on the XCode project.

Is there a way to add a custom header on the connection so that I can pretty much just attempt to stuff the cookie in the header in the iOS version? Is there another trick that I'm missing?

olofd commented 7 years ago

Ah. interesting. Of course I have not used this lib with credentials, so there we have our discrepancies. Let me look into this tonight and I'll get back to you.

somoso commented 7 years ago

I've just noticed while browsing your source code that in ajax.js there is an XMLHttpRequest() object which is used to help signalr communicate - I'm wondering if that XMLHttpRequest could do with having withCredentials: true being passed in as well as setting it at the signalr level.

I'll give this a shot on my end and see if it fixes things tomorrow morning.

somoso commented 7 years ago

So just as a follow up, I've tried adding withCredentials: true to the XMLHttpRequest and I saw no improvements. As I was unsure if it was being sent properly, after the line request.open('GET', options.url);, I added the line request.setRequestHeader('x-react-native-header', 'soheb'); to see if my weird header would get passed along the chain - but it didn't!

Using Wireshark, I saw the header on the GET /signalr/negotiate call, but it completely vanishes on the GET /signalr/connect call.

I'm going to have to dig into ms-signalr-client and see what's going on

somoso commented 7 years ago

Right. Got it in the end.

Turns out that react's WebSocket implementation has an undocumented API that passes in headers: http://stackoverflow.com/questions/37246446/sending-cookies-with-react-native-websockets

So after much meddling with the source code, I decided to do a cheeky thing and just monkey patched in my own WebSockets, passing in my own cookies:

      var oldws = window.WebSocket;
      window.WebSocket = function(url) {
          return new oldws(url, '', {Cookie: cookie_string});
      }

That way, I don't need to keep patching your code nor Microsoft's SignalR code to pass in the cookies. It might be worth exposing the hidden 3rd parameter in WebSockets in your own APIs to allow people to set custom headers.


As for why it is even doing that in the first place, I've done some digging and found it's down to React Native source - in the Android side, the cookies get pulled in and passed in the header here: https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java#L94

While in iOS the connect call in RCTWebSocketModule uses initWithURLRequest (https://github.com/facebook/react-native/blob/master/Libraries/WebSocket/RCTWebSocketModule.m#L60) but looking at the RCTSRWebSocket class there is initWithURL which handles cookies and calls initWithURLRequest (https://github.com/facebook/react-native/blob/master/Libraries/WebSocket/RCTSRWebSocket.m#L292) that gets completely bypassed.

I'll close this ticket, but you can re-open if you want. I'll try to keep an eye on this for a while for any extra feedback & comments.


EDIT: There is a pull request for the iOS version on React Native: https://github.com/facebook/react-native/pull/10575#issuecomment-265790339 - it seems like it is waiting on some testing