Skip to content

Instantly share code, notes, and snippets.

@NielsLeenheer
Last active May 1, 2023 13:16
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save NielsLeenheer/c4775f1f04fc470cd727 to your computer and use it in GitHub Desktop.
Save NielsLeenheer/c4775f1f04fc470cd727 to your computer and use it in GitHub Desktop.
How a web app running on HTTPS can communicatie with local services
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.
@tblumer3
Copy link

tblumer3 commented Sep 4, 2017

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.

@vasso123
Copy link

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.

@ArntWork
Copy link

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

@davidst2017
Copy link

Hi, thanks for your solution. What happens when your certificate expires?

@Xsmael
Copy link

Xsmael commented Dec 20, 2022

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