├── LICENSE
├── README.md
├── android
└── GhostBridge.java
├── demo-avatars.png
├── demo-welcome.png
├── ghostbridge.go
└── ghostbridge_test.go
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Péter Szilágyi. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of the copyright holder. nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-ghostbridge – React Native to Go bridge
2 |
3 | Crossing language barriers is hard... really hard!
4 |
5 | If you have a React Native mobile UI and would like to call a library method written in Go, you theoretically need to cross `JavaScript → Java → C → Go`. Practically, if we add the tech layers too, it's `UI (JavaScript) → Native Modules (JavaScript/Java) → Android (Java) → Native Interface (Java/C) → Shared library (C) → Go wrappers (C/Go) → Library (Go)`.
6 |
7 | Although there are amazing tools to help streamline the above, the user is still required to do a lot of manual gluing together. Even with all the effort put into it, every language crossed introduces restrictions and performance penalties. At some point, the whole system becomes too brittle to maintain.
8 |
9 | The `ghostbridge` project aims to establish direct communication between the frontend and the app logic, omitting all the intermediate languages, hooking React Native straight into Go.
10 |
11 | *Note, this project is not a silver bullet, it may be overkill or inappropriate for your use case. Before diving in, please consider the rationale behind the creation of `ghostbridge`.*
12 |
13 | ## The platform model sucks
14 |
15 | To put it bluntly, React Native is a glorified website. It may a marvel of the modern world, but under the hood it's still a dumb HTML & JavaScript combo. This makes data processing and shuffling between UI and device exceedingly hard: JavaScript is simply not suited to directly handle the sheer amount of data a modern application needs.
16 |
17 | The React Native solution was the introduction of native modules: instead of touching the data directly (e.g. render a photo), the UI wraps OS components and delegates the heavy lifting to them instead. This works surprisingly well, as long as the OS components can short circuit the data between themselves, without it ever having to enter JavaScript.
18 |
19 | Therein lies the problem. For this delicate equilibrium to hold, the underlying components need to be über-optimized and über-aware of each other. That of course holds true for Android components, but things start falling apart when users introduce libraries written in languages other than Java. Low level JNI interfaces are fine for simple operations, but tight integration with specialized data pathways is not realistic.
20 |
21 | There are two classical ways out: a) invest an insane amount of time to create a tight enough integration to provide the performance guarantees; b) surface the library functionality straight into JavaScript and bear the performance hit:
22 |
23 | - Option (a) is unrealistic if the library itself is heavily changing.
24 | - Option (b) is unrealistic if large amounts of data are handled.
25 |
26 | ## The website model works
27 |
28 | As mentioned above, a React Native app is a website. It might have lots of bells and whistles and tight integrations, but at the end of the day, its core is just a website. This website model means, however, that HTTP is its native form of interaction with remote services.
29 |
30 | This is an interesting edge: since most React Native applications access data from remote servers via HTTP, its implementation is both highly optimized and fairly flexible. Furthermore, since most developers rely on RESTful APIs and CDNs for their remote services, the React Native component ecosystem also evolved around HTTP.
31 |
32 | Instead of fighting React Native's mental model, we can embrace it. Instead of exposing a Go library across 4 languages (losing any hope of sanity), we can convert our library into an HTTP micro-service running on the device itself! Go excels at HTTP anyway, so this seems like a no-brainer.
33 |
34 | Of course, I didn't invent the wheel here. Many developers implemented such a communication model before, yet it never really got popular. Turns out, this seemingly simple idea has some painful security implications:
35 |
36 | - If you run an HTTP server on your mobile device, any application – even worse, any webpage running in any browser on the device – can interact with it.
37 | - If you run an HTTP server on `localhost`, by default there's no encryption or authentication involved, so malicious applications can eavesdrop and execute man-in-the-middle attacks.
38 |
39 | ## From prototype to production
40 |
41 | Exposing a Go library as an HTTP service seems more like a tech demo than a working product. No serious publisher will permit such glaring security holes in their applications (one can only hope). Can we take this model and make it secure though? Turns out the answer is yes, but it needs some code-fu.
42 |
43 | There are three individual problems that we must solve:
44 |
45 | - The UI must ensure it's talking to the correct service before exchanging any sensitive data.
46 | - The Go library must ensure it's talking to the correct UI before exchanging any sensitive data.
47 | - Both endpoints must establish an encrypted stream before exchanging any sensitive data.
48 |
49 | This problem is already solved with real-world services: TLS certificates and API tokens. With an HTTP service running on `localhost` however, things break:
50 |
51 | - If you ship a certificate within your app, that authenticates `localhost`, anyone can extract it and impersonate you, since your service is sharing the `localhost` domain with every other app.
52 | - Without a certificate, we're out of luck. HTTP was designed around this form of security. If we want to use HTTP, we need to play by its rules.
53 |
54 | The key realization here is that a TLS certificate for `localhost` is a perfectly workable solution, as long as **nobody can copy it**. The challenge is not to devise an alternative security protocol, but to devise a way to protect the certificate!
55 |
56 | ### Ephemeral self-signed certificates
57 |
58 | The drama in the above section was around certificates and the challenge of keeping them a secret, but the best way to keep something a secret, is not to create it in the first place! HTTP doesn't care who creates a certificate, as long as it can validate it. Instead of shipping one, we can generate one on the fly!
59 |
60 | The Go library we'd like to expose is a fully fledged Go program. As such, it is perfectly capable of not only running its own web-server, but also generating a random, one-shot, self-signed TLS certificate for it! Yes, there's a catch: Go might be happy with its new certificate, but React Native will flat out reject it as untrusted. There are a few more tools in our shed!
61 |
62 | Although we're advocating that the best form of communication between Go and React Native is HTTPS, the messy platform APIs are still available. Since the Go library is still executing within the context of the Android application, it can also interact with the system and inject the ephemeral certificate into the application's trusted certificate pool!
63 |
64 | 💥 Encryption and server authentication solved! 💥
65 |
66 | ### Ephemeral authorization tokens
67 |
68 | React Native is now protected against eavesdropping and man-in-the-middle attacks. No data will ever leak out from the UI to a malicious entity. We can't state the same about the Go service yet. Without an authentication layer, any application and website can still access it (ignoring the certificate error).
69 |
70 | The classical solution with real-world services is API tokens. If the connecting client knows a pre-agreed upon secret key, they can use the service. Obtaining this API token is usually done out-of-bounds on some other protocol. We can use this exact same mechanism!
71 |
72 | Beside generating the self-signed certificate, the Go library can also generate a random, ephemeral access token. As with the certificate, the Go library can use the platform native interfaces (`Go → C → JNI → Java`) to inject the token into the Android application's HTTP client!
73 |
74 | 💥 Client authentication solved! 💥
75 |
76 | ### Ephemeral service endpoints
77 |
78 | Although all security issues are addressed, there are still a few practicalities before the service model is complete. Starting an HTTP server on `localhost` cannot reliably use the same well defined port: it may be taken by some other program or the OS might be holding it for some reason.
79 |
80 | The simplest solution is to let the operating system pick the socket port for the HTTPS server. That's easy enough, just set the requested port to 0. A changing endpoint, however, means that the React Native UI needs to track which port the service is currently running on, which ruins the simplicity of stable URLs.
81 |
82 | We can again abuse the fact that the Go library executes in the same process as the rest of the Android application and hook into the app's network layer. Instead of having to constantly change the URLs on the UI, we can define a meta-endpoint (e.g. `https://ghost-bridge`), which gets silently redirected to the current live real endpoint by an HTTP interceptor!
83 |
84 | 💥 Seamless ephemeral endpoints solved! 💥
85 |
86 | ## Assemble all the things
87 |
88 | **Q: Does the theory make sense?**
89 | *A: Hell yeah!*
90 |
91 | **Q: Do you want to implement it?**
92 | *A: Erm...*
93 |
94 | I don't blame you. Gluing together all these worlds in a secure way seems like a repetitive, error prone and frustrating experience (to me). Thus *Ghost Bridge* was born, with the aim of never having to do this again, ever. Ever!
95 |
96 | The `ghostbridge` project is an automation layer between **your** Go library and **your** React Native UI, aiming to seamlessly solve all the security and usability challenges whilst, *staying out of the effn way*™.
97 |
98 | ### Creating the Go-React Native bridge
99 |
100 | `ghostbridge` assumes you already have a Go `http.Handler` that it can secure. Creating this RESTful API for your library is your task. The benefit of requiring an `http.Handler` is that you can test your code via the exact same HTTP APIs that React Native will call.
101 |
102 | For demo purposes, let's hack together an `http.Handler` that generates [`adorable`](github.com/ipsn/go-adorable) avatars:
103 |
104 | ```go
105 | package avatars
106 |
107 | import (
108 | "net/http"
109 | "github.com/ipsn/go-adorable"
110 | )
111 |
112 | // service is a simple HTTP handler to generate pseudo-random avatars based on
113 | // the requested URL path.
114 | var service = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
115 | w.Write(adorable.PseudoRandom([]byte(r.URL.Path)))
116 | })
117 | ```
118 |
119 | We need three things:
120 |
121 | - Running the `http.Handler` in a secure way described in this document.
122 | - Exposing the control mechanisms for Android to manage the Go service.
123 | - Bundle the package up as an Android archive to link together with the UI.
124 |
125 | The `ghostbridge` package takes care of security. It has a method that wraps a simple Go `http.Handler` into a mutually authenticated and encrypted HTTPS server `ghostbridge.New(handler http.Handler)`. It can be started, stopped and its security parameters retrieved.
126 |
127 | Although `ghostbridge` provides the control mechanisms (i.e. functions) on the created `ghostbridge.Bridge` object, `gomobile` does not support bundling up external methods into an archive. We need a tiny bit of boilerplate to force these methods into our own package:
128 |
129 | ```go
130 | package avatars
131 |
132 | import (
133 | "net/http"
134 | "github.com/ipsn/go-adorable"
135 | "github.com/ipsn/go-ghostbridge"
136 | )
137 |
138 | // service is a simple HTTP handler to generate pseudo-random avatars based on
139 | // the requested URL path.
140 | var service = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
141 | w.Write(adorable.PseudoRandom([]byte(r.URL.Path)))
142 | })
143 |
144 | // Bridge is a tiny struct (re)definition so gomobile will export all the built
145 | // in methods of the underlying ghostbridge.Bridge struct.
146 | type Bridge struct {
147 | *ghostbridge.Bridge
148 | }
149 |
150 | // NewBridge creates an instance of the ghost bridge, typed such as gomobile to
151 | // generate a Bridge constructor out of it.
152 | func NewBridge() (*Bridge, error) {
153 | bridge, err := ghostbridge.New(service)
154 | if err != nil {
155 | return nil, err
156 | }
157 | return &Bridge{bridge}, nil
158 | }
159 | ```
160 |
161 | We're jumping through a few hoops so that the `ghostbridge.Bridge` object we created via `ghostbridge.New` is converted into a type local to the package (as `gomobile` will only export local types and methods). Although ugly, the above code is just boilerplate, independent of your `http.Handler` implementation, so copy-paste away!
162 |
163 | Creating the Android library now boils down to calling a `gomobile bind` on it (I'm running from within the package folder that contains the above file):
164 |
165 | ```
166 | $ gomobile bind -v -target=android .
167 | [...half a minute later...]
168 |
169 | $ ls -l
170 | total 17428
171 | -rw-r--r-- 1 karalabe karalabe 17832115 Mar 2 16:18 avatars.aar
172 | -rw-rw-r-- 1 karalabe karalabe 678 Mar 2 16:18 avatars.go
173 | -rw-r--r-- 1 karalabe karalabe 7050 Mar 2 16:18 avatars-sources.jar
174 | ```
175 |
176 | If you've never used `gomobile` before, [its wiki pages](https://github.com/golang/go/wiki/Mobile) are a good starting point and [@eliasnaur](https://github.com/eliasnaur) a good followup! :D He's a great guy!
177 |
178 | ### Linking the Go-React Native bridge
179 |
180 | Analogous to our demo Go library, we'll create a new demo React Native UI. First thing's first, let's make a new project and make sure it runs correctly on our Android device or emulator.
181 |
182 | ```
183 | $ react-native init avatardemo
184 | [...a bit later...]
185 | To run your app on Android:
186 | cd /work/src/github.com/ipsn/avatardemo
187 | Have an Android emulator running (quickest way to get started), or a device connected
188 | react-native run-android
189 |
190 | $ cd avatardemo
191 | $ react-native run-android
192 | [...a bit later...]
193 | Starting: Intent { cmp=com.avatardemo/.MainActivity }
194 | ```
195 |
196 | This should get you the stock *welcome* view of React Native.
197 |
198 | 
199 |
200 | Let's replace that with the avatars! First up, we need to link the `avatars.aar` to the React Native Android project. Although there are fancy ways to hook Go sources directly into the Gradle build system, we'll add it manually now.
201 |
202 | Open your project's Gradle build file `avatardemo/android/build.gradle` and modify its repository section to allow local `.aar` archives. Add the `flatDir` section in the group below:
203 |
204 | ```groovy
205 | allprojects {
206 | repositories {
207 | mavenLocal()
208 | google()
209 | jcenter()
210 | maven {
211 | url "$rootDir/../node_modules/react-native/android"
212 | }
213 | flatDir {
214 | dirs 'libs'
215 | }
216 | }
217 | }
218 | ```
219 |
220 | Open your application's Gradle builder `avatardemo/android/app/build.gradle` and add a compile time dependency `compile(name:'avatars', ext:'aar')` to the generated archive (add this to the `dependencies { ... }` section).
221 |
222 | Lastly copy the generated `avatars.aar` into `avatardemo/android/app/libs`. All said and done, you should be able to rebuild your Android application and have the Go code linked:
223 |
224 | ```
225 | $ react-native run-android
226 | [...a bit later...]
227 | Starting: Intent { cmp=com.avatardemo/.MainActivity }
228 | ```
229 |
230 | ### Running the Go-React Native bridge
231 |
232 | Linking the Go library to the React Native didn't do much. We need to actually start the thing! We could attempt some magic startup on library load, but the app developer is better suited to decide when to run the Go service and where (background thread, foreground thread, application thread).
233 |
234 | Since this is a demo, we won't make it fancy: let's start it when the application itself starts. Edit your `MainApplication.java` to import the bridge (`import avatars.Bridge;`) and to start it, add to `onCreate`:
235 |
236 | ```java
237 | try {
238 | Bridge bridge = new Bridge();
239 | } catch (Exception e) {
240 | e.printStackTrace();
241 | }
242 | ```
243 |
244 | Unfortunately `gomobile` is unable to fully expose the Android certificate APIs to Go, so instead of the Go bridge injecting the security configurations auto-magically into Android, we'll delegate that to a Ghost Bridge java utility class. This may change in the future, but requires a bit of work on `gomobile`.
245 |
246 | Please copy the [`GhostBridge.java`](./android/GhostBridge.java) file into your app's source folder. You can import it via `import io.ipsn.ghostbridge.GhostBridge;` and finally modify the bridge creation to initialize the Android side of the crypto bridge:
247 |
248 | ```java
249 | try {
250 | Bridge bridge = new Bridge();
251 | GhostBridge.init(bridge.port(), bridge.cert(), bridge.token());
252 | } catch (Exception e) {
253 | e.printStackTrace();
254 | }
255 | ```
256 |
257 | Redeploy everything and verify that it all still builds:
258 |
259 | ```
260 | $ react-native run-android
261 | [...a bit later...]
262 | Starting: Intent { cmp=com.avatardemo/.MainActivity }
263 | ```
264 |
265 | ### Demo the Go-React Native bridge
266 |
267 | Everything works in theory. But in practice? Let's find out!
268 |
269 | Modify your `avatardemo/App.js` React Native UI file so it also imports `Image` from `react-native`, and replace the boring welcome message with:
270 |
271 | ```js
272 | Welcome to Ghost Bridge!
273 |
277 | ```
278 |
279 | Moment of truth... refresh your app (double press `R` on your keyboard)...
280 |
281 | 
282 |
283 | 💥 Seamless, mutually-authenticated, encrypted bridge! 💥
284 |
285 | ## Explaining the magic
286 |
287 | Wow, it worked! That said, `https://ghost-bridge/avatar.png` is one magical URL. Let's break it down to see what goes on under the hood.
288 |
289 | - `ghost-bridge` is obviously a non existent domain (it doesn't even have a TLD). When we initialized the Ghost Bridge via the Java utility, it inserted an interceptor into React Native's `okhttp3` library. Whenever React Native makes an HTTP request, the interceptor redirects the `ghost-bridge` domain to the Go service running on `localhost`.
290 | - `https:\\` requires a valid, trusted TLS certificate, which we obviously don't have. When the Ghost bridge initializes itself, it retrieves the randomly generated new certificate and injects it into the app's `TrustManager`, which is then injected into React Native's socket factory.
291 | - Client authentication requires an API token, which we obviously didn't specify. Ghost Bridge is again to blame: during initialization it injects another interceptor into `okhttp3`, which adds an `Authorization: Bearer` header every HTTP request sent to the `ghost-bridge` domain.
292 |
293 | The end result is that React Native remains completely oblivious to the fact that it's actually talking to a local library and also remains oblivious to the seamless encryption and mutual authorization. *Everything just works*™.
294 |
295 | ## Credits
296 |
297 | This repository is maintained by Péter Szilágyi ([@karalabe](https://github.com/karalabe)).
298 |
299 | ## License
300 |
301 | 3-Clause BSD.
302 |
--------------------------------------------------------------------------------
/android/GhostBridge.java:
--------------------------------------------------------------------------------
1 | // go-ghostbridge - React Native to Go bridge
2 | // Copyright (c) 2019 Péter Szilágyi. All rights reserved.
3 | package io.ipsn.ghostbridge;
4 |
5 | import com.facebook.react.modules.network.OkHttpClientProvider;
6 | import com.facebook.react.modules.network.OkHttpClientFactory;
7 | import com.facebook.react.modules.network.ReactCookieJarContainer;
8 |
9 | import java.io.IOException;
10 | import java.io.StringBufferInputStream;
11 | import java.security.KeyStore;
12 | import java.security.SecureRandom;
13 | import java.security.cert.CertificateException;
14 | import java.security.cert.CertificateFactory;
15 | import java.security.cert.X509Certificate;
16 |
17 | import javax.net.ssl.SSLContext;
18 | import javax.net.ssl.TrustManager;
19 | import javax.net.ssl.TrustManagerFactory;
20 | import javax.net.ssl.X509TrustManager;
21 |
22 | import okhttp3.Interceptor;
23 | import okhttp3.OkHttpClient;
24 | import okhttp3.Request;
25 | import okhttp3.Response;
26 |
27 | // GhostBridge is a utility class to retrieve the security parameters of an already
28 | // established bridge and inject the certificate, token and domain interceptor into
29 | // React Native's HTTP client.
30 | public class GhostBridge {
31 | // GhostBridge creates a new GhostBridge, both constructing the server side as
32 | // well as authenticating and authorizing the client HTTP library.
33 | public static void init(final long port, final String cert, final String token) throws Exception {
34 | final X509Certificate bridgeCert = (X509Certificate)CertificateFactory.getInstance("X.509")
35 | .generateCertificate(new StringBufferInputStream(cert));
36 |
37 | // Wrap all the trust managers so they each trust out self-signer certificate
38 | TrustManagerFactory factory = TrustManagerFactory.getInstance("X509");
39 | factory.init((KeyStore) null);
40 |
41 | TrustManager[] trustManagers = factory.getTrustManagers();
42 | for (int i = 0; i < trustManagers.length; i++) {
43 | if (trustManagers[i] instanceof X509TrustManager) {
44 | final X509TrustManager current = (X509TrustManager)trustManagers[i];
45 |
46 | // TLS trust manager found, add security exception for own certificate
47 | trustManagers[i] = new X509TrustManager() {
48 | @Override
49 | public X509Certificate[] getAcceptedIssuers() {
50 | return current.getAcceptedIssuers();
51 | }
52 |
53 | @Override
54 | public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
55 | current.checkClientTrusted(x509Certificates, s);
56 | }
57 |
58 | @Override
59 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
60 | // If the server authenticated itself with the trusted certificate, accept
61 | for (X509Certificate cert : chain) {
62 | if (cert.equals(bridgeCert)) {
63 | return;
64 | }
65 | }
66 | // Certificate chain not the self-signed one, delegate to system authority
67 | current.checkServerTrusted(chain, authType);
68 | }
69 | };
70 | }
71 | }
72 | final SSLContext sslContext = SSLContext.getInstance("TLS");
73 | sslContext.init(null, trustManagers, new SecureRandom());
74 |
75 | // Replace React Native's stock socket factory with the trusted self-signed version
76 | OkHttpClientProvider.setOkHttpClientFactory(new OkHttpClientFactory() {
77 | public OkHttpClient createNewNetworkModuleClient() {
78 | return new OkHttpClient.Builder()
79 | .cookieJar(new ReactCookieJarContainer())
80 | .sslSocketFactory(sslContext.getSocketFactory())
81 | .addInterceptor(new Interceptor() {
82 | @Override
83 | public Response intercept(Chain chain) throws IOException {
84 | // If the request is not addressed the bridge, execute directly
85 | Request original = chain.request();
86 | if (!original.url().host().equals("ghost-bridge")) {
87 | return chain.proceed(original);
88 | }
89 | // Request sent to the ghost-bridge, redirect to localhost
90 | Request redirected = original.newBuilder()
91 | .url(original.url().newBuilder()
92 | .host("localhost")
93 | .port((int)port)
94 | .build())
95 | .header("Authorization", "Bearer " + token)
96 | .build();
97 | return chain.proceed(redirected);
98 | }
99 | })
100 | .build();
101 | }
102 | });
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/demo-avatars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsn/go-ghostbridge/78924eea6711ab83540e016f0be83d8adab4ef05/demo-avatars.png
--------------------------------------------------------------------------------
/demo-welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsn/go-ghostbridge/78924eea6711ab83540e016f0be83d8adab4ef05/demo-welcome.png
--------------------------------------------------------------------------------
/ghostbridge.go:
--------------------------------------------------------------------------------
1 | // go-ghostbridge - React Native to Go bridge
2 | // Copyright (c) 2019 Péter Szilágyi. All rights reserved.
3 |
4 | // Package ghostbridge is a secure React Native to Go web bridge.
5 | package ghostbridge
6 |
7 | import (
8 | "crypto/ecdsa"
9 | "crypto/elliptic"
10 | "crypto/rand"
11 | "crypto/tls"
12 | "crypto/x509"
13 | "crypto/x509/pkix"
14 | "encoding/base64"
15 | "encoding/pem"
16 | "io"
17 | "math/big"
18 | "net"
19 | "net/http"
20 | "time"
21 | )
22 |
23 | // Bridge is an HTTPS server that bridges Go and React Native in a secure way,
24 | // providing an encrypted and mutually authenticated data pathway.
25 | type Bridge struct {
26 | token string // Client authorization token to access the HTTPS bridge
27 | listener net.Listener // TCP listener accepting the HTTPS connections from React Native
28 | certificate string // TLS certificate proving the server's authenticity
29 | }
30 |
31 | // New create a new secure web bridge into a Go HTTP server with an authentication
32 | // wrapper built around it, ensuring mobile app security.
33 | func New(handler http.Handler) (*Bridge, error) {
34 | // Generate a private key for the certificate
35 | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
36 | if err != nil {
37 | return nil, err
38 | }
39 | blob, err := x509.MarshalECPrivateKey(priv)
40 | if err != nil {
41 | return nil, err
42 | }
43 | pemPriv := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: blob})
44 |
45 | // Generate the self-signed certificate
46 | template := x509.Certificate{
47 | SerialNumber: big.NewInt(1),
48 | Subject: pkix.Name{
49 | Organization: []string{"Ghost Bridge"},
50 | },
51 | DNSNames: []string{"localhost"},
52 | NotBefore: time.Now(),
53 | NotAfter: time.Now().Add(time.Hour * 24 * 365),
54 |
55 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
56 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
57 | BasicConstraintsValid: true,
58 | }
59 | blob, err = x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
60 | if err != nil {
61 | return nil, err
62 | }
63 | pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: blob})
64 |
65 | // Load the certificate and start an HTTPS server with it
66 | cert, err := tls.X509KeyPair(pemCert, pemPriv)
67 | if err != nil {
68 | return nil, err
69 | }
70 | listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
71 | if err != nil {
72 | return nil, err
73 | }
74 | // Create the verification middleware to authorize the client
75 | blob = make([]byte, 32)
76 | if _, err := io.ReadFull(rand.Reader, blob); err != nil {
77 | return nil, err
78 | }
79 | token := base64.StdEncoding.EncodeToString(blob)
80 |
81 | go http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82 | if r.Header.Get("Authorization") != "Bearer "+token {
83 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
84 | return
85 | }
86 | handler.ServeHTTP(w, r)
87 | }))
88 |
89 | return &Bridge{
90 | token: token,
91 | listener: listener,
92 | certificate: string(pemCert),
93 | }, nil
94 | }
95 |
96 | // Close terminates the underlying listener, and implicitly the bridge.
97 | func (b *Bridge) Close() error {
98 | return b.listener.Close()
99 | }
100 |
101 | // Port returns the listener port assigned to the bridge.
102 | func (b *Bridge) Port() int {
103 | return b.listener.Addr().(*net.TCPAddr).Port
104 | }
105 |
106 | // Cert returns the TLS certificate assigned to the bridge.
107 | func (b *Bridge) Cert() string {
108 | return b.certificate
109 | }
110 |
111 | // Token returns the client authorization token to access the bridge.
112 | func (b *Bridge) Token() string {
113 | return b.token
114 | }
115 |
--------------------------------------------------------------------------------
/ghostbridge_test.go:
--------------------------------------------------------------------------------
1 | // go-ghostbridge - React Native to Go bridge
2 | // Copyright (c) 2019 Péter Szilágyi. All rights reserved.
3 |
4 | package ghostbridge
5 |
6 | import (
7 | "crypto/tls"
8 | "crypto/x509"
9 | "fmt"
10 | "net/http"
11 | "testing"
12 | )
13 |
14 | // Test that a new, self-signed TLS certificate can be generated and an HTTPS
15 | // server spun up with it.
16 | func TestBridge(t *testing.T) {
17 | // Create a TLS bridge to serve some requests
18 | bridge, err := New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 | w.Write([]byte("Yay, it works!"))
20 | }))
21 | if err != nil {
22 | t.Fatalf("Failed to create self-signed TLS bridge")
23 | }
24 | defer bridge.Close()
25 |
26 | // Create a TLS client with the self-signed certificate injected
27 | roots := x509.NewCertPool()
28 | if !roots.AppendCertsFromPEM([]byte(bridge.Cert())) {
29 | t.Fatalf("Failed to load server certificate")
30 | }
31 | client := &http.Client{
32 | Transport: &http.Transport{
33 | TLSClientConfig: &tls.Config{
34 | RootCAs: roots,
35 | },
36 | },
37 | }
38 | // Assemble the authenticated HTTP request and ensure it executes ok
39 | req, err := http.NewRequest("GET", fmt.Sprintf("https://localhost:%d", bridge.Port()), nil)
40 | if err != nil {
41 | t.Fatalf("Failed to create HTTP request: %v", err)
42 | }
43 | req.Header.Set("Authorization", "Bearer "+bridge.Token())
44 |
45 | res, err := client.Do(req)
46 | if err != nil {
47 | t.Fatalf("Failed to execute HTTP request: %v", err)
48 | }
49 | if res.StatusCode != http.StatusOK {
50 | t.Fatalf("Invalid status returned from bridge: have %v, want %v", res.StatusCode, http.StatusOK)
51 | }
52 | res.Body.Close()
53 | }
54 |
--------------------------------------------------------------------------------