Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SignalR not supporting self-signed certificates #4697

Open
rgaufman opened this issue Feb 6, 2024 · 8 comments
Open

SignalR not supporting self-signed certificates #4697

rgaufman opened this issue Feb 6, 2024 · 8 comments

Comments

@rgaufman
Copy link

rgaufman commented Feb 6, 2024

Expected behavior

It should create the connection

Actual behavior

It drops the connection without any useful error, just showing:

Could not connect Error: Error during negotiation request.

It shows this error even if I specify a completely invalid URL too.

Steps to reproduce

I have this simple javascript code that works fine over http, but as soon as I try to make a connection to an https server with a self signed certificate, it fails with: "Could not connect Error: Error during negotiation request."

The code is:

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const getHtml = () => (`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <script src="http://127.0.0.1/jquery-2.2.2.min.js"></script>
        <script src="http://127.0.0.1/jquery.signalr-2.4.3.min.js"></script>
        <script type="text/javascript">
          const onEventReceive = (eventData) => {
            console.log('Received event... ' + new Date())
          }

          const startConnection = () => {
            url = "https://192.168.1.86:8443"
            token = "TOKEN-PROVIDED-ELSEWHERE"
            let _connection = $.hubConnection(url);
            let _proxy = _connection.createHubProxy('eventHubLocal');
            _connection.qs = {token: token};

            _proxy.on('liveEvents', (event) => {
                console.log('Event triggered:')
                console.log(event)
                onEventReceive(event)
            });

            _connection.error(function(error) {
              console.warn(error);
            });

            _connection.logging = true;
            _connection.start()
                .done((data) => {
                    console.log('started', data);
                    _proxy.invoke('subscribeToLiveEvents')
                        .done(() => {
                            console.log('Subscribed to live events');
                        })
                        .fail((e) => {
                            console.log('Failed to connect. Reconnecting in 3 seconds')
                            console.warn(e);
                            setTimeout(() => {
                              startConnection()
                            }, 3000)
                        });
                })
                .fail((error) => {
                    console.log('Could not connect ' + error);
                    console.log(JSON.stringify(error, null, 2));
                    //reject(error);
                });
          }

          startConnection()
        </script>
      </head>
      <body >
      </body>
    </html>
`);


const virtualConsole = new jsdom.VirtualConsole();

virtualConsole.on("log", (a) => {
    console.log(a);
});

console.log('Starting process... ' + new Date())

const options = {
  runScripts: "dangerously",
  resources: "usable",
  virtualConsole
};

const html = getHtml();
const dom = new JSDOM(html, options);

I start the script with:

node test.js

Any solution to make it work with self signed certificates? - This is not a secure application so I would be happy with a mechanism not to validate the SSL certificate.

@davidfowl
Copy link
Member

To get a better understanding of this problem we'd need to better understand the setup:

  • Where is SignalR hosted and how is it hosted?
  • Where is this self signed certificate configured?
  • What is the client stack?
  • How does that client stack validate certificates (or how do you skip validation on that client)

@rgaufman
Copy link
Author

rgaufman commented Feb 8, 2024

We have no control of the server and the server is working over http, just not over https. It is the Paxton Net2 access control system that just has a self signed SSL certificate. If I open the page in a web browser, I have to allow unsecure connection and then it works in the browser.

I am trying to replicate this allowing of unsecure connection with jquery.signalr-2.4.3.min.js. The client "stack" is just the above code, that's the entirety of it so far and it is launched by node (v12).

There doesn't seem to be much logging at all as launching the script just shows:

Could not connect Error: Error during negotiation request.

This error shows even if I point it to an IP that doesn't exist, so the error logging leaves a lot to be desired. Is there a way to enable verbose logging in jquery.signalr-2.4.3.min.js? - I couldn't find a way.

@davidfowl
Copy link
Member

So far we have some more clues. You’re running a node based process so this is the node client networking stack that is rejecting the self signed cert.

Can you make a normal fetch request to this server? Lets take SignalR out of the picture.

@rgaufman
Copy link
Author

rgaufman commented Feb 9, 2024

Yes, of course, this works fine for example:

const fetch = require('node-fetch');
const WebSocket = require('ws');
const https = require('https');

const YOUR_TOKEN = 'Token';

console.log('Starting negotiation...');

async function negotiateSignalRConnection() {
  const negotiateUrl = 'https://192.168.1.86:8443/signalr/negotiate';
  console.log(`Negotiating at: ${negotiateUrl}`);
  
  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${YOUR_TOKEN}`,
    },
    agent: new https.Agent({
      rejectUnauthorized: false,
    }),
  };

  try {
    const response = await fetch(negotiateUrl, options);
    console.log('Negotiation response status:', response.status);
    const jsonResponse = await response.json();
    console.log('Negotiation response body:', jsonResponse);
    return jsonResponse;
  } catch (error) {
    console.error('Negotiation error:', error);
    throw error;
  }
}

function connectToWebSocket(negotiationResponse) {
  const connectionToken = encodeURIComponent(negotiationResponse.ConnectionToken);
  const webSocketUrl = `wss://192.168.1.86:8443/signalr/connect?transport=webSockets&connectionToken=${connectionToken}`;

  console.log(`Connecting to WebSocket at: ${webSocketUrl}`);

  const ws = new WebSocket(webSocketUrl, {
    handshakeTimeout: 5000,
    agent: new https.Agent({
      rejectUnauthorized: false,
    }),
  });

  ws.on('open', function open() {
    console.log('WebSocket Connected');
  });

  ws.on('message', function incoming(data) {
    console.log('WebSocket Received:', data);
  });

  ws.on('error', function error(err) {
    console.error('WebSocket Connection error:', err);
  });

  ws.on('close', function close() {
    console.log('WebSocket Connection closed');
  });
}

negotiateSignalRConnection()
  .then(connectToWebSocket)
  .catch(error => console.error('Failed to connect:', error));

I see this output:

$ node test.js
Starting negotiation...
Negotiating at: https://192.168.1.86:8443/signalr/negotiate
Negotiation response status: 200
Negotiation response body: {
  Url: '/signalr',
  ConnectionToken: 'AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAK36McRVrEUmnlgQaS7Ug/wAAAAACAAAAAAAQZgAAAAEAACAAAAABFTiZnDE1LcLFdbPSQCy/8/t1Wj98Hb1JuTjvo9WzMAAAAAAOgAAAAAIAACAAAADmPFA0wMG/12WC3bOEjyxVfg7DwEpGRFrFb02xyKPz/TAAAACI9804EyCjBAimFtMgUKCBol+lYsF4/drwD6izewodSSuUnr/I2czDyl/CY/82En1AAAAAIA+oAtPZO9PiTRVLjFTEAQTgp2+SV8Y0px0Y8QjY6IrdeNmzTzWLXxega06UJiB+lBM8RgrLzuDxXTqmf4TCvA==',
  ConnectionId: 'abed5998-7a38-4c0c-a057-2c6e4ef2539c',
  KeepAliveTimeout: 20,
  DisconnectTimeout: 30,
  ConnectionTimeout: 110,
  TryWebSockets: true,
  ProtocolVersion: '1.2',
  TransportConnectTimeout: 5,
  LongPollDelay: 0
}
Connecting to WebSocket at: wss://192.168.1.86:8443/signalr/connect?transport=webSockets&connectionToken=AQAAANCMnd8BFdERjHoAwE%2FCl%2BsBAAAAK36McRVrEUmnlgQaS7Ug%2FwAAAAACAAAAAAAQZgAAAAEAACAAAAABFTiZnDE1LcLFdbPSQCy%2F8%2Ft1Wj98Hb1JuTjvo9WzMAAAAAAOgAAAAAIAACAAAADmPFA0wMG%2F12WC3bOEjyxVfg7DwEpGRFrFb02xyKPz%2FTAAAACI9804EyCjBAimFtMgUKCBol%2BlYsF4%2FdrwD6izewodSSuUnr%2FI2czDyl%2FCY%2F82En1AAAAAIA%2BoAtPZO9PiTRVLjFTEAQTgp2%2BSV8Y0px0Y8QjY6IrdeNmzTzWLXxega06UJiB%2BlBM8RgrLzuDxXTqmf4TCvA%3D%3D
WebSocket Connected
WebSocket Received: {"C":"d-F6B04C11-H,1","S":1,"M":[]}

I am guessing the payload I'm receiving is the SignalR?

My issue is when I add jquery.signalr-2.4.3.min.js to the equation I can't find a mechanism to do something like:

rejectUnauthorized: false

I also cannot find a way to add better logging as it always just comes back with "Could not connect Error: Error during negotiation request.", even if I just point to a random IP that does not exist. I have tested it to work fine with http though so I suspect my issue is the self signed certificate being rejected.

@davidfowl
Copy link
Member

davidfowl commented Feb 9, 2024

Maybe the next test should be making the request the same way signalr does it to see the differences? Since the negotiate request is failing that tells me websockets isn't the problem. the initial ajax request seems to be failing.

Logging guidance for the client is here:
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client#logging

@davidfowl
Copy link
Member

This version of SignalR is using jQuery under the covers for the transport:

return $.ajax(
$.extend(/*deep copy*/ true, {}, $.signalR.ajaxDefaults, {
type: "GET",
data: {},
xhrFields: { withCredentials: connection.withCredentials },
contentType: connection.contentType,
dataType: connection.ajaxDataType
}, options));
},

It seems like you are wanting to flow something here, but I'm not sure exactly what the setting is called (or if there is one).

@davidfowl
Copy link
Member

davidfowl commented Feb 9, 2024

const ws = new WebSocket(webSocketUrl, {
    handshakeTimeout: 5000,
    agent: new https.Agent({
      rejectUnauthorized: false,
    }),
  });

Is this a nodejs API or is it the browser API?

EDIT: Seems like this a node API. In your signalr example I see you;re using JSDom. Doesn't that fundamentally change how networking works?

When you run code using browser APIs in node, do you know how they are mapped?

@davidfowl
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants