diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java index df04f26ca38..5e2450b14d6 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -28,11 +28,27 @@ public abstract class InstallationTokenResult { * The amount of time, in milliseconds, before the auth-token expires for this firebase * installation. */ + @NonNull public abstract long getTokenExpirationTimestampMillis(); @NonNull - public static InstallationTokenResult create( - @NonNull String authToken, long tokenExpirationTimestampMillis) { - return new AutoValue_InstallationTokenResult(authToken, tokenExpirationTimestampMillis); + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static InstallationTokenResult.Builder builder() { + return new AutoValue_InstallationTokenResult.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setAuthToken(@NonNull String value); + + @NonNull + public abstract Builder setTokenExpirationTimestampMillis(@NonNull long value); + + @NonNull + public abstract InstallationTokenResult build(); } } diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index 45123f1c6ce..ec0224515dd 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -45,6 +45,10 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" + annotationProcessor "com.google.auto.value:auto-value:1.6.2" + testImplementation 'androidx.test:core:1.2.0' testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index f135aa2e197..e33664b6e32 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -78,7 +78,7 @@ public Task getId() { @NonNull @Override public Task getAuthToken(boolean forceRefresh) { - return Tasks.forResult(InstallationTokenResult.create("dummy_auth_token", 1000l)); + return Tasks.forResult(InstallationTokenResult.builder().build()); } /** diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java new file mode 100644 index 00000000000..51cf2e1a219 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -0,0 +1,256 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations.remote; + +import android.util.JsonReader; +import androidx.annotation.NonNull; +import com.google.firebase.installations.InstallationTokenResult; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.json.JSONException; +import org.json.JSONObject; + +/** Http client that sends request to Firebase Installations backend API. */ +public class FirebaseInstallationServiceClient { + private static final String FIREBASE_INSTALLATIONS_API_DOMAIN = + "firebaseinstallations.googleapis.com"; + private static final String CREATE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations"; + private static final String GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT = + "projects/%s/installations/%s/auth:generate"; + private static final String DELETE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations/%s"; + private static final String FIREBASE_INSTALLATIONS_API_VERSION = "v1"; + private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_V2"; + + private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; + private static final String ACCEPT_HEADER_KEY = "Accept"; + private static final String JSON_CONTENT_TYPE = "application/json"; + private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; + private static final String GZIP_CONTENT_ENCODING = "gzip"; + + private static final String UNAUTHORIZED_ERROR_MESSAGE = + "The request did not have the required credentials."; + private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; + private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + + @NonNull + public InstallationResponse createFirebaseInstallation( + long projectNumber, + @NonNull String apiKey, + @NonNull String firebaseInstallationId, + @NonNull String appId) + throws FirebaseInstallationServiceException { + String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildCreateFirebaseInstallationRequestBody(firebaseInstallationId, appId) + .toString() + .getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readCreateResponse(httpsURLConnection); + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + + private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, String appId) + throws JSONException { + JSONObject firebaseInstallationData = new JSONObject(); + firebaseInstallationData.put("fid", fid); + firebaseInstallationData.put("appId", appId); + firebaseInstallationData.put("appVersion", FIREBASE_INSTALLATION_AUTH_VERSION); + return firebaseInstallationData; + } + + @NonNull + public void deleteFirebaseInstallation( + long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + throws FirebaseInstallationServiceException { + String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("DELETE"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return; + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + + @NonNull + public InstallationTokenResult generateAuthToken( + long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + throws FirebaseInstallationServiceException { + String resourceName = + String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readGenerateAuthTokenResponse(httpsURLConnection); + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + // Read the response from the createFirebaseInstallation API. + private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException { + JsonReader reader = + new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + InstallationTokenResult.Builder installationTokenResult = InstallationTokenResult.builder(); + InstallationResponse.Builder builder = InstallationResponse.builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("name")) { + builder.setName(reader.nextString()); + } else if (name.equals("refreshToken")) { + builder.setRefreshToken(reader.nextString()); + } else if (name.equals("authToken")) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (key.equals("token")) { + installationTokenResult.setAuthToken(reader.nextString()); + } else if (key.equals("expiresIn")) { + installationTokenResult.setTokenExpirationTimestampMillis(reader.nextLong()); + } else { + reader.skipValue(); + } + } + builder.setAuthToken(installationTokenResult.build()); + reader.endObject(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + return builder.build(); + } + + // Read the response from the generateAuthToken FirebaseInstallation API. + private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) + throws IOException { + JsonReader reader = + new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + InstallationTokenResult.Builder builder = InstallationTokenResult.builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("token")) { + builder.setAuthToken(reader.nextString()); + } else if (name.equals("expiresIn")) { + builder.setTokenExpirationTimestampMillis(reader.nextLong()); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + return builder.build(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java new file mode 100644 index 00000000000..b703b56d539 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java @@ -0,0 +1,57 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations.remote; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseException; + +/** The class for all Exceptions thrown by {@link FirebaseInstallationServiceClient}. */ +public class FirebaseInstallationServiceException extends FirebaseException { + + public enum Code { + SERVER_ERROR, + + NETWORK_ERROR, + + UNAUTHORIZED + } + + @NonNull private final Code code; + + FirebaseInstallationServiceException(@NonNull Code code) { + this.code = code; + } + + FirebaseInstallationServiceException(@NonNull String message, @NonNull Code code) { + super(message); + this.code = code; + } + + FirebaseInstallationServiceException( + @NonNull String message, @NonNull Code code, Throwable cause) { + super(message, cause); + this.code = code; + } + + /** + * Gets the status code for the operation that failed. + * + * @return the code for the FirebaseInstallationServiceException + */ + @NonNull + public Code getCode() { + return code; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java new file mode 100644 index 00000000000..2022a498fe1 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations.remote; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; +import com.google.firebase.installations.InstallationTokenResult; + +@AutoValue +public abstract class InstallationResponse { + + @NonNull + public abstract String getName(); + + @NonNull + public abstract String getRefreshToken(); + + @NonNull + public abstract InstallationTokenResult getAuthToken(); + + @NonNull + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static InstallationResponse.Builder builder() { + return new AutoValue_InstallationResponse.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setName(@NonNull String value); + + @NonNull + public abstract Builder setRefreshToken(@NonNull String value); + + @NonNull + public abstract Builder setAuthToken(@NonNull InstallationTokenResult value); + + @NonNull + public abstract InstallationResponse build(); + } +}