Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions android-core/proguard.pro
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@

-keep class com.mparticle.MPEvent$* { *; }
-keep class com.mparticle.MParticle { *; }
-keep class com.mparticle.MParticle$Internal { *; }
-keep class com.mparticle.internal.ConfigManager {
public com.mparticle.networking.NetworkOptions getNetworkOptions();
}
Comment thread
thomson-t marked this conversation as resolved.
-keep class com.mparticle.MParticle$EventType { *; }
-keep class com.mparticle.MParticle$InstallType { *; }
-keep class com.mparticle.MParticle$IdentityType { *; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ class UploadServiceTest : BaseMPServiceTest() {
assertEquals(6, UploadService.getReadyUploads(database).size)
}

@Test
@Throws(JSONException::class)
fun testUploadSettingsPreserveCustomBaseURL() {
val uploadSettings = UploadSettings(
"apiKey",
"secret",
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com:8443")
.build(),
"",
"",
)
UploadService.insertUpload(database, uploadJson(System.currentTimeMillis()), uploadSettings)

val readyUploads = UploadService.getReadyUploads(database)

assertEquals(1, readyUploads.size)
assertEquals("rkt.example.com:8443", readyUploads[0].uploadSettings.networkOptions.customBaseURL)
}

@Throws(JSONException::class)
private fun uploadJson(timestampMillis: Long): JSONObject = JSONObject()
.put(Constants.MessageKey.TIMESTAMP, timestampMillis)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,177 @@ class MParticleBaseClientImplTest : BaseCleanInstallEachTest() {
)
assertEquals(null, result)
}

@Test
@Throws(MalformedURLException::class)
fun testCustomBaseURLConfigEndpoint() {
val options =
MParticleOptions
.builder(mContext)
.credentials(apiKey, "secret")
.networkOptions(
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com")
.build(),
).build()
MParticle.start(options)
val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl
val configUrl = baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.CONFIG)
Assert.assertTrue(configUrl.toString().contains("rkt.example.com/config/v4/"))
Assert.assertFalse(configUrl.toString().contains("config2.mparticle.com"))
}

@Test
@Throws(MalformedURLException::class)
fun testCustomBaseURLEventsEndpoint() {
val options =
MParticleOptions
.builder(mContext)
.credentials(apiKey, "secret")
.networkOptions(
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com")
.build(),
).build()
MParticle.start(options)
val uploadSettings = UploadSettings(apiKey, "secret", options.networkOptions, "", "")
val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl
val eventsUrl =
baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.EVENTS, null, null, uploadSettings)
Assert.assertTrue(eventsUrl.toString().contains("rkt.example.com/nativeevents/v2/"))
Assert.assertFalse(eventsUrl.toString().contains("nativesdks"))
}

@Test
@Throws(MalformedURLException::class)
fun testCustomBaseURLIdentityEndpoint() {
val options =
MParticleOptions
.builder(mContext)
.credentials(apiKey, "secret")
.networkOptions(
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com")
.build(),
).build()
MParticle.start(options)
val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl
val identityUrl =
baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.IDENTITY, "login")
Assert.assertTrue(identityUrl.toString().contains("rkt.example.com/identity/v1/login"))
Assert.assertFalse(identityUrl.toString().contains("identity.us1.mparticle.com"))
}

@Test
@Throws(MalformedURLException::class)
fun testCustomBaseURLAliasEndpoint() {
val options =
MParticleOptions
.builder(mContext)
.credentials(apiKey, "secret")
.networkOptions(
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com")
.build(),
).build()
MParticle.start(options)
val uploadSettings = UploadSettings(apiKey, "secret", options.networkOptions, "", "")
val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl
val aliasUrl =
baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.ALIAS, null, null, uploadSettings)
Assert.assertTrue(aliasUrl.toString().contains("rkt.example.com/nativeevents/v1/identity/"))
Assert.assertTrue(aliasUrl.toString().endsWith("/alias"))
Assert.assertFalse(aliasUrl.toString().contains("nativesdks"))
}

@Test
@Throws(MalformedURLException::class)
fun testCustomBaseURLAudienceEndpoint() {
val options =
MParticleOptions
.builder(mContext)
.credentials(apiKey, "secret")
.networkOptions(
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com")
.build(),
).build()
MParticle.start(options)
val baseClientImpl = AccessUtils.getApiClient() as MParticleBaseClientImpl
val audienceUrl =
baseClientImpl.getUrl(MParticleBaseClientImpl.Endpoint.AUDIENCE, 12345L)
Assert.assertTrue(audienceUrl.toString().contains("rkt.example.com/nativeevents/v1/"))
Assert.assertTrue(audienceUrl.toString().contains("/audience"))
Assert.assertFalse(audienceUrl.toString().contains("nativesdks"))
}

@Test
fun testCustomBaseURLRejectsNonHTTPS() {
val opts =
NetworkOptions
.builder()
.setCustomBaseURL("http://rkt.example.com")
.build()
assertEquals(null, opts.customBaseURL)
}

@Test
fun testCustomBaseURLStripsPath() {
val opts =
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com/some/path?q=1")
.build()
assertEquals("rkt.example.com", opts.customBaseURL)
}

@Test
fun testCustomBaseURLPreservesPort() {
val opts =
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com:8443")
.build()
assertEquals("rkt.example.com:8443", opts.customBaseURL)
}

@Test
fun testCustomBaseURLRejectsMalformed() {
val opts =
NetworkOptions
.builder()
.setCustomBaseURL("not a url")
.build()
assertEquals(null, opts.customBaseURL)
}

@Test
fun testCustomBaseURLSurvivesJsonRoundTrip() {
val original =
NetworkOptions
.builder()
.setCustomBaseURL("https://rkt.example.com:8443")
.build()
Assert.assertEquals("rkt.example.com:8443", original.customBaseURL)

val json = original.toJson().toString()
val restored = NetworkOptions.withNetworkOptions(json)
Assert.assertNotNull(restored)
Assert.assertEquals("rkt.example.com:8443", restored!!.customBaseURL)
}

@Test
fun testCustomBaseURLOmittedFromJsonWhenUnset() {
val opts = NetworkOptions.builder().build()
val json = opts.toJson()
Assert.assertFalse(json.has("customBaseURL"))

val restored = NetworkOptions.withNetworkOptions(json.toString())
Assert.assertNull(restored!!.customBaseURL)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,38 +114,26 @@
protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath,HashMap<String, String> audienceQueryParams, @Nullable UploadSettings uploadSettings) throws MalformedURLException {
NetworkOptions networkOptions = uploadSettings == null ? mConfigManager.getNetworkOptions() : uploadSettings.getNetworkOptions();
DomainMapping domainMapping = networkOptions.getDomain(endpoint);
String url = NetworkOptionsManager.getDefaultUrl(endpoint);
String apiKey = uploadSettings == null ? mApiKey : uploadSettings.getApiKey();
final boolean usingCustomBaseURL = !MPUtility.isEmpty(networkOptions.getCustomBaseURL());

// `defaultDomain` variable is for URL generation when domain mapping is specified.
String defaultDomain = url;
boolean isDefaultDomain = true;

// Check if domain mapping is specified and update the URL based on domain mapping
String domainMappingUrl = domainMapping != null ? domainMapping.getUrl() : null;
if (!MPUtility.isEmpty(domainMappingUrl)) {
isDefaultDomain = url.equals(domainMappingUrl);
url = domainMappingUrl;
}

if (endpoint != Endpoint.CONFIG) {
// Set URL with pod prefix if it’s the default domain and endpoint is not CONFIG
if (isDefaultDomain) {
url = getPodUrl(url, mConfigManager.getPodPrefix(apiKey), mConfigManager.isDirectUrlRoutingEnabled());
} else {
// When domain mapping is specified, generate the default domain. Whether podRedirection is enabled or not, always use the original URL.
defaultDomain = getPodUrl(defaultDomain, null, false);
}
}
ResolvedHost host = resolveHost(endpoint, networkOptions, domainMapping, apiKey);
String url = host.url;
String defaultDomain = host.defaultDomain;
boolean isDefaultDomain = host.isDefaultDomain;

Uri uri;
String subdirectory;
String pathPrefix;
String pathPostfix;
boolean overridesSubdirectory = domainMapping != null && domainMapping.isOverridesSubdirectory();
if (usingCustomBaseURL && overridesSubdirectory) {
Logger.warning("NetworkOptions: customBaseURL with overridesSubdirectory is unsupported for CDN routing; overridesSubdirectory will be ignored for " + endpoint.name() + ".");
overridesSubdirectory = false;
}
switch (endpoint) {
case CONFIG:
pathPrefix = SERVICE_VERSION_4 + "/";
pathPrefix = usingCustomBaseURL ? "/config/v4/" : SERVICE_VERSION_4 + "/";

Check warning on line 136 in android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this hard-coded path-delimiter.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-android-sdk&issues=AZ4i2zrRtm2WlJS-GGUS&open=AZ4i2zrRtm2WlJS-GGUS&pullRequest=701
subdirectory = overridesSubdirectory ? "" : pathPrefix;
pathPostfix = mApiKey + "/config";
Uri.Builder builder = new Uri.Builder()
Expand All @@ -165,9 +153,9 @@
}
}
}
return MPUrl.getUrl(builder.build().toString(), generateDefaultURL(isDefaultDomain, builder.build(), defaultDomain, (pathPrefix + pathPostfix)));
return MPUrl.getUrl(builder.build().toString(), generateDefaultURL(isDefaultDomain, builder.build(), defaultDomain, (SERVICE_VERSION_4 + "/" + pathPostfix)));
case EVENTS:
pathPrefix = SERVICE_VERSION_2 + "/";
pathPrefix = usingCustomBaseURL ? "/nativeevents/v2/" : SERVICE_VERSION_2 + "/";

Check warning on line 158 in android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this hard-coded path-delimiter.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-android-sdk&issues=AZ4i2zrRtm2WlJS-GGUT&open=AZ4i2zrRtm2WlJS-GGUT&pullRequest=701
subdirectory = overridesSubdirectory ? "" : pathPrefix;
pathPostfix = apiKey + "/events";
uri = new Uri.Builder()
Expand All @@ -176,36 +164,38 @@
.path(subdirectory + pathPostfix)
.build();

return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (pathPrefix + pathPostfix)));
return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_2 + "/" + pathPostfix)));
case ALIAS:
pathPrefix = SERVICE_VERSION_1 + "/identity/";
pathPrefix = usingCustomBaseURL ? "/nativeevents/v1/identity/" : SERVICE_VERSION_1 + "/identity/";
subdirectory = overridesSubdirectory ? "" : pathPrefix;
pathPostfix = apiKey + "/alias";
uri = new Uri.Builder()
.scheme(BuildConfig.SCHEME)
.encodedAuthority(url)
.path(subdirectory + pathPostfix)
.build();
return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (pathPrefix + pathPostfix)));
return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_1 + "/identity/" + pathPostfix)));
case IDENTITY:
pathPrefix = SERVICE_VERSION_1 + "/";
subdirectory = overridesSubdirectory ? "" : SERVICE_VERSION_1 + "/";
pathPrefix = usingCustomBaseURL ? "/identity/v1/" : SERVICE_VERSION_1 + "/";

Check warning on line 179 in android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this hard-coded path-delimiter.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-android-sdk&issues=AZ4i2zrRtm2WlJS-GGUU&open=AZ4i2zrRtm2WlJS-GGUU&pullRequest=701
subdirectory = overridesSubdirectory ? "" : pathPrefix;
pathPostfix = identityPath;
uri = new Uri.Builder()
.scheme(BuildConfig.SCHEME)
.encodedAuthority(url)
.path(subdirectory + pathPostfix)
.build();
return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (pathPrefix + pathPostfix)));
return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_1 + "/" + pathPostfix)));
case AUDIENCE:
pathPostfix = SERVICE_VERSION_1 + "/" + mApiKey + "/audience";
pathPostfix = usingCustomBaseURL
? "/nativeevents/v1/" + mApiKey + "/audience"

Check failure on line 190 in android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "/audience" 3 times.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-android-sdk&issues=AZ4i2zrRtm2WlJS-GGUR&open=AZ4i2zrRtm2WlJS-GGUR&pullRequest=701
: SERVICE_VERSION_1 + "/" + mApiKey + "/audience";
uri = new Uri.Builder()
.scheme(BuildConfig.SCHEME)
.encodedAuthority(url)
.path(pathPostfix)
.appendQueryParameter("mpid", String.valueOf(mConfigManager.getMpid()))
.build();
return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, pathPostfix));
return MPUrl.getUrl(uri.toString(), generateDefaultURL(isDefaultDomain, uri, defaultDomain, (SERVICE_VERSION_1 + "/" + mApiKey + "/audience")));
default:
return null;
}
Expand Down Expand Up @@ -246,6 +236,57 @@
return null;
}

/**
* Resolves the host(s) used to build the endpoint URL. Returns three values:
* {@code url} — host used for the request, {@code defaultDomain} — host used as the
* fallback for {@link #generateDefaultURL}, and {@code isDefaultDomain} — true when
* {@code url} is the unmodified mParticle default.
*
* <p>Priority: {@code customBaseURL} → per-endpoint {@code DomainMapping} → default.
*/
private ResolvedHost resolveHost(Endpoint endpoint, NetworkOptions networkOptions, DomainMapping domainMapping, String apiKey) {
String defaultUrl = NetworkOptionsManager.getDefaultUrl(endpoint);
String customBaseURL = networkOptions.getCustomBaseURL();

if (!MPUtility.isEmpty(customBaseURL)) {
if (domainMapping != null && !MPUtility.isEmpty(domainMapping.getUrl())) {
Logger.warning("NetworkOptions: customBaseURL is set; domain mapping for " + endpoint.name() + " is ignored.");
}
// When custom CNAME is used, the default-domain URL still needs the pod prefix
// so MPConnectionTest matching and pinning fallbacks continue to work.
String defaultDomain = endpoint == Endpoint.CONFIG ? defaultUrl : getPodUrl(defaultUrl, null, false);
return new ResolvedHost(customBaseURL, defaultDomain, false);
}

String domainMappingUrl = domainMapping != null ? domainMapping.getUrl() : null;
boolean isDefaultDomain = MPUtility.isEmpty(domainMappingUrl) || defaultUrl.equals(domainMappingUrl);
String url = isDefaultDomain ? defaultUrl : domainMappingUrl;
String defaultDomain = defaultUrl;

if (endpoint != Endpoint.CONFIG) {
if (isDefaultDomain) {
// Default domain gets the pod prefix.
url = getPodUrl(url, mConfigManager.getPodPrefix(apiKey), mConfigManager.isDirectUrlRoutingEnabled());
} else {
// Domain-mapped: always generate the default with the original (un-pod-prefixed) host.
defaultDomain = getPodUrl(defaultDomain, null, false);
}
}
return new ResolvedHost(url, defaultDomain, isDefaultDomain);
}

private static final class ResolvedHost {
final String url;
final String defaultDomain;
final boolean isDefaultDomain;

ResolvedHost(String url, String defaultDomain, boolean isDefaultDomain) {
this.url = url;
this.defaultDomain = defaultDomain;
this.isDefaultDomain = isDefaultDomain;
}
}

public enum Endpoint {
CONFIG(1),
IDENTITY(2),
Expand Down
Loading
Loading