Last active
May 1, 2023 13:16
-
-
Save NielsLeenheer/c4775f1f04fc470cd727 to your computer and use it in GitHub Desktop.
How a web app running on HTTPS can communicatie with local services
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I have a web app that needs to talk to point of sale hardware: receipt printers, customer displays, | |
cash drawers and payment terminals. Direct communication from the web app with these devices is | |
impossible, so I created a native app that can be installed on the computer that can talk to the | |
devices. The app runs a web server that exposes these devices using a REST api. | |
The native app registers itself upon startup with a webservice. It sends its own IP address on | |
the local network and the server also sees the external IP address of the network. The web app | |
sends a discovery request to the webservice and it receives all the local IP addresses which | |
have the same external IP address. | |
This scheme works perfectly if the web app runs on plain, unsecure HTTP. You can use XMLHttpRequest | |
to talk to the native app and send commands and receive back responses. | |
The problem: | |
When the web app is running on HTTPS you cannot use XMLHttpRequest to talk to an unsecure HTTP | |
server. If we want to talk to our native app, we must also use HTTPS to communicate with it. | |
That leads us to our next problem: It is not possible to get a trusted certificate for a local | |
IP address or localhost. Nor would we want to buy a certicate for each installation of the | |
native app. And without a certificate we can't use HTTPS. | |
The solution: | |
- Get a domain name specifically for your native app. For example: nativeapp.me | |
- Buy a wildcard TLS certificate for the domain: *.nativeapp.me and ship it with the native | |
app to set up a HTTPS REST API | |
- Install PowerDNS on a server and run xip.io DNS for your domain | |
- Use the following scheme from the web app to call the native app: [ip address].nativeapp.me | |
If you use xip.io in combination with PowerDNS all DNS requests that look like | |
[ip address].nativeapp.me will resolve to the IP address you specified in the domain name. | |
For example, if our native app runs on a machine with local IP address 192.168.0.19, we want | |
to connect to it using 192.168.0.19.nativeapp.me which resolves to 192.168.0.19. | |
Now, this is causing another problem, because our wildcard certificate is just for *.nativeapp.me | |
and not *.*.*.*.nativeapp.me. Luckily xip.io can also understand base 36 encoded ip addresses. | |
On our web app side we use this function to convert the local IP address to base 36: | |
function base36(ip) | |
{ | |
var d = ip.split('.'); | |
return (((((((+d[3])*256)+(+d[2]))*256)+(+d[1]))*256)+(+d[0])).toString(36); | |
} | |
base36('192.168.0.19') | |
=> "59t7ls" | |
So our local IP address of 192.168.0.19 needs to be called using the 59t7ls.nativeapp.me domain | |
which resolves back to 192.168.0.19. And because we have a wildcard certificate for | |
*.nativeapp.me the connection is secure and trusted no matter on which IP address the native | |
app runs. | |
There is one thing though... Not only do we need to ship the certificate with the native app. | |
We also need to ship the private key for the whole wildcarded domain. That makes it vulnerable | |
to be extracted from the binary of the native app. We could let the native app download it from | |
the server upon startup and never store it on the hard disk, but it will still be in memory on | |
the computer, or alternatively the webservice could be tricked into giving the private key. | |
We can secure our private key with a password, but in that case we need to include that password | |
in our app's source code. | |
There is no way around this, I think. So one precaution you must absolutely take is to use a | |
new domain name specifically for this, so traffic you send over the public internet cannot be | |
intercepted and the connection between your web app and the public server is never compromised. | |
I'm not really an expert in certificates, but what if you decide to use a self-signed root certificate, and sign with it all certificates for the "local web services"? In that instance, could you get certificate for a local ip address?
Only issue of course would be to import the self-signed root cert to the browser.
Note that shipping the private key will cause your certificate to be revoked if discovered, I think. see e.g.: https://groups.google.com/forum/#!msg/mozilla.dev.security.policy/pk039T_wPrI/tGnFDFTnCQAJ
Hi, thanks for your solution. What happens when your certificate expires?
Brilliant! apart from the risk @ArntWork mentioned
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for this note - I might be facing a similar set of challenges with a hardware + browser project I am working on. I am still a few weeks of dev work away from crossing this bridge, however.
One approach I have considered is calling the local web server from the browser (http://localhost:8000/ or whatever), having that web server listen to the native app for hardware inputs, and then the local web server composes the inputs and makes https calls to my cloud backend.
I'm not sure if you considered this approach - but if you have any feedback on it I would appreciate it! Feel free to poke holes or point out security concerns.