Skip to content

Commit 9ace7b1

Browse files
authored
feat(rtdb): Connect to RTDB emulator when valid emulator URL is passed OR env vars are set correctly (firebase#299)
* Talk to RTDB emulator instead of database instance if FIREBASE_RTDB_EMULATOR_HOST=true * Added test for talking to rtdb emulator when vars are set * Updated code and added tests to reflect valid emulator URLs and the logic for inferring them correctly * Remove stray character * Renamed setup and teardown methods to be correct * Addressed most review comments, moved emulator URL override check to FirebaseDatabase only, updated test cases * Added deferred credentials functionality * Verified and fixed parsing of encoded URLs * Fixed another round of reveiw comments * Fixed checkstyle errors * Allowed HTTPS connections to the emulator, only if the user passes in a valid URL through the databaseUrl field * Minor changes * Minor fixes * Fixes from review comments * Fixed test imports * Handled + in URLs and added test * Inlined variable declarations
1 parent 0a06159 commit 9ace7b1

File tree

15 files changed

+664
-150
lines changed

15 files changed

+664
-150
lines changed

src/main/java/com/google/firebase/FirebaseApp.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.common.base.Preconditions.checkArgument;
2020
import static com.google.common.base.Preconditions.checkNotNull;
2121
import static com.google.common.base.Preconditions.checkState;
22+
import static com.google.firebase.FirebaseOptions.APPLICATION_DEFAULT_CREDENTIALS;
2223

2324
import com.google.api.client.googleapis.util.Utils;
2425
import com.google.api.client.json.JsonFactory;
@@ -33,6 +34,7 @@
3334
import com.google.common.base.Joiner;
3435
import com.google.common.base.MoreObjects;
3536
import com.google.common.base.Strings;
37+
import com.google.common.base.Supplier;
3638
import com.google.common.collect.ImmutableList;
3739
import com.google.firebase.internal.FirebaseAppStore;
3840
import com.google.firebase.internal.FirebaseScheduledExecutor;
@@ -582,10 +584,9 @@ private static FirebaseOptions getOptionsFromEnvironment() throws IOException {
582584
String defaultConfig = System.getenv(FIREBASE_CONFIG_ENV_VAR);
583585
if (Strings.isNullOrEmpty(defaultConfig)) {
584586
return new FirebaseOptions.Builder()
585-
.setCredentials(GoogleCredentials.getApplicationDefault())
586-
.build();
587+
.setCredentials(APPLICATION_DEFAULT_CREDENTIALS)
588+
.build();
587589
}
588-
589590
JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
590591
FirebaseOptions.Builder builder = new FirebaseOptions.Builder();
591592
JsonParser parser;
@@ -597,7 +598,7 @@ private static FirebaseOptions getOptionsFromEnvironment() throws IOException {
597598
parser = jsonFactory.createJsonParser(reader);
598599
}
599600
parser.parseAndClose(builder);
600-
builder.setCredentials(GoogleCredentials.getApplicationDefault());
601+
builder.setCredentials(APPLICATION_DEFAULT_CREDENTIALS);
601602
return builder.build();
602603
}
603604
}

src/main/java/com/google/firebase/FirebaseOptions.java

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@
2626
import com.google.auth.oauth2.GoogleCredentials;
2727
import com.google.cloud.firestore.FirestoreOptions;
2828
import com.google.common.base.Strings;
29+
import com.google.common.base.Supplier;
30+
import com.google.common.base.Suppliers;
2931
import com.google.common.collect.ImmutableList;
3032
import com.google.firebase.internal.FirebaseThreadManagers;
3133
import com.google.firebase.internal.NonNull;
3234
import com.google.firebase.internal.Nullable;
3335

36+
import java.io.IOException;
3437
import java.util.HashMap;
3538
import java.util.List;
3639
import java.util.Map;
@@ -56,9 +59,21 @@ public final class FirebaseOptions {
5659
"https://www.googleapis.com/auth/cloud-platform",
5760
"https://www.googleapis.com/auth/datastore");
5861

62+
static final Supplier<GoogleCredentials> APPLICATION_DEFAULT_CREDENTIALS =
63+
new Supplier<GoogleCredentials>() {
64+
@Override
65+
public GoogleCredentials get() {
66+
try {
67+
return GoogleCredentials.getApplicationDefault().createScoped(FIREBASE_SCOPES);
68+
} catch (IOException e) {
69+
throw new IllegalStateException(e);
70+
}
71+
}
72+
};
73+
5974
private final String databaseUrl;
6075
private final String storageBucket;
61-
private final GoogleCredentials credentials;
76+
private final Supplier<GoogleCredentials> credentialsSupplier;
6277
private final Map<String, Object> databaseAuthVariableOverride;
6378
private final String projectId;
6479
private final String serviceAccountId;
@@ -69,11 +84,10 @@ public final class FirebaseOptions {
6984
private final ThreadManager threadManager;
7085
private final FirestoreOptions firestoreOptions;
7186

72-
private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) {
73-
this.credentials = checkNotNull(builder.credentials,
74-
"FirebaseOptions must be initialized with setCredentials().")
75-
.createScoped(FIREBASE_SCOPES);
87+
private FirebaseOptions(@NonNull final FirebaseOptions.Builder builder) {
7688
this.databaseUrl = builder.databaseUrl;
89+
this.credentialsSupplier = checkNotNull(
90+
builder.credentialsSupplier, "FirebaseOptions must be initialized with setCredentials().");
7791
this.databaseAuthVariableOverride = builder.databaseAuthVariableOverride;
7892
this.projectId = builder.projectId;
7993
if (!Strings.isNullOrEmpty(builder.storageBucket)) {
@@ -118,7 +132,7 @@ public String getStorageBucket() {
118132
}
119133

120134
GoogleCredentials getCredentials() {
121-
return credentials;
135+
return credentialsSupplier.get();
122136
}
123137

124138
/**
@@ -227,8 +241,7 @@ public static final class Builder {
227241

228242
@Key("serviceAccountId")
229243
private String serviceAccountId;
230-
231-
private GoogleCredentials credentials;
244+
private Supplier<GoogleCredentials> credentialsSupplier;
232245
private FirestoreOptions firestoreOptions;
233246
private HttpTransport httpTransport = Utils.getDefaultTransport();
234247
private JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
@@ -248,7 +261,7 @@ public Builder() {}
248261
public Builder(FirebaseOptions options) {
249262
databaseUrl = options.databaseUrl;
250263
storageBucket = options.storageBucket;
251-
credentials = options.credentials;
264+
credentialsSupplier = options.credentialsSupplier;
252265
databaseAuthVariableOverride = options.databaseAuthVariableOverride;
253266
projectId = options.projectId;
254267
httpTransport = options.httpTransport;
@@ -308,7 +321,20 @@ public Builder setStorageBucket(String storageBucket) {
308321
* @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
309322
*/
310323
public Builder setCredentials(GoogleCredentials credentials) {
311-
this.credentials = checkNotNull(credentials);
324+
this.credentialsSupplier = Suppliers
325+
.ofInstance(checkNotNull(credentials).createScoped(FIREBASE_SCOPES));
326+
return this;
327+
}
328+
329+
/**
330+
* Sets the <code>Supplier</code> of <code>GoogleCredentials</code> to use to authenticate the
331+
* SDK. This is NOT intended for public use outside the SDK.
332+
*
333+
* @param credentialsSupplier Supplier instance that wraps GoogleCredentials.
334+
* @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
335+
*/
336+
Builder setCredentials(Supplier<GoogleCredentials> credentialsSupplier) {
337+
this.credentialsSupplier = checkNotNull(credentialsSupplier);
312338
return this;
313339
}
314340

src/main/java/com/google/firebase/database/FirebaseDatabase.java

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import static com.google.common.base.Preconditions.checkNotNull;
2020
import static com.google.common.base.Preconditions.checkState;
2121

22+
import com.google.auth.oauth2.AccessToken;
23+
import com.google.auth.oauth2.GoogleCredentials;
24+
import com.google.common.base.Strings;
25+
import com.google.common.collect.ImmutableMap;
2226
import com.google.firebase.FirebaseApp;
2327
import com.google.firebase.FirebaseOptions;
2428
import com.google.firebase.ImplFirebaseTrampolines;
@@ -27,15 +31,20 @@
2731
import com.google.firebase.database.core.Repo;
2832
import com.google.firebase.database.core.RepoInfo;
2933
import com.google.firebase.database.core.RepoManager;
34+
import com.google.firebase.database.util.EmulatorHelper;
3035
import com.google.firebase.database.utilities.ParsedUrl;
3136
import com.google.firebase.database.utilities.Utilities;
3237
import com.google.firebase.database.utilities.Validation;
3338
import com.google.firebase.internal.FirebaseService;
3439

3540
import com.google.firebase.internal.SdkUtils;
41+
import java.io.IOException;
3642
import java.util.Collections;
43+
import java.util.Date;
3744
import java.util.HashMap;
45+
import java.util.List;
3846
import java.util.Map;
47+
import java.util.concurrent.TimeUnit;
3948
import java.util.concurrent.atomic.AtomicBoolean;
4049

4150
/**
@@ -112,15 +121,21 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String
112121
if (service == null) {
113122
service = ImplFirebaseTrampolines.addService(app, new FirebaseDatabaseService());
114123
}
115-
116-
DatabaseInstances dbInstances = service.getInstance();
117124
if (url == null || url.isEmpty()) {
118125
throw new DatabaseException(
119126
"Failed to get FirebaseDatabase instance: Specify DatabaseURL within "
120127
+ "FirebaseApp or from your getInstance() call.");
121128
}
122-
123-
ParsedUrl parsedUrl = Utilities.parseUrl(url);
129+
ParsedUrl parsedUrl;
130+
boolean connectingToEmulator = false;
131+
String possibleEmulatorUrl = EmulatorHelper
132+
.getEmulatorUrl(url, EmulatorHelper.getEmulatorHostFromEnv());
133+
if (!Strings.isNullOrEmpty(possibleEmulatorUrl)) {
134+
parsedUrl = Utilities.parseUrl(possibleEmulatorUrl);
135+
connectingToEmulator = true;
136+
} else {
137+
parsedUrl = Utilities.parseUrl(url);
138+
}
124139
if (!parsedUrl.path.isEmpty()) {
125140
throw new DatabaseException(
126141
"Specified Database URL '"
@@ -130,6 +145,7 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String
130145
+ parsedUrl.path.toString());
131146
}
132147

148+
DatabaseInstances dbInstances = service.getInstance();
133149
FirebaseDatabase database = dbInstances.get(parsedUrl.repoInfo);
134150
if (database == null) {
135151
DatabaseConfig config = new DatabaseConfig();
@@ -140,11 +156,12 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String
140156
config.setSessionPersistenceKey(app.getName());
141157
}
142158
config.setFirebaseApp(app);
143-
159+
if (connectingToEmulator) {
160+
config.setCustomCredentials(new EmulatorCredentials(), true);
161+
}
144162
database = new FirebaseDatabase(app, parsedUrl.repoInfo, config);
145163
dbInstances.put(parsedUrl.repoInfo, database);
146164
}
147-
148165
return database;
149166
}
150167

@@ -207,6 +224,11 @@ public DatabaseReference getReference(String path) {
207224
public DatabaseReference getReferenceFromUrl(String url) {
208225
checkNotNull(url,
209226
"Can't pass null for argument 'url' in FirebaseDatabase.getReferenceFromUrl()");
227+
String possibleEmulatorUrl = EmulatorHelper
228+
.getEmulatorUrl(url, EmulatorHelper.getEmulatorHostFromEnv());
229+
if (!Strings.isNullOrEmpty(possibleEmulatorUrl)) {
230+
url = possibleEmulatorUrl;
231+
}
210232
ParsedUrl parsedUrl = Utilities.parseUrl(url);
211233
Repo repo = ensureRepo();
212234
if (!parsedUrl.repoInfo.host.equals(repo.getRepoInfo().host)) {
@@ -380,4 +402,26 @@ public void destroy() {
380402
instance.destroy();
381403
}
382404
}
405+
406+
private static class EmulatorCredentials extends GoogleCredentials {
407+
408+
EmulatorCredentials() {
409+
super(newToken());
410+
}
411+
412+
private static AccessToken newToken() {
413+
return new AccessToken("owner",
414+
new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)));
415+
}
416+
417+
@Override
418+
public AccessToken refreshAccessToken() {
419+
return newToken();
420+
}
421+
422+
@Override
423+
public Map<String, List<String>> getRequestMetadata() throws IOException {
424+
return ImmutableMap.of();
425+
}
426+
}
383427
}

src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@
3535
import java.io.EOFException;
3636
import java.net.URI;
3737
import java.security.KeyStore;
38+
import java.security.KeyStoreException;
39+
import java.security.NoSuchAlgorithmException;
3840
import java.util.concurrent.ExecutorService;
3941
import java.util.concurrent.ThreadFactory;
42+
import javax.net.ssl.SSLException;
4043
import javax.net.ssl.TrustManagerFactory;
4144

4245
/**
@@ -57,13 +60,15 @@ class NettyWebSocketClient implements WebsocketConnection.WSClient {
5760
private final ChannelHandler channelHandler;
5861
private final ExecutorService executorService;
5962
private final EventLoopGroup group;
63+
private final boolean isSecure;
6064

6165
private Channel channel;
6266

6367
NettyWebSocketClient(
64-
URI uri, String userAgent, ThreadFactory threadFactory,
68+
URI uri, boolean isSecure, String userAgent, ThreadFactory threadFactory,
6569
WebsocketConnection.WSClientEventHandler eventHandler) {
6670
this.uri = checkNotNull(uri, "uri must not be null");
71+
this.isSecure = isSecure;
6772
this.eventHandler = checkNotNull(eventHandler, "event handler must not be null");
6873
this.channelHandler = new WebSocketClientHandler(uri, userAgent, eventHandler);
6974
this.executorService = new FirebaseScheduledExecutor(threadFactory,
@@ -75,20 +80,26 @@ class NettyWebSocketClient implements WebsocketConnection.WSClient {
7580
public void connect() {
7681
checkState(channel == null, "channel already initialized");
7782
try {
78-
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(
79-
TrustManagerFactory.getDefaultAlgorithm());
80-
trustFactory.init((KeyStore) null);
81-
final SslContext sslContext = SslContextBuilder.forClient()
82-
.trustManager(trustFactory).build();
8383
Bootstrap bootstrap = new Bootstrap();
84+
SslContext sslContext = null;
85+
if (this.isSecure) {
86+
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(
87+
TrustManagerFactory.getDefaultAlgorithm());
88+
trustFactory.init((KeyStore) null);
89+
sslContext = SslContextBuilder.forClient()
90+
.trustManager(trustFactory).build();
91+
}
8492
final int port = uri.getPort() != -1 ? uri.getPort() : DEFAULT_WSS_PORT;
93+
final SslContext finalSslContext = sslContext;
8594
bootstrap.group(group)
8695
.channel(NioSocketChannel.class)
8796
.handler(new ChannelInitializer<SocketChannel>() {
8897
@Override
8998
protected void initChannel(SocketChannel ch) {
9099
ChannelPipeline p = ch.pipeline();
91-
p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), port));
100+
if (finalSslContext != null) {
101+
p.addLast(finalSslContext.newHandler(ch.alloc(), uri.getHost(), port));
102+
}
92103
p.addLast(
93104
new HttpClientCodec(),
94105
// Set the max size for the HTTP responses. This only applies to the WebSocket

src/main/java/com/google/firebase/database/connection/WebsocketConnection.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,8 @@ public WSClient newClient(WSClientEventHandler delegate) {
413413
String host = (optCachedHost != null) ? optCachedHost : hostInfo.getHost();
414414
URI uri = HostInfo.getConnectionUrl(
415415
host, hostInfo.isSecure(), hostInfo.getNamespace(), optLastSessionId);
416-
return new NettyWebSocketClient(
417-
uri, context.getUserAgent(), context.getThreadFactory(), delegate);
416+
return new NettyWebSocketClient(uri, hostInfo.isSecure(), context.getUserAgent(),
417+
context.getThreadFactory(), delegate);
418418
}
419419
}
420420

src/main/java/com/google/firebase/database/core/Context.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.firebase.database.core;
1818

19+
import com.google.auth.oauth2.GoogleCredentials;
1920
import com.google.firebase.FirebaseApp;
2021
import com.google.firebase.ImplFirebaseTrampolines;
2122
import com.google.firebase.database.DatabaseException;
@@ -233,4 +234,13 @@ private String buildUserAgent(String platformAgent) {
233234
.append(platformAgent);
234235
return sb.toString();
235236
}
237+
238+
public void setCustomCredentials(GoogleCredentials customCredentials, boolean autoRefresh) {
239+
// ensure that platform exists
240+
getPlatform();
241+
// ensure that runloop exists else we might get a NPE
242+
this.ensureRunLoop();
243+
this.authTokenProvider = new JvmAuthTokenProvider(firebaseApp, this.getExecutorService(),
244+
autoRefresh, customCredentials);
245+
}
236246
}

src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ public class JvmAuthTokenProvider implements AuthTokenProvider {
4141
}
4242

4343
JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh) {
44-
this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp);
44+
this(firebaseApp, executor, autoRefresh, ImplFirebaseTrampolines.getCredentials(firebaseApp));
45+
}
46+
47+
JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh,
48+
GoogleCredentials customCredentials) {
49+
this.credentials = customCredentials;
4550
this.authVariable = firebaseApp.getOptions().getDatabaseAuthVariableOverride();
4651
this.executor = executor;
4752
if (autoRefresh) {

0 commit comments

Comments
 (0)