Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Hosting SignalR under SSL/https


:P
On this page:

As I've described in several previous posts, self hosting SignalR is very straight forward to set up. It's easy enough to do, which is great if you need to hook SignalR as an event source to standard Windows based application such as a Service or even a WPF or Winforms desktop application that needs to send push notifications to many users.

One aspect of self-hosting that's not quite so transparent or documented though, is running a self hosted SignalR service under SSL. The Windows certificate store and creation, configuration and installation of certificates is still a pain as there's no UI in Windows that provides linking endpoints to certificates and the process is not very well documented end to end. It's easy enough once you know what command line tool you need to call, but this process certainly could be a little smoother and better documented. Hence I'm rehashing this topic here to provide a little more detail and hopefully a more coherent description of setting up a certificate for self-hosting an OWIN service in general and specifically for SignalR.

Self-Hosting and OWIN

When you're self hosting SignalR you're essentially using the hosting services provided by OWIN/Katana. OWIN is a low level spec that for implementing custom hosting providers that can be used interchangeably. The idea is to decouple the hosting process from a specific implementation and make it pluggable, so you can choose your hosting implementations.

Katana is Microsoft's implementation of OWIN, that provides a couple of specific implementations. For self-hosting there's the HttpListener based host which is completely decoupled from IIS and its infrastructure. For hosting inside of ASP.NET there also is an ASP.NET based implementation that is used for SignalR apps running inside of ASP.NET. Both implementations provide the base hosting support for SignalR, so that for the most part the same code base can be used for running SignalR under ASP.NET or under your own self-hosted EXEs like services, console or desktop apps.

Binding certificates to SSL Ports for Self-Hosting

Self hosting under HttpListener is wonderful and completely self-contained, but one of the downsides of not being part of IIS is that it also doesn't know about certificates that are installed for IIS, which means that certificates you want to use have to be explicitly bound to a port. Note that you can use IIS certificates and if you need to acquire a full certificate for use with a self-hosted application, going through the IIS certificate process is the easiest way to get the certificate loaded. If you need a certificate for local testing too IIS's self-signed certificate creation tool makes that very easy as well (I'll describe that below).

For now let's assume you already have a certificate installed in the Windows certificate store. In order to bind the certificate to a self-hosted endpoint, you have to use the netsh command line utility to register it on the machine (all on one line):

netsh http add sslcert ipport=0.0.0.0:8082
           appid={12345678-db90-4b66-8b01-88f7af2e36bf} 
           certhash=d37b844594e5c23702ef4e6bd17719a079b9bdf

For every endpoint mapping you need to supply 3 values:

  • The ipport which identifies the ip and port
    Specified as ipport=0.0.0.0:8082 where the zeros mean all ip addresses on port 8082. Otherwise you can also specify a specific Ip Address.

  • The certhash which is the Certificate's Thumbprint
    The certhash is the id that maps the certificate to the IP endpoint above. You can find this hash by looking at the certificate in the Windows Certificate store. More on this in a minute.

  • An AppID which is fixed for HttpListener Hosting
    This value is static magic value so always use appid={12345678-db90-4b66-8b01-88f7af2e36bf}. Once the above command has been run you should check if it worked by looking at the binding. Use this:

netsh http show sslcert ipport=0.0.0.0:8082

which gives you a display like this:

netsh

Finding the CertHash

I mentioned the certhash above: To find the certhash, you need to find the certificate's ThumbPrint which can be found in a couple of ways using:

  • The IIS Certificate Manager
  • The Windows Certificate Storage Manager

Using IIS to get Certificate Info

If IIS is installed the former is the easiest. Here you can easily see all installed certificates and this UI is also the easiest way to create local self-signed certificates.

To look up an existing certificate, simply bring up the IIS Management Console, go to the Machine node, then Server Certificates:

IISCerts

You can see the certificate hash in the rightmost column. You can also double click and open the certificate and go in the Details of the certificate. Look for the thumbprint which contains the hash.

CertificateDetails 

Unfortunately neither of these places makes it easy to copy the hash, so you either have to copy it manually or remove the spaces from the thumbprint data in the dialog.

Using IIS to create a self-signed Certificate

If you don't have a full server certificate yet, but you'd like to test with SSL operations locally you can also use the IIS Admin interface to very easily create a self-signed certificate. The IIS Management console provides one of the easiest ways to create a local self-signed certificate.

Here's how to do it:

  • Go to the machine root of the IIS Service Manager
  • Go to the Server Certificates Item in the IIS section
  • On the left click Create Self-Signed Certificate
  • Give it a name, and select the Personal store
  • Click OK

IisCreateCert

That's all there is to create the self-signed local certificate.

Copy the self-signed Certificate to the Trusted Root Certification Store

Once you have a self-signed certificate, you need one more step to make the certificate trusted, so Http clients will accept it on your machine without certificate errors. The process involves copying the certificate from the personal store to the trusted machine store.

To do this:

  • From the StartMenu use Manage Computer Certificates
  • Go into Personal | Certificates and find your certificate
  • Drag and Copy (Ctrl-Drag) the certificate to Trusted Root Certificates | Certificates

TrustCertificate

You should now have a certificate that browsers will trust. This works fine for IE, Chrome and Safari, but FireFox will need some special steps (thanks to Eric Lawrence) and Opera also requires specific registration of certificates.

Using a full IIS Certificate

Self signed certificates are great for testing under SSL to make sure your application works, but it's not so nice for production apps as the certificate would have to be installed on any machine you'd expect to trust this certificate which is a hassle.

Once you go to production, especially public production you'll need an 'official' certificate signed by a one of the global certificate authorities for $$$ (or these days LetsEncrypt).

The easiest way to do this is to purchase or generate a full IIS certificate and install it in IIS. The IIS certificate can also be used for self-hosted applications using the HttpListener so it will work just fine with a self-hosted SignalR or any HttpListener application.

So once the time comes to go live, register a new certificate through IIS, then use netsh http add sslcert to register that certificate as shown above. A public SSL certificate in most cases is already recognized so no further certificate store moving is required - all you need is the netsh registration to tie it to a particular port and app Id.

Running SignalR with SSL

With the certificate installed, switching SignalR to start with SSL is as easy as changing the startup URL.

Self Hosted Server Configuration

In the self hosted server, you now specify the new SSL URL in your startup factory invocation:

var signalR = WebApp.Start<SignalRStartup>([https://*:8082/](https://*:8082/));

This binds SignalR to the all ip addresses on port 8082. You can also specify a specific IP address, but using * is more portable especially if you set the value as part of a shared configuration file.

If you recall from my last self-hosting post, OWIN uses a startup class (SignalRStartup in this case) to handle OWIN and SignalR HubConfiguration, but the only thing that needs to change is the startup URL and your self-hosted server is ready to go.

SignalR Web App Page Url Configuration

On the Web Page consume the SignalR service to hubs or connections change the script URL that loads up the SignalR client library for your hubs or connections like this:

<script src="[https://RasXps:8082/signalr/hubs">script>

where RasXps here is my exact local machine name that has the certificate registered to it. As with all certificates make sure that the domain name matches the certificate's name exactly. For local machines that means don't use localhost if the certificate is assigned to your local machines NetBios name as it is by default. Don't use your IP address either - use whatever the certificate is assigned to.

You'll also need to assign the hub Url to your SSL url as part of the SignalR startup routine that calls $connection.hub.start:

$.connection.hub.url = self.hubUrl; // ie. "[https://rasxps:8082/signalR](https://rasxps:8082/signalR);"

For more context here's a typical hub startup/error handler setup routine that I use to get the hub going:

startHub: function () {
    $.connection.hub.url = self.hubUrl;  // ie. "https://rasxps:8082/signalR";

    // capture the hub for easier access
    var hub  = $.connection.queueMonitorServiceHub;
                                    
    // This means the <script> proxy failed - have to reload
    if (hub == null) {
        self.viewModel.connectionStatus("Offline");                
        toastr.error("Couldn't connect to server. Please refresh the page.");
        return;
    }
            
    // Connection Events
    hub.connection.error(function (error) {                
        if (error)
            toastr.error("An error occurred: " + error.message);
        self.hub = null;
    });
    hub.connection.disconnected(function (error) {                
        self.viewModel.connectionStatus("Connection lost");
        toastr.error("Connection lost. " + error);                

        // IMPORTANT: continuously try re-starting connection
        setTimeout(function () {                    
            $.connection.hub.start();                    
        }, 2000);
    });            
            
    // map client callbacks
    hub.client.writeMessage = self.writeMessage;
    hub.client.writeQueueMessage = self.writeQueueMessage;            
    hub.client.statusMessage = self.statusMessage;
    …
              
    // start the hub and handle after start actions
    $.connection.hub
        .start()
        .done(function () {
            hub.connection.stateChanged(function (change) {
                if (change.newState === $.signalR.connectionState.reconnecting)
                    self.viewModel.connectionStatus("Connection lost");
                else if (change.newState === $.signalR.connectionState.connected) {
                    self.viewModel.connectionStatus("Online");

                    // IMPORTANT: On reconnection you have to reset the hub
                    self.hub = $.connection.queueMonitorServiceHub;
                }
                else if (change.newState === $.signalR.connectionState.disconnected)
                    self.viewModel.connectionStatus("Disconnected");
            })     
        .error(function (error) {
            if (!error)
                error = "Disconnected";
            toastr.error(error.message);
        })
        .disconnected(function (msg) {
            toastr.warning("Disconnected: " + msg);
        });
                                    
        self.viewModel.connectionStatus("Online");                

        // get initial status from the server (RPC style method)
        self.getServiceStatus();
        self.getInitialMessages();                    
        });            
},

From a code perspective other than the two small URL code changes there isn't anything that changes for SSL operation, which is nice.

And… you're done!

SSL Configuration

SSL usage is becoming ever more important as more and more application require transport security. Even if your self-hosted SignalR application doesn't explicitly require SSL, if the SignalR client is hosted inside of a Web page that's running SSL you have to run SignalR under SSL, if you want it to work without browser error messages or failures under some browsers that will reject mixed content on SSL pages.

SSL configuration is always a drag, as it's not intuitive and requires a bit of research. It'd be nice if the HttpListener certificate configuration would be as easy as IIS configuration is today or better yet, if self-hosted apps could just use already installed IIS certificates. Unfortunately it's not quite that easy and you do need to run a command line utility with some magic ID associated with it.

Installing a certificate isn't rocket science, but it's not exactly well documented. While looking for information I found a few unrelated articles that discuss the process but a few were dated and others didn't specifically cover SignalR or even self-hosting Web sites. So I hope this post makes it a little easier to find this information in the proper context.

This article focuses on SignalR self-hosting with SSL, but the same concepts can be applied to any self-hosted application using HttpListener.

Resources

Posted in OWIN  SignalR  

The Voices of Reason


 

Richard L
October 08, 2013

# re: Hosting SignalR under SSL/https

Excellent write-up, thank you!

One thing to beware of though - when copying the thumbprint from Certificate Manager in the details window, it appears that a hidden character gets copied at the start. You can check this by using the cursor to move through it - delete it if it's there! Leaving it in caused the "The Parameter is Incorrect" message for me, when the command on the command prompt *looked* correct. This was at least the case for me on Windows 8 and Server 2012.

Jan K
October 12, 2013

# re: Hosting SignalR under SSL/https

Thanks for a great post.
Here is a great tool that I stumbled upon by Rafaele (http://www.iamraf.net/) for handeling certificates and getting the thumbprint without clutter.
Check it out http://www.iamraf.net/Downloads/DeployManager.exe

lrpham
March 27, 2014

# re: Hosting SignalR under SSL/https

Thanks for the great writeup. Following the directions, I was able to get SignalR to work under HTTPS. Is it possible to have SignalR handle both HTTPS and HTTP under the same service, under the startup factory invocation ? Thanks.

Rick Strahl
March 27, 2014

# re: Hosting SignalR under SSL/https

Irpham - yes you can - just run WebApp.Start() multiple times for each of the URLs involved. This creates separate service hosts, but they all use the same static SignalR components.

lrpham
July 17, 2014

# re: Hosting SignalR under SSL/https

Hi Rick, I've been running this HTTPS configuration for a few months without issues but now, messages sent to any specific client does not reach the client. I'm using hubs and is using this method to send the message:

Clients.client(connectionID).publishMessage()

However, when sending a message back to a client caller it works fine using this method:

Clients.Caller.publishMessage()

The only thing that changed recently was the connection string to the database server.
Have you run into this problem with HTTPS ? Thanks.

Ian
September 03, 2014

# re: Hosting SignalR under SSL/https

I don't know if I'm missing something but when I tried to use SSL with a self signed certificate my hub connection fails on start with the exception

"The remote certificate is invalid according to the validation procedure."

Is there a way to tell SignalR to not worry about self signed certificates?

Ben T
June 11, 2015

# re: Hosting SignalR under SSL/https

Thanks to Richard L! I hit the "The Parameter is Incorrect" problem. It would have taken ages to figure out that hidden character issue.

Kevin
June 13, 2016

# re: Hosting SignalR under SSL/https

I used this guide to make a local self-hosted SignalR app to work with some of our in-house web applications that use SSL. It was working until corporate pushed through regedits that require browsers to use the TLS 1.2 protocol. After this change, the browser throws an unsupported SSL version error when the browser tries to connect to the app's SignalR hub. So is there any way to specify which SSL protocol that the self-hosted SignalR app uses?

Rick Strahl
June 13, 2016

# re: Hosting SignalR under SSL/https

@Kevin - You have to make sure you use a TLS 1.2 certificate on the server. There's no way to control 'which version' is served - the cert supports a certain level of SSL/TLS and that's what you get. If you need to support the newer type you need to install a TLS 1.2 cert on the server.

Junaid
July 14, 2016

# re: Hosting SignalR under SSL/https

I have developed a portable, self hosted windows service(SignalR), by portable i mean that any user who wants to communicate/upload the data to my web app should install this service on their local system. There was no problem communicating over HTTP. I followed all the steps provided in this blog to make it work over HTTPS. It works fine when the windows service and the web application are hosted from the same system. But it fails to communicate over HTTPS(using self signed certificate) when the service is hosted from a different machine and throws up error ERR_INSECURE_RESPONSE, though i could notice that the signalR proxy is generated.
What kind of SSL certificate do i have to create to distribute to multiple clients to communicate. How do i register the SSL certificate in local system and communicate over it. Any pointers over this will be of great help.
Thanks in anticipation.

Rick Strahl
July 16, 2016

# re: Hosting SignalR under SSL/https

@Junaid - if you create a self-signed certificate that certificate is valid only on the machine that you created it on - unless you register that certificate on other machines. You can do that by installing the certificate on remote machines as shown in the Copy Certificate to Trusted Root section in the post above. You can do the same on other machines that you want to trust.

If you want a certificate that 'just works' on machines then you have to use a certificate authority signed certificate from a full CA. These days you can use Lets Encrypt to create these certificates for IIS and then use them in your SignalR appplications. For more info see:

* https://weblog.west-wind.com/posts/2016/Feb/22/Using-Lets-Encrypt-with-IIS-on-Windows

and

* https://weblog.west-wind.com/posts/2016/Jul/09/Moving-to-Lets-Encrypt-SSL-Certificates

Adin
April 02, 2017

# re: Hosting SignalR under SSL/https

Had same issue as Richard L and used their solution. Excellent comment Richard.


Johnny K.
April 21, 2017

# re: Hosting SignalR under SSL/https

Hi thanks for a great article..
I wonder if anyone has an input on doing this from app.config only. I use "Lets encrypt" certificates (valid 3 month and i autorenew) I dont want to "netsh" with new thumbprint, every 3 month. I want to find the cert in store by name and use it in my windows service.

Any comments?


signalr_dev
July 08, 2017

# re: Hosting SignalR under SSL/https

Hi Rick, Thanks for great post. I have developed SignalR HttpListener based host. My web application communicates with this SignalR host installed on local machine. It works fine with HTTP.

I have tried to apply SSL certificate (HTTPS) using SelfSSL on SignalR based host (certificate issued to HOSTNAME), but when my web page(HTTPS) access signalR host (HTTPS) it does not connect with SignalR host and proxy is coming undefined.(CONNECTION_REFUSED error)

when i open SignalR proxy on browser tab and add it in Exception List(Security Exception) of browser, it connect with signalR host, but browser turns from secure site to Not Secure site. How can i resolve this Not Secure error comes in browser address bar? How can i make it working without adding in security exception list of browser?

If i want to use SignalR host on 100 computers, Is there any easy way i can install certificate on hundred of computers? Do i have to buy SSL certificate issued to local HostName from Certificate autority? If yes, single wildcard certificate can work for all 100 computers? OR i will have to buy for each computers seperately? Please guide.


Guilherme
July 11, 2017

# re: Hosting SignalR under SSL/https

If my IIS is using the valid certificate and I want to apply the same certificate to a self-hosted SignalR I have no problem?

Ex: https://www.contoso.com.br = My site running with SSL Ex: https://www.contoso.com.br:9090 = my self-hosted SignalR running with the same certificate

I get this scenario?


Oliver Clare
September 05, 2017

# re: Hosting SignalR under SSL/https

4 years on and this article still seems relevant today. We're struggling with an HTTPS setup for SignalR, but not for the obvious reasons. We seem to have an issue that only occurs when there's an HTTPS binding and reliably disappears when we remove the HTTPS binding and use HTTP only. However, we have had the same code working fine on HTTPS and sometimes it seems to be okay... Due to a recent bunch of tests (swapping in/out various bindings), we are convinced it's something to do with our code on an HTTPS setup, but the fact it works sometimes on HTTPS is really frustrating!

We're getting the following error chain: System.Net.Http.HttpRequestException: An error occurred while sending the request. ---> System.Net.WebException: The underlying connection was closed: An unexpected error occurred on a send. ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host

I'm currently thinking it has to be that we're not making enough attempts to restart the connection on disconnect, but any advice would be welcome. Many thanks!


Ian
September 20, 2017

# re: Hosting SignalR under SSL/https

Hi Rick, thanks for the solution. When I started with SignalR you blog helped a lot and it still does.

I have a question...do you perhaps know why when I run netsh http show servicestate the registered urls is different to the certificate I loaded. I bound to https://abc.domnain.xx.xx:8088/MyUri but in servicestate it show HTTPS://ABC.DOMAIN.xx.xx:443/SINGALR/:8088

When I browse to my url to see if the Hub is started I get 'resource (url) is online but are not responding to conncetion attempts'

Could you please help and guide.

Thanks


Ian
September 20, 2017

# re: Hosting SignalR under SSL/https

Thanks...got it sorted. It was the MyUri portion I used at the end of my netsh command. Once I removed that it all worked fine.

Thanks again for your blog it is a real reference when getting stuck.


Sudhakar
November 30, 2017

# re: Hosting SignalR under SSL/https

Thanks Rick for the great article.

Could you please give inputs on implementing browser client side certificate verification. I mean, two way certificate verification.


Markus Wagner
February 12, 2018

# re: Hosting SignalR under SSL/https

I get the same error as Oliver Clare:

System.Net.Http.HttpRequestException: An error occurred while sending the request. --->
System.Net.WebException: The underlying connection was closed: An unexpected error occurred on a send. ---> 
System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> 
System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host

The strange thing is, I get the exception only in my .net client using SSL. The same implementation is working like a charm in .net without SSL or in a web client with SSL.

At the moment, I am running out of ideas what could be wrong...


Kevin
March 02, 2018

# re: Hosting SignalR under SSL/https

I meant to revisit this a while ago. In response to my TLS question: when Big Brother pushed the SChannel regedits to our machines, they didn't set the "DisabledByDefault" key to "0" for TLS 1.2. For machines running Windows 7, TLS 1.2 is installed, but disabled, so it is necessary to set this key value as per Microsoft's instruction. As soon as I added it manually and restarted, everything worked no problem.


David
May 17, 2018

# re: Hosting SignalR under SSL/https

I have same problem with Junaid where my webapp is hosted on one server, but my signalR service is hosted on a different server. They are both using selfsigned certs and I've set things up properly on the server side. The problem is I get this error on the browser client side: Error in connection establishment: net::ERR_CERT_AUTHORITY_INVALID

Now if I open another tab in same browser to the server hosting the signalR service and click advanced and proceed to unsafe site, then everything works. Is it possible on the javascript client side to ignore cert warnings? I know we can do this in chrome by changing the chrome setting but this would be much easier if I could do this in javascript code. I have a feeling it's not possible due to security reasons but can someone confirm? I know only other options are to get a paid cert, add cert to client's trusted root store, or have it added on the domain controllers so all computers on domain have it trusted.


12343954
August 29, 2018

# re: Hosting SignalR under SSL/https

hi,where is the “App ID”? I can‘t find it!

netsh http add sslcert ipport=0.0.0.0:8082
           **appid={12345678-db90-4b66-8b01-88f7af2e36bf} **
           certhash=d37b844594e5c23702ef4e6bd17719a079b9bdf

Rick Strahl
August 31, 2018

# re: Hosting SignalR under SSL/https

As the post says: "The AppId is a hardcoded value that never changes". I presume it's specific to HttpHandler.


Sébastien
December 12, 2018

# re: Hosting SignalR under SSL/https

For those with the same issue as Olivier Clare and Markus Wagner, it's probably because of TLS version not enabled for the version of net framework you use. I had the same issue and also took me a while to find the root cause. Following this StackOverflow link for details: https://stackoverflow.com/a/46223433


Sreejith
February 27, 2019

# re: Hosting SignalR under SSL/https

Hi i read through your post i am also facing a similar issue not sure what is the correct way to resolve, it works in http://localhost:port/signalr but when i host it in IIS with https://domain.com/api/signalr it says and http error 404 with the following error message No HTTP resource was found that matches the request URI http://domain.com/api/signalr/negotiate?clientProtocol=2.0&connectionData=[{"name":"progress"}]&_=1551264914856' SignalR error: Error: Error during negotiation request. This is my client js call $(function () {

    //$.support.cors = true;
    $.connection.hub.logging = true;
    $.connection.hub.url = Config.APIHost + "api/signalr";
    var chat = $.connection.progress;

    chat.client.addProgress = function (message, percentage) {
        
        ProgressBarModal("show", message + " " + percentage);
        $('#ProgressMessage').width(percentage);
        if (percentage == "100%") {
            
            ProgressBarModal();
         }
    };

    $.connection.hub.start().done(function () {
        //debugger;
        var connectionId = $.connection.hub.id;
        console.log(connectionId);
    });


    $.connection.hub.error(function (error) {
        //debugger;
        console.log('SignalR error: ' + error)
    });
});

The request header although has the https://domain.com/api/signalr/negotiate?clientProtocol=2.0&connectionData=[{"name":"progress"}]&_=1551264914856'

I am really not clear on what is wrong here.


Mladen Mihajlovic
March 19, 2019

# re: Hosting SignalR under SSL/https

This is one of those things that I don't need very often but when I do I've forgotten everything and need to look it all up again 😃

Just a note that netsh has been "superseded" by powershell these days so the above commands can actually be done using the following:

netsh http add sslcert ipport=0.0.0.0:8082
           appid={12345678-db90-4b66-8b01-88f7af2e36bf} 
           certhash=d37b844594e5c23702ef4e6bd17719a079b9bdf

translates to

Add-NetIPHttpsCertBinding -IpPort "0.0.0.0:8282"  -ApplicationId "{12345678-db90-4b66-8b01-88f7af2e36bf}" -CertificateHash "d37b844594e5c23702ef4e6bd17719a079b9bdf" -CertificateStoreName "My" -NullEncryption $false

you might also need

New-SelfSignedCertificate -Subject "CertSubject"

Aravind S
August 31, 2021

# re: Hosting SignalR under SSL/https

Hi Rick

Myself Aravind from India. I have created certificate using Opc.Ua.Generator and did port binding for my application certificate. Initially I was getting certificate issues on the client side hub saying "The underline connection was closed.Could not establish true relationship for SSL/TLS secure channel." I got this issue fixed by changing the Certificate name to Host name of the system which is running SignalR service. After this i was able to setup chat and communicate successfully with 3 systems in the same domain but this didn't work out when I took the setup to a different domain. I ended up in the former error and applying of the Host name didn't work out for the other network. Is there any problem in the fix I have applied or any alternate solution is there, Please let me know.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024