├── 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 | ![welcome](./demo-welcome.png) 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 | ![avatars](./demo-avatars.png) 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 | --------------------------------------------------------------------------------