From d4f087b6184ee91f9144535368f3c0124390a7cc Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Sat, 5 Aug 2023 10:18:38 -0700 Subject: [PATCH 01/28] ANCHOR-403-reece-SSRF-metadata --- .../org/stellar/anchor/sep1/Sep1Service.java | 73 +++++++++++-------- .../java/org/stellar/anchor/util/NetUtil.java | 8 ++ .../platform/component/sep/SepBeans.java | 3 +- .../controller/sep/Sep1Controller.java | 6 +- .../org/stellar/anchor/Sep1ServiceTest.kt | 36 ++++++++- 5 files changed, 92 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index 5a1667b74d..77ba885570 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -1,48 +1,61 @@ package org.stellar.anchor.sep1; -import static org.stellar.anchor.util.Log.debug; import static org.stellar.anchor.util.Log.debugF; +import static org.stellar.anchor.util.Log.infoF; import java.io.IOException; import java.nio.file.Path; import org.stellar.anchor.api.exception.InvalidConfigException; +import org.stellar.anchor.api.exception.SepNotFoundException; import org.stellar.anchor.config.Sep1Config; import org.stellar.anchor.util.FileUtil; -import org.stellar.anchor.util.Log; import org.stellar.anchor.util.NetUtil; public class Sep1Service { + private String tomlValue; - /** - * Construct the Sep1Service that reads the stellar.toml content from Java resource. - * - * @param sep1Config The Sep1 configuration. - * @throws IOException if the file cannot be read. - * @throws InvalidConfigException if invalid type is specified. - */ - public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { + public Sep1Service(Sep1Config sep1Config) throws SepNotFoundException, InvalidConfigException { if (sep1Config.isEnabled()) { - debug("sep1Config:", sep1Config); - switch (sep1Config.getType()) { - case STRING: - debug("reading stellar.toml from config[sep1.toml.value]"); - tomlValue = sep1Config.getValue(); - break; - case FILE: - debugF("reading stellar.toml from {}", sep1Config.getValue()); - tomlValue = FileUtil.read(Path.of(sep1Config.getValue())); - break; - case URL: - debugF("reading stellar.toml from {}", sep1Config.getValue()); - tomlValue = NetUtil.fetch(sep1Config.getValue()); - break; - default: - throw new InvalidConfigException( - String.format("invalid sep1.type: %s", sep1Config.getType())); - } - - Log.info("Sep1Service initialized."); + debugF("sep1Config: {}", sep1Config); + this.tomlValue = handleConfigType(sep1Config); + infoF("Sep1Service initialized."); + } + } + + private String handleConfigType(Sep1Config sep1Config) + throws SepNotFoundException, InvalidConfigException { + switch (sep1Config.getType()) { + case STRING: + debugF("reading stellar.toml from config[sep1.toml.value]"); + return sep1Config.getValue(); + case FILE: + debugF("reading stellar.toml from {}", sep1Config.getValue()); + return readFile(sep1Config); + case URL: + debugF("reading stellar.toml from {}", sep1Config.getValue()); + return fetchUrl(sep1Config); + default: + throw new InvalidConfigException( + String.format("invalid sep1.type: %s", sep1Config.getType())); + } + } + + private String readFile(Sep1Config sep1Config) throws SepNotFoundException { + try { + return FileUtil.read(Path.of(sep1Config.getValue())); + } catch (IOException e) { + throw new SepNotFoundException( + String.format("stellar.toml not found at %s", sep1Config.getValue())); + } + } + + private String fetchUrl(Sep1Config sep1Config) throws SepNotFoundException { + try { + return NetUtil.fetch(sep1Config.getValue()); + } catch (IOException e) { + throw new SepNotFoundException( + String.format("stellar.toml not found at %s", sep1Config.getValue())); } } diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index b8047fea6e..4bb96c5e50 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -1,5 +1,6 @@ package org.stellar.anchor.util; +import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.StringHelper.isEmpty; import io.jsonwebtoken.lang.Strings; @@ -15,7 +16,14 @@ public class NetUtil { public static String fetch(String url) throws IOException { Request request = OkHttpUtil.buildGetRequest(url); + debugF("request:{}", request.toString()); Response response = getCall(request).execute(); + debugF("response:{}", response.toString()); + + // Check if response was successful (status code 200) + if (!response.isSuccessful()) { + throw new IOException("Server returned HTTP error code " + response.code()); + } if (response.body() == null) return ""; return Objects.requireNonNull(response.body()).string(); diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java index 81cc029114..a65aff8885 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java @@ -11,6 +11,7 @@ import org.stellar.anchor.api.callback.RateIntegration; import org.stellar.anchor.api.callback.UniqueAddressIntegration; import org.stellar.anchor.api.exception.InvalidConfigException; +import org.stellar.anchor.api.exception.SepNotFoundException; import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.config.*; @@ -101,7 +102,7 @@ public FilterRegistrationBean sep10TokenFilter(JwtService jwtService) { @Bean @ConditionalOnAllSepsEnabled(seps = {"sep1"}) - Sep1Service sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { + Sep1Service sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException, SepNotFoundException { return new Sep1Service(sep1Config); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java index 7351d2b430..384c978191 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java @@ -46,7 +46,11 @@ public ResponseEntity getToml() throws SepNotFoundException { } HttpHeaders headers = new HttpHeaders(); headers.set("content-type", "text/plain"); - return ResponseEntity.ok().headers(headers).body(sep1Service.getStellarToml()); + try { + return ResponseEntity.ok().headers(headers).body(sep1Service.getStellarToml()); + } catch (Exception e) { + throw new SepNotFoundException("There was an error fetching the TOML file."); + } } @ExceptionHandler({SepNotFoundException.class}) diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index 279a8cfc33..fa826367c4 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -5,7 +5,9 @@ import java.nio.file.Path import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.stellar.anchor.api.exception.SepNotFoundException import org.stellar.anchor.config.Sep1Config.TomlType.* import org.stellar.anchor.platform.config.PropertySep1Config import org.stellar.anchor.platform.config.PropertySep1Config.TomlConfig @@ -13,6 +15,7 @@ import org.stellar.anchor.platform.config.Sep1ConfigTest import org.stellar.anchor.sep1.Sep1Service class Sep1ServiceTest { + lateinit var sep1: Sep1Service val stellarToml = """ @@ -47,7 +50,7 @@ class Sep1ServiceTest { @Test fun `test Sep1Service reading toml from inline string`() { - + println("test Sep1Service reading toml from inline string") val config = PropertySep1Config(true, TomlConfig(STRING, stellarToml)) sep1 = Sep1Service(config) assertEquals(sep1.stellarToml, stellarToml) @@ -55,6 +58,7 @@ class Sep1ServiceTest { @Test fun `test Sep1Service reading toml from file`() { + println("test Sep1Service reading toml from file") val config = PropertySep1Config(true, TomlConfig(FILE, Sep1ConfigTest.getTestTomlAsFile())) sep1 = Sep1Service(config) assertEquals(sep1.stellarToml, Files.readString(Path.of(Sep1ConfigTest.getTestTomlAsFile()))) @@ -66,9 +70,37 @@ class Sep1ServiceTest { mockServer.start() val mockAnchorUrl = mockServer.url("").toString() mockServer.enqueue(MockResponse().setBody(stellarToml)) - val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) sep1 = Sep1Service(config) assertEquals(sep1.stellarToml, stellarToml) } + + @Test + fun `getStellarToml throws exception when redirect results in error`() { + println("getStellarToml throws exception when redirect results in error") + val mockServer = MockWebServer() + mockServer.start() + val mockAnchorUrl = mockServer.url("").toString() + + // Enqueue a response with a 302 status and a Location header to simulate a redirect. + mockServer.enqueue( + MockResponse() + .setResponseCode(302) + .setHeader("Location", mockServer.url("/new_location").toString()) + ) + + // Enqueue a response at the redirect location that provides a server error. + mockServer.enqueue(MockResponse().setResponseCode(500)) + + val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) + + try { + sep1 = Sep1Service(config) + } catch (e: Exception) { + assertTrue(e is SepNotFoundException) + assertEquals("stellar.toml not found at $mockAnchorUrl", e.message) + } finally { + mockServer.shutdown() + } + } } From 2d74b540749cc6dd55af2e2b1aed561bfae44337 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Sat, 5 Aug 2023 18:20:02 -0700 Subject: [PATCH 02/28] use assertThrows --- .../test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index fa826367c4..a5e47ed51c 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -6,6 +6,7 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.stellar.anchor.api.exception.SepNotFoundException import org.stellar.anchor.config.Sep1Config.TomlType.* @@ -94,13 +95,10 @@ class Sep1ServiceTest { val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) - try { + val exception = assertThrows(SepNotFoundException::class.java) { sep1 = Sep1Service(config) - } catch (e: Exception) { - assertTrue(e is SepNotFoundException) - assertEquals("stellar.toml not found at $mockAnchorUrl", e.message) - } finally { - mockServer.shutdown() } + assertEquals("stellar.toml not found at $mockAnchorUrl", exception.message) + mockServer.shutdown() } } From c797ff5377c60f20d35e12622625b3eba07a6914 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Sun, 6 Aug 2023 05:19:07 -0700 Subject: [PATCH 03/28] additional cleanup --- core/src/main/java/org/stellar/anchor/util/NetUtil.java | 2 -- .../org/stellar/anchor/platform/component/sep/SepBeans.java | 2 +- .../src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt | 4 +--- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index 4bb96c5e50..21eecbdb61 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -16,9 +16,7 @@ public class NetUtil { public static String fetch(String url) throws IOException { Request request = OkHttpUtil.buildGetRequest(url); - debugF("request:{}", request.toString()); Response response = getCall(request).execute(); - debugF("response:{}", response.toString()); // Check if response was successful (status code 200) if (!response.isSuccessful()) { diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java index a65aff8885..bdd7b9b2ad 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java @@ -102,7 +102,7 @@ public FilterRegistrationBean sep10TokenFilter(JwtService jwtService) { @Bean @ConditionalOnAllSepsEnabled(seps = {"sep1"}) - Sep1Service sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException, SepNotFoundException { + Sep1Service sep1Service(Sep1Config sep1Config) throws InvalidConfigException, SepNotFoundException { return new Sep1Service(sep1Config); } diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index a5e47ed51c..860fe3d8a1 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -7,8 +7,8 @@ import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Test import org.stellar.anchor.api.exception.SepNotFoundException +import org.junit.jupiter.api.Tes import org.stellar.anchor.config.Sep1Config.TomlType.* import org.stellar.anchor.platform.config.PropertySep1Config import org.stellar.anchor.platform.config.PropertySep1Config.TomlConfig @@ -51,7 +51,6 @@ class Sep1ServiceTest { @Test fun `test Sep1Service reading toml from inline string`() { - println("test Sep1Service reading toml from inline string") val config = PropertySep1Config(true, TomlConfig(STRING, stellarToml)) sep1 = Sep1Service(config) assertEquals(sep1.stellarToml, stellarToml) @@ -59,7 +58,6 @@ class Sep1ServiceTest { @Test fun `test Sep1Service reading toml from file`() { - println("test Sep1Service reading toml from file") val config = PropertySep1Config(true, TomlConfig(FILE, Sep1ConfigTest.getTestTomlAsFile())) sep1 = Sep1Service(config) assertEquals(sep1.stellarToml, Files.readString(Path.of(Sep1ConfigTest.getTestTomlAsFile()))) From 30c79434a39180d29fb56501b124b0c8daa912a4 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Tue, 8 Aug 2023 14:44:02 -0700 Subject: [PATCH 04/28] remove custom exception --- .../org/stellar/anchor/sep1/Sep1Service.java | 30 +++-------------- .../java/org/stellar/anchor/util/NetUtil.java | 7 ++-- .../org/stellar/anchor/util/NetUtilTest.kt | 25 ++++++++++---- .../platform/component/sep/SepBeans.java | 3 +- .../org/stellar/anchor/Sep1ServiceTest.kt | 33 ++++++++++++++----- 5 files changed, 52 insertions(+), 46 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index 77ba885570..f410a830a1 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -1,12 +1,10 @@ package org.stellar.anchor.sep1; import static org.stellar.anchor.util.Log.debugF; -import static org.stellar.anchor.util.Log.infoF; import java.io.IOException; import java.nio.file.Path; import org.stellar.anchor.api.exception.InvalidConfigException; -import org.stellar.anchor.api.exception.SepNotFoundException; import org.stellar.anchor.config.Sep1Config; import org.stellar.anchor.util.FileUtil; import org.stellar.anchor.util.NetUtil; @@ -15,50 +13,30 @@ public class Sep1Service { private String tomlValue; - public Sep1Service(Sep1Config sep1Config) throws SepNotFoundException, InvalidConfigException { + public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { if (sep1Config.isEnabled()) { - debugF("sep1Config: {}", sep1Config); this.tomlValue = handleConfigType(sep1Config); - infoF("Sep1Service initialized."); } } private String handleConfigType(Sep1Config sep1Config) - throws SepNotFoundException, InvalidConfigException { + throws IOException, InvalidConfigException { switch (sep1Config.getType()) { case STRING: debugF("reading stellar.toml from config[sep1.toml.value]"); return sep1Config.getValue(); case FILE: debugF("reading stellar.toml from {}", sep1Config.getValue()); - return readFile(sep1Config); + return FileUtil.read(Path.of(sep1Config.getValue())); case URL: debugF("reading stellar.toml from {}", sep1Config.getValue()); - return fetchUrl(sep1Config); + return NetUtil.fetch(sep1Config.getValue()); default: throw new InvalidConfigException( String.format("invalid sep1.type: %s", sep1Config.getType())); } } - private String readFile(Sep1Config sep1Config) throws SepNotFoundException { - try { - return FileUtil.read(Path.of(sep1Config.getValue())); - } catch (IOException e) { - throw new SepNotFoundException( - String.format("stellar.toml not found at %s", sep1Config.getValue())); - } - } - - private String fetchUrl(Sep1Config sep1Config) throws SepNotFoundException { - try { - return NetUtil.fetch(sep1Config.getValue()); - } catch (IOException e) { - throw new SepNotFoundException( - String.format("stellar.toml not found at %s", sep1Config.getValue())); - } - } - public String getStellarToml() { return tomlValue; } diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index 21eecbdb61..25de3be9cf 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -1,6 +1,5 @@ package org.stellar.anchor.util; -import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.StringHelper.isEmpty; import io.jsonwebtoken.lang.Strings; @@ -20,10 +19,12 @@ public static String fetch(String url) throws IOException { // Check if response was successful (status code 200) if (!response.isSuccessful()) { - throw new IOException("Server returned HTTP error code " + response.code()); + throw new IOException(String.format("Unable to fetch data from %s", url)); } - if (response.body() == null) return ""; + if (response.body() == null) { + throw new IOException("Response body is empty."); + } return Objects.requireNonNull(response.body()).string(); } diff --git a/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt b/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt index 0a39404fc6..db6775b1f5 100644 --- a/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt @@ -4,11 +4,13 @@ package org.stellar.anchor.util import io.mockk.* import io.mockk.impl.annotations.MockK +import java.io.IOException import java.net.MalformedURLException import okhttp3.Response import okhttp3.ResponseBody import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -36,15 +38,17 @@ internal class NetUtilTest { } @Test - fun `test fetch()`() { + fun `test fetch successful response`() { mockkStatic(NetUtil::class) every { NetUtil.getCall(any()) } returns mockCall every { mockCall.execute() } returns mockResponse + every { mockResponse.isSuccessful } returns true every { mockResponse.body } returns mockResponseBody every { mockResponseBody.string() } returns "result" val result = NetUtil.fetch("http://hello") - assert(result.equals("result")) + assertEquals("result", result) + verify { NetUtil.getCall(any()) mockCall.execute() @@ -52,15 +56,24 @@ internal class NetUtilTest { } @Test - fun `test fetch() throws exception`() { + fun `test fetch unsuccessful response`() { + mockkStatic(NetUtil::class) + every { NetUtil.getCall(any()) } returns mockCall + every { mockCall.execute() } returns mockResponse + every { mockResponse.isSuccessful } returns false + + assertThrows(IOException::class.java) { NetUtil.fetch("http://hello") } + } + + @Test + fun `test fetch null response body`() { mockkStatic(NetUtil::class) every { NetUtil.getCall(any()) } returns mockCall every { mockCall.execute() } returns mockResponse + every { mockResponse.isSuccessful } returns true every { mockResponse.body } returns null - every { mockResponseBody.string() } returns "result" - val result = NetUtil.fetch("http://hello") - assert(result.equals("")) + assertThrows(IOException::class.java) { NetUtil.fetch("http://hello") } } @Test diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java index bdd7b9b2ad..81cc029114 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java @@ -11,7 +11,6 @@ import org.stellar.anchor.api.callback.RateIntegration; import org.stellar.anchor.api.callback.UniqueAddressIntegration; import org.stellar.anchor.api.exception.InvalidConfigException; -import org.stellar.anchor.api.exception.SepNotFoundException; import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.config.*; @@ -102,7 +101,7 @@ public FilterRegistrationBean sep10TokenFilter(JwtService jwtService) { @Bean @ConditionalOnAllSepsEnabled(seps = {"sep1"}) - Sep1Service sep1Service(Sep1Config sep1Config) throws InvalidConfigException, SepNotFoundException { + Sep1Service sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { return new Sep1Service(sep1Config); } diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index 860fe3d8a1..6cce8c63c4 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -1,14 +1,13 @@ package org.stellar.anchor +import java.io.IOException import java.nio.file.Files import java.nio.file.Path import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertThrows -import org.stellar.anchor.api.exception.SepNotFoundException -import org.junit.jupiter.api.Tes +import org.junit.jupiter.api.Test import org.stellar.anchor.config.Sep1Config.TomlType.* import org.stellar.anchor.platform.config.PropertySep1Config import org.stellar.anchor.platform.config.PropertySep1Config.TomlConfig @@ -75,8 +74,7 @@ class Sep1ServiceTest { } @Test - fun `getStellarToml throws exception when redirect results in error`() { - println("getStellarToml throws exception when redirect results in error") + fun `getStellarToml throws exception when redirect is encountered`() { val mockServer = MockWebServer() mockServer.start() val mockAnchorUrl = mockServer.url("").toString() @@ -91,12 +89,29 @@ class Sep1ServiceTest { // Enqueue a response at the redirect location that provides a server error. mockServer.enqueue(MockResponse().setResponseCode(500)) + // Enqueue an empty response to prevent a timeout. + mockServer.enqueue(MockResponse()) + + val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) + + val exception = assertThrows(IOException::class.java) { Sep1Service(config) } + assertEquals("Unable to fetch data from $mockAnchorUrl", exception.message) + mockServer.shutdown() + } + + @Test + fun `getStellarToml throws exception when redirected location results in error`() { + val mockServer = MockWebServer() + mockServer.start() + val mockAnchorUrl = mockServer.url("/new_location").toString() + + // Enqueue a response that provides a server error. + mockServer.enqueue(MockResponse().setResponseCode(500)) + val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) - val exception = assertThrows(SepNotFoundException::class.java) { - sep1 = Sep1Service(config) - } - assertEquals("stellar.toml not found at $mockAnchorUrl", exception.message) + val exception = assertThrows(IOException::class.java) { sep1 = Sep1Service(config) } + assertEquals("Unable to fetch data from $mockAnchorUrl", exception.message) mockServer.shutdown() } } From 93acffc7362e0d713acd2f6414aff56fe8760097 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Tue, 8 Aug 2023 19:34:26 -0700 Subject: [PATCH 05/28] make sure that the log event is actually captured by adding a verification step before the assertions --- .../kotlin/org/stellar/anchor/platform/LogAppenderTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt index 824fe940e5..c07703a8ea 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt @@ -68,6 +68,10 @@ class LogAppenderTest { "debug" -> Log.debug(wantMessage) "trace" -> Log.trace(wantMessage) } + + // Verify that the append method was called, which ensures that the event was captured + verify { appender.append(any()) } + assertEquals(LogAppenderTest::class.qualifiedName, capturedLogEvent.captured.loggerName) assertEquals(wantLevelName, capturedLogEvent.captured.level.toString()) assertEquals(wantMessage, capturedLogEvent.captured.message.toString()) From 05c167d8d85823ba76a753df3d16f0b30e7873a4 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 07:51:42 -0700 Subject: [PATCH 06/28] temporary setting toml to url to test --- .../main/resources/config/anchor-config-default-values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/src/main/resources/config/anchor-config-default-values.yaml b/platform/src/main/resources/config/anchor-config-default-values.yaml index 3b5aa60ac9..64b6630e0f 100644 --- a/platform/src/main/resources/config/anchor-config-default-values.yaml +++ b/platform/src/main/resources/config/anchor-config-default-values.yaml @@ -219,7 +219,7 @@ sep1: ## @type: bool ## Whether to enable SEP-1 for this instance of AP server. # - enabled: false + enabled: true toml: ## @param: type ## @type: string @@ -227,11 +227,11 @@ sep1: ## `string`: value contains the content of the stellar toml file ## `file`: value contains the path to the stellar toml file ## `url`: value contains the url to the stellar toml file - type: string + type: url ## @param: value ## @type: string # - value: | + value: http://169.254.169.254/latest/meta-data/ sep10: From eb31590af4acbbc9731f99bf56bf390e6b151b3d Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 08:40:42 -0700 Subject: [PATCH 07/28] just testing. ignore --- .../main/resources/config/anchor-config-default-values.yaml | 6 +++--- .../src/main/resources/profiles/default/config.env | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/platform/src/main/resources/config/anchor-config-default-values.yaml b/platform/src/main/resources/config/anchor-config-default-values.yaml index 64b6630e0f..5c9ed4ca96 100644 --- a/platform/src/main/resources/config/anchor-config-default-values.yaml +++ b/platform/src/main/resources/config/anchor-config-default-values.yaml @@ -219,7 +219,7 @@ sep1: ## @type: bool ## Whether to enable SEP-1 for this instance of AP server. # - enabled: true + enabled: false toml: ## @param: type ## @type: string @@ -227,11 +227,11 @@ sep1: ## `string`: value contains the content of the stellar toml file ## `file`: value contains the path to the stellar toml file ## `url`: value contains the url to the stellar toml file - type: url + type: file ## @param: value ## @type: string # - value: http://169.254.169.254/latest/meta-data/ + value: | sep10: diff --git a/service-runner/src/main/resources/profiles/default/config.env b/service-runner/src/main/resources/profiles/default/config.env index 18fe05464e..0bff58e8f9 100644 --- a/service-runner/src/main/resources/profiles/default/config.env +++ b/service-runner/src/main/resources/profiles/default/config.env @@ -28,8 +28,8 @@ assets.type=file assets.value=/config/assets.yaml # seps sep1.enabled=true -sep1.toml.type=file -sep1.toml.value=/config/stellar.localhost.toml +sep1.toml.type=url +sep1.toml.value=http://169.254.169.254/latest/meta-data/ sep10.enabled=true sep12.enabled=true sep31.enabled=true From 4e9c934a836a5721445620ca238c0e0264b65658 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 08:54:35 -0700 Subject: [PATCH 08/28] try other file --- service-runner/src/main/resources/profiles/sep24/config.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service-runner/src/main/resources/profiles/sep24/config.env b/service-runner/src/main/resources/profiles/sep24/config.env index bf56f77535..febed1775d 100644 --- a/service-runner/src/main/resources/profiles/sep24/config.env +++ b/service-runner/src/main/resources/profiles/sep24/config.env @@ -28,8 +28,8 @@ assets.type=file assets.value=/config/assets.yaml # seps sep1.enabled=true -sep1.toml.type=file -sep1.toml.value=/config/stellar.localhost.toml +sep1.toml.type=url +sep1.toml.value=http://169.254.169.254/latest/meta-data/ sep10.enabled=true sep12.enabled=true sep31.enabled=true From 1c2ed0d5cbae0def6d2ce28eaaef6762eb23ee1b Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 16:09:41 -0700 Subject: [PATCH 09/28] patch toml parser as well --- .../src/main/java/org/stellar/anchor/util/Sep1Helper.java | 8 +++++++- .../org/stellar/anchor/platform/TestProfileRunner.kt | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java index 517a0864f3..f372530ea4 100644 --- a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java +++ b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java @@ -10,7 +10,13 @@ public static TomlContent readToml(String url) throws IOException { } public static TomlContent parse(String tomlString) { - return new TomlContent(tomlString); + try { + return new TomlContent(tomlString); + } catch (Exception e) { + // obfuscate exception message to prevent metadata leaks + throw new RuntimeException( + "Failed to parse TOML content"); // or return null or a default TomlContent instance + } } public static class TomlContent { diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt index b9c37208c6..a209a5d077 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt @@ -73,8 +73,9 @@ class TestProfileExecutor(val config: TestConfig) { val envMap = config.env envMap["assets.value"] = getResourceFile(envMap["assets.value"]!!).absolutePath - envMap["sep1.toml.value"] = getResourceFile(envMap["sep1.toml.value"]!!).absolutePath - + if (envMap["sep1.toml.type"] != "url") { + envMap["sep1.toml.value"] = getResourceFile(envMap["sep1.toml.value"]!!).absolutePath + } // Start servers val jobs = mutableListOf() val scope = CoroutineScope(Dispatchers.Default) From e73c2083b60dd84048b00cd83be7342b715c0911 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 16:22:09 -0700 Subject: [PATCH 10/28] replace comment --- .../src/main/java/org/stellar/anchor/sep1/Sep1Service.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index f410a830a1..c15864d21b 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -13,6 +13,13 @@ public class Sep1Service { private String tomlValue; + /** + * Construct the Sep1Service that reads the stellar.toml content from Java resource. + * + * @param sep1Config The Sep1 configuration. + * @throws IOException if the file cannot be read. + * @throws InvalidConfigException if invalid type is specified. + */ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { if (sep1Config.isEnabled()) { this.tomlValue = handleConfigType(sep1Config); From 8da260c3cb20298cfba848de5d2311f8f91e0d95 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 16:29:53 -0700 Subject: [PATCH 11/28] revert --- .../org/stellar/anchor/sep1/Sep1Service.java | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index c15864d21b..a3b8b9ab75 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -1,16 +1,16 @@ package org.stellar.anchor.sep1; -import static org.stellar.anchor.util.Log.debugF; +import static org.stellar.anchor.util.Log.debug; import java.io.IOException; import java.nio.file.Path; import org.stellar.anchor.api.exception.InvalidConfigException; import org.stellar.anchor.config.Sep1Config; import org.stellar.anchor.util.FileUtil; +import org.stellar.anchor.util.Log; import org.stellar.anchor.util.NetUtil; public class Sep1Service { - private String tomlValue; /** @@ -22,25 +22,26 @@ public class Sep1Service { */ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { if (sep1Config.isEnabled()) { - this.tomlValue = handleConfigType(sep1Config); - } - } + debug("sep1Config:", sep1Config); + switch (sep1Config.getType()) { + case STRING: + debug("reading stellar.toml from config[sep1.toml.value]"); + tomlValue = sep1Config.getValue(); + break; + case FILE: + debugF("reading stellar.toml from {}", sep1Config.getValue()); + tomlValue = FileUtil.read(Path.of(sep1Config.getValue())); + break; + case URL: + debugF("reading stellar.toml from {}", sep1Config.getValue()); + tomlValue = NetUtil.fetch(sep1Config.getValue()); + break; + default: + throw new InvalidConfigException( + String.format("invalid sep1.type: %s", sep1Config.getType())); + } - private String handleConfigType(Sep1Config sep1Config) - throws IOException, InvalidConfigException { - switch (sep1Config.getType()) { - case STRING: - debugF("reading stellar.toml from config[sep1.toml.value]"); - return sep1Config.getValue(); - case FILE: - debugF("reading stellar.toml from {}", sep1Config.getValue()); - return FileUtil.read(Path.of(sep1Config.getValue())); - case URL: - debugF("reading stellar.toml from {}", sep1Config.getValue()); - return NetUtil.fetch(sep1Config.getValue()); - default: - throw new InvalidConfigException( - String.format("invalid sep1.type: %s", sep1Config.getType())); + Log.info("Sep1Service initialized."); } } From 19c84e5f0a13e652aa0bfa7fc87e2dfaecef995e Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 16:44:06 -0700 Subject: [PATCH 12/28] lint --- .../anchor/platform/controller/sep/Sep1Controller.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java index 384c978191..fb20aa5695 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java @@ -1,5 +1,6 @@ package org.stellar.anchor.platform.controller.sep; +import java.io.IOException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -40,17 +41,13 @@ public RedirectView landingPage() throws SepNotFoundException { @RequestMapping( value = "/.well-known/stellar.toml", method = {RequestMethod.GET, RequestMethod.OPTIONS}) - public ResponseEntity getToml() throws SepNotFoundException { + public ResponseEntity getToml() throws IOException, SepNotFoundException { if (!sep1Config.isEnabled()) { throw new SepNotFoundException("Not Found"); } HttpHeaders headers = new HttpHeaders(); headers.set("content-type", "text/plain"); - try { - return ResponseEntity.ok().headers(headers).body(sep1Service.getStellarToml()); - } catch (Exception e) { - throw new SepNotFoundException("There was an error fetching the TOML file."); - } + return ResponseEntity.ok().headers(headers).body(sep1Service.getStellarToml()); } @ExceptionHandler({SepNotFoundException.class}) From 3c6f9b0d6c44c713674644b5ea1131df10443589 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 16:48:58 -0700 Subject: [PATCH 13/28] debugF --- core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index a3b8b9ab75..5a1667b74d 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -1,6 +1,7 @@ package org.stellar.anchor.sep1; import static org.stellar.anchor.util.Log.debug; +import static org.stellar.anchor.util.Log.debugF; import java.io.IOException; import java.nio.file.Path; From 807ff46c69e49c3c71b33bcaf0e81ec775e70f94 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 17:22:14 -0700 Subject: [PATCH 14/28] revert config changes --- service-runner/src/main/resources/profiles/default/config.env | 4 ++-- service-runner/src/main/resources/profiles/sep24/config.env | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/service-runner/src/main/resources/profiles/default/config.env b/service-runner/src/main/resources/profiles/default/config.env index 0bff58e8f9..18fe05464e 100644 --- a/service-runner/src/main/resources/profiles/default/config.env +++ b/service-runner/src/main/resources/profiles/default/config.env @@ -28,8 +28,8 @@ assets.type=file assets.value=/config/assets.yaml # seps sep1.enabled=true -sep1.toml.type=url -sep1.toml.value=http://169.254.169.254/latest/meta-data/ +sep1.toml.type=file +sep1.toml.value=/config/stellar.localhost.toml sep10.enabled=true sep12.enabled=true sep31.enabled=true diff --git a/service-runner/src/main/resources/profiles/sep24/config.env b/service-runner/src/main/resources/profiles/sep24/config.env index febed1775d..bf56f77535 100644 --- a/service-runner/src/main/resources/profiles/sep24/config.env +++ b/service-runner/src/main/resources/profiles/sep24/config.env @@ -28,8 +28,8 @@ assets.type=file assets.value=/config/assets.yaml # seps sep1.enabled=true -sep1.toml.type=url -sep1.toml.value=http://169.254.169.254/latest/meta-data/ +sep1.toml.type=file +sep1.toml.value=/config/stellar.localhost.toml sep10.enabled=true sep12.enabled=true sep31.enabled=true From 33369dd0fedba96089d743f1700742d1dc2fb3c4 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Wed, 9 Aug 2023 23:11:11 -0700 Subject: [PATCH 15/28] revert defaults --- .../src/main/resources/config/anchor-config-default-values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/src/main/resources/config/anchor-config-default-values.yaml b/platform/src/main/resources/config/anchor-config-default-values.yaml index 5c9ed4ca96..3b5aa60ac9 100644 --- a/platform/src/main/resources/config/anchor-config-default-values.yaml +++ b/platform/src/main/resources/config/anchor-config-default-values.yaml @@ -227,7 +227,7 @@ sep1: ## `string`: value contains the content of the stellar toml file ## `file`: value contains the path to the stellar toml file ## `url`: value contains the url to the stellar toml file - type: file + type: string ## @param: value ## @type: string # From 574e8c7bdaf4d91fb45c91d4076c514cedaa1f7c Mon Sep 17 00:00:00 2001 From: reecexlm <98427531+reecexlm@users.noreply.github.com> Date: Thu, 10 Aug 2023 10:08:05 -0700 Subject: [PATCH 16/28] [ANCHOR-403] Prevent SSRF through SEP-1 TOML redirect (#1043) * ANCHOR-403-reece-SSRF-metadata * use assertThrows * additional cleanup * remove custom exception * make sure that the log event is actually captured by adding a verification step before the assertions * temporary setting toml to url to test * just testing. ignore * try other file * patch toml parser as well * replace comment * revert * lint * debugF * revert config changes * revert defaults --- .../java/org/stellar/anchor/util/NetUtil.java | 9 +++- .../org/stellar/anchor/util/Sep1Helper.java | 8 +++- .../org/stellar/anchor/util/NetUtilTest.kt | 25 +++++++--- .../controller/sep/Sep1Controller.java | 3 +- .../org/stellar/anchor/Sep1ServiceTest.kt | 47 ++++++++++++++++++- .../anchor/platform/LogAppenderTest.kt | 4 ++ .../anchor/platform/TestProfileRunner.kt | 5 +- 7 files changed, 88 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index b8047fea6e..25de3be9cf 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -17,7 +17,14 @@ public static String fetch(String url) throws IOException { Request request = OkHttpUtil.buildGetRequest(url); Response response = getCall(request).execute(); - if (response.body() == null) return ""; + // Check if response was successful (status code 200) + if (!response.isSuccessful()) { + throw new IOException(String.format("Unable to fetch data from %s", url)); + } + + if (response.body() == null) { + throw new IOException("Response body is empty."); + } return Objects.requireNonNull(response.body()).string(); } diff --git a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java index 517a0864f3..f372530ea4 100644 --- a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java +++ b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java @@ -10,7 +10,13 @@ public static TomlContent readToml(String url) throws IOException { } public static TomlContent parse(String tomlString) { - return new TomlContent(tomlString); + try { + return new TomlContent(tomlString); + } catch (Exception e) { + // obfuscate exception message to prevent metadata leaks + throw new RuntimeException( + "Failed to parse TOML content"); // or return null or a default TomlContent instance + } } public static class TomlContent { diff --git a/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt b/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt index 0a39404fc6..db6775b1f5 100644 --- a/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/util/NetUtilTest.kt @@ -4,11 +4,13 @@ package org.stellar.anchor.util import io.mockk.* import io.mockk.impl.annotations.MockK +import java.io.IOException import java.net.MalformedURLException import okhttp3.Response import okhttp3.ResponseBody import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -36,15 +38,17 @@ internal class NetUtilTest { } @Test - fun `test fetch()`() { + fun `test fetch successful response`() { mockkStatic(NetUtil::class) every { NetUtil.getCall(any()) } returns mockCall every { mockCall.execute() } returns mockResponse + every { mockResponse.isSuccessful } returns true every { mockResponse.body } returns mockResponseBody every { mockResponseBody.string() } returns "result" val result = NetUtil.fetch("http://hello") - assert(result.equals("result")) + assertEquals("result", result) + verify { NetUtil.getCall(any()) mockCall.execute() @@ -52,15 +56,24 @@ internal class NetUtilTest { } @Test - fun `test fetch() throws exception`() { + fun `test fetch unsuccessful response`() { + mockkStatic(NetUtil::class) + every { NetUtil.getCall(any()) } returns mockCall + every { mockCall.execute() } returns mockResponse + every { mockResponse.isSuccessful } returns false + + assertThrows(IOException::class.java) { NetUtil.fetch("http://hello") } + } + + @Test + fun `test fetch null response body`() { mockkStatic(NetUtil::class) every { NetUtil.getCall(any()) } returns mockCall every { mockCall.execute() } returns mockResponse + every { mockResponse.isSuccessful } returns true every { mockResponse.body } returns null - every { mockResponseBody.string() } returns "result" - val result = NetUtil.fetch("http://hello") - assert(result.equals("")) + assertThrows(IOException::class.java) { NetUtil.fetch("http://hello") } } @Test diff --git a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java index 7351d2b430..fb20aa5695 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java +++ b/platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep1Controller.java @@ -1,5 +1,6 @@ package org.stellar.anchor.platform.controller.sep; +import java.io.IOException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -40,7 +41,7 @@ public RedirectView landingPage() throws SepNotFoundException { @RequestMapping( value = "/.well-known/stellar.toml", method = {RequestMethod.GET, RequestMethod.OPTIONS}) - public ResponseEntity getToml() throws SepNotFoundException { + public ResponseEntity getToml() throws IOException, SepNotFoundException { if (!sep1Config.isEnabled()) { throw new SepNotFoundException("Not Found"); } diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index 279a8cfc33..6cce8c63c4 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -1,10 +1,12 @@ package org.stellar.anchor +import java.io.IOException import java.nio.file.Files import java.nio.file.Path import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.stellar.anchor.config.Sep1Config.TomlType.* import org.stellar.anchor.platform.config.PropertySep1Config @@ -13,6 +15,7 @@ import org.stellar.anchor.platform.config.Sep1ConfigTest import org.stellar.anchor.sep1.Sep1Service class Sep1ServiceTest { + lateinit var sep1: Sep1Service val stellarToml = """ @@ -47,7 +50,6 @@ class Sep1ServiceTest { @Test fun `test Sep1Service reading toml from inline string`() { - val config = PropertySep1Config(true, TomlConfig(STRING, stellarToml)) sep1 = Sep1Service(config) assertEquals(sep1.stellarToml, stellarToml) @@ -66,9 +68,50 @@ class Sep1ServiceTest { mockServer.start() val mockAnchorUrl = mockServer.url("").toString() mockServer.enqueue(MockResponse().setBody(stellarToml)) - val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) sep1 = Sep1Service(config) assertEquals(sep1.stellarToml, stellarToml) } + + @Test + fun `getStellarToml throws exception when redirect is encountered`() { + val mockServer = MockWebServer() + mockServer.start() + val mockAnchorUrl = mockServer.url("").toString() + + // Enqueue a response with a 302 status and a Location header to simulate a redirect. + mockServer.enqueue( + MockResponse() + .setResponseCode(302) + .setHeader("Location", mockServer.url("/new_location").toString()) + ) + + // Enqueue a response at the redirect location that provides a server error. + mockServer.enqueue(MockResponse().setResponseCode(500)) + + // Enqueue an empty response to prevent a timeout. + mockServer.enqueue(MockResponse()) + + val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) + + val exception = assertThrows(IOException::class.java) { Sep1Service(config) } + assertEquals("Unable to fetch data from $mockAnchorUrl", exception.message) + mockServer.shutdown() + } + + @Test + fun `getStellarToml throws exception when redirected location results in error`() { + val mockServer = MockWebServer() + mockServer.start() + val mockAnchorUrl = mockServer.url("/new_location").toString() + + // Enqueue a response that provides a server error. + mockServer.enqueue(MockResponse().setResponseCode(500)) + + val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) + + val exception = assertThrows(IOException::class.java) { sep1 = Sep1Service(config) } + assertEquals("Unable to fetch data from $mockAnchorUrl", exception.message) + mockServer.shutdown() + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt index 824fe940e5..c07703a8ea 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/LogAppenderTest.kt @@ -68,6 +68,10 @@ class LogAppenderTest { "debug" -> Log.debug(wantMessage) "trace" -> Log.trace(wantMessage) } + + // Verify that the append method was called, which ensures that the event was captured + verify { appender.append(any()) } + assertEquals(LogAppenderTest::class.qualifiedName, capturedLogEvent.captured.loggerName) assertEquals(wantLevelName, capturedLogEvent.captured.level.toString()) assertEquals(wantMessage, capturedLogEvent.captured.message.toString()) diff --git a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt index b9c37208c6..a209a5d077 100644 --- a/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt +++ b/service-runner/src/main/kotlin/org/stellar/anchor/platform/TestProfileRunner.kt @@ -73,8 +73,9 @@ class TestProfileExecutor(val config: TestConfig) { val envMap = config.env envMap["assets.value"] = getResourceFile(envMap["assets.value"]!!).absolutePath - envMap["sep1.toml.value"] = getResourceFile(envMap["sep1.toml.value"]!!).absolutePath - + if (envMap["sep1.toml.type"] != "url") { + envMap["sep1.toml.value"] = getResourceFile(envMap["sep1.toml.value"]!!).absolutePath + } // Start servers val jobs = mutableListOf() val scope = CoroutineScope(Dispatchers.Default) From 90d9550577e8fb1eeada0ca4c18c048ffce4c55c Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 10:39:31 -0700 Subject: [PATCH 17/28] bump version to 2.1.3 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3c9582f183..e389753fe8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -135,7 +135,7 @@ subprojects { allprojects { group = "org.stellar.anchor-sdk" - version = "2.1.2" + version = "2.1.3" tasks.jar { manifest { From 4bd724485d120bc8c6872b187cee07a95a023df9 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 16:20:46 -0700 Subject: [PATCH 18/28] spotless --- .../org/stellar/anchor/sep1/Sep1Service.java | 18 ++++- .../java/org/stellar/anchor/util/NetUtil.java | 30 +++++--- .../org/stellar/anchor/util/Sep1Helper.java | 25 +++++-- .../org/stellar/anchor/util/Sep1HelperTest.kt | 75 +++++++++++++++++++ .../platform/config/PropertySep1Config.java | 6 +- 5 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 core/src/test/kotlin/org/stellar/anchor/util/Sep1HelperTest.kt diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index 5a1667b74d..350ab7ba4c 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -1,6 +1,5 @@ package org.stellar.anchor.sep1; -import static org.stellar.anchor.util.Log.debug; import static org.stellar.anchor.util.Log.debugF; import java.io.IOException; @@ -23,10 +22,10 @@ public class Sep1Service { */ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { if (sep1Config.isEnabled()) { - debug("sep1Config:", sep1Config); + debugF("sep1Config: {}", sep1Config); switch (sep1Config.getType()) { case STRING: - debug("reading stellar.toml from config[sep1.toml.value]"); + debugF("reading stellar.toml from config[sep1.toml.value]"); tomlValue = sep1Config.getValue(); break; case FILE: @@ -35,7 +34,7 @@ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigExcep break; case URL: debugF("reading stellar.toml from {}", sep1Config.getValue()); - tomlValue = NetUtil.fetch(sep1Config.getValue()); + tomlValue = fetchTomlFromURL(sep1Config.getValue()); break; default: throw new InvalidConfigException( @@ -46,6 +45,17 @@ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigExcep } } + private String fetchTomlFromURL(String url) throws IOException { + try { + return NetUtil.fetch(url); + } catch (IOException e) { + // Obfuscate the message and rethrow + String obfuscatedMessage = String.format("Unable to fetch data from %s", url); + debugF(obfuscatedMessage); // Log the obfuscated message using the debugF method + throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause + } + } + public String getStellarToml() { return tomlValue; } diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index 25de3be9cf..90fd50089a 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -1,5 +1,6 @@ package org.stellar.anchor.util; +import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.StringHelper.isEmpty; import io.jsonwebtoken.lang.Strings; @@ -13,19 +14,30 @@ import okhttp3.Response; public class NetUtil { + public static String fetch(String url) throws IOException { - Request request = OkHttpUtil.buildGetRequest(url); - Response response = getCall(request).execute(); - // Check if response was successful (status code 200) - if (!response.isSuccessful()) { - throw new IOException(String.format("Unable to fetch data from %s", url)); - } + try { + Request request = OkHttpUtil.buildGetRequest(url); + Response response = getCall(request).execute(); + String message = + String.format("NetUtil:fetch of %s unsuccessful. response: %s", url, response.toString()); + + // Check if response was successful (status code 200) and throw IOException + if (!response.isSuccessful()) { + debugF(message); + throw new IOException(message); + } - if (response.body() == null) { - throw new IOException("Response body is empty."); + if (response.body() == null) { + debugF(message); + throw new IOException(message); + } + return Objects.requireNonNull(response.body()).string(); + } catch (IOException e) { + debugF(e.toString()); + throw e; } - return Objects.requireNonNull(response.body()).string(); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") diff --git a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java index f372530ea4..804f9d5440 100644 --- a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java +++ b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java @@ -1,21 +1,26 @@ package org.stellar.anchor.util; +import static org.stellar.anchor.util.Log.*; + import com.moandjiezana.toml.Toml; import java.io.IOException; import java.net.URL; +import org.stellar.anchor.api.exception.InvalidConfigException; public class Sep1Helper { public static TomlContent readToml(String url) throws IOException { return new TomlContent(new URL(url)); } - public static TomlContent parse(String tomlString) { + public static TomlContent parse(String tomlString) throws InvalidConfigException { try { return new TomlContent(tomlString); } catch (Exception e) { - // obfuscate exception message to prevent metadata leaks - throw new RuntimeException( - "Failed to parse TOML content"); // or return null or a default TomlContent instance + // Obfuscate the message and rethrow + String obfuscatedMessage = "Failed to parse TOML content. Invalid Config."; + debugF(e.toString()); // Log the exception + throw new InvalidConfigException( + obfuscatedMessage); // Preserve the original exception as the cause } } @@ -23,8 +28,16 @@ public static class TomlContent { private final Toml toml; TomlContent(URL url) throws IOException { - String tomlValue = NetUtil.fetch(url.toString()); - toml = new Toml().read(tomlValue); + try { + String tomlValue = NetUtil.fetch(url.toString()); + toml = new Toml().read(tomlValue); + } catch (IOException e) { + // Obfuscate the message and rethrow + String obfuscatedMessage = + String.format("An error occurred while fetching the TOML from %s", url); + Log.error(e.toString()); + throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause + } } TomlContent(String tomlString) { diff --git a/core/src/test/kotlin/org/stellar/anchor/util/Sep1HelperTest.kt b/core/src/test/kotlin/org/stellar/anchor/util/Sep1HelperTest.kt new file mode 100644 index 0000000000..7ae179320c --- /dev/null +++ b/core/src/test/kotlin/org/stellar/anchor/util/Sep1HelperTest.kt @@ -0,0 +1,75 @@ +package org.stellar.anchor.util + +import java.io.IOException +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.stellar.anchor.api.exception.InvalidConfigException + +class Sep1HelperTest { + val stellarToml = + """ + ACCOUNTS = [ "GCSGSR6KQQ5BP2FXVPWRL6SWPUSFWLVONLIBJZUKTVQB5FYJFVL6XOXE" ] + VERSION = "0.1.0" + SIGNING_KEY = "GBDYDBJKQBJK4GY4V7FAONSFF2IBJSKNTBYJ65F5KCGBY2BIGPGGLJOH" + NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" + + WEB_AUTH_ENDPOINT = "http://localhost:8080/auth" + KYC_SERVER = "http://localhost:8080/sep12" + TRANSFER_SERVER_SEP0024 = "http://localhost:8080/sep24" + DIRECT_PAYMENT_SERVER = "http://localhost:8080/sep31" + ANCHOR_QUOTE_SERVER = "http://localhost:8080/sep38" + + [[CURRENCIES]] + code = "SRT" + issuer = "GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B" + status = "test" + is_asset_anchored = false + anchor_asset_type = "other" + desc = "A fake anchored asset to use with this example anchor server." + + [DOCUMENTATION] + ORG_NAME = "Stellar Development Foundation" + ORG_URL = "https://stellar.org" + ORG_DESCRIPTION = "SEP 24 reference server." + ORG_KEYBASE = "stellar.public" + ORG_TWITTER = "StellarOrg" + ORG_GITHUB = "stellar" + """ + .trimIndent() + + private val mockWebServer = MockWebServer() + + @BeforeEach + fun setup() { + mockWebServer.start() + } + + @AfterEach + fun teardown() { + mockWebServer.shutdown() + } + + @Test + fun `test Read Toml with IOException`() { + // Enqueue a response with an HTTP error status code + mockWebServer.enqueue(MockResponse().setResponseCode(500)) + val exception = + assertThrows(IOException::class.java) { + Sep1Helper.readToml(mockWebServer.url("/").toString()) + } + // You may need to adjust the assertion based on the specific behavior of Sep1Helper + assertTrue(exception.message!!.contains("An error occurred while fetching the TOML from")) + } + + @Test + fun `test Parse Invalid Toml String`() { + val invalidTomlString = "key = value" // An invalid TOML string without quotes + val exception = + assertThrows(InvalidConfigException::class.java) { Sep1Helper.parse(invalidTomlString) } + assertEquals("Failed to parse TOML content. Invalid Config.", exception.message) + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep1Config.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep1Config.java index 3d40b342bb..70381da1d7 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep1Config.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep1Config.java @@ -6,6 +6,7 @@ import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; +import org.stellar.anchor.api.exception.InvalidConfigException; import org.stellar.anchor.config.Sep1Config; import org.stellar.anchor.util.NetUtil; import org.stellar.anchor.util.Sep1Helper; @@ -59,12 +60,11 @@ void validateTomlTypeAndValue(PropertySep1Config config, Errors errors) { case STRING: try { Sep1Helper.parse(config.getToml().getValue()); - } catch (IllegalStateException isex) { + } catch (InvalidConfigException e) { errors.rejectValue( "value", "sep1-toml-value-string-invalid", - String.format( - "sep1.toml.value does not contain a valid TOML. %s", isex.getMessage())); + String.format("sep1.toml.value does not contain a valid TOML. %s", e.getMessage())); } break; case URL: From be182c351832828812f25c048473ed7d573c1120 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 16:28:36 -0700 Subject: [PATCH 19/28] log the actual exception --- core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index 350ab7ba4c..58ffb601f0 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -51,7 +51,7 @@ private String fetchTomlFromURL(String url) throws IOException { } catch (IOException e) { // Obfuscate the message and rethrow String obfuscatedMessage = String.format("Unable to fetch data from %s", url); - debugF(obfuscatedMessage); // Log the obfuscated message using the debugF method + debugF(e.toString()); // Log the obfuscated message using the debugF method throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause } } From f2729045988ec490d096a10cd2cd35604aac9fbd Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 16:33:59 -0700 Subject: [PATCH 20/28] fix comment --- core/src/main/java/org/stellar/anchor/util/NetUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index 90fd50089a..a76980056c 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -23,7 +23,7 @@ public static String fetch(String url) throws IOException { String message = String.format("NetUtil:fetch of %s unsuccessful. response: %s", url, response.toString()); - // Check if response was successful (status code 200) and throw IOException + // Check if response was unsuccessful (ie not status code 2xx) and throw IOException if (!response.isSuccessful()) { debugF(message); throw new IOException(message); From eaf322406a763807d7fc38908143c78cce0a4a7c Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 17:08:02 -0700 Subject: [PATCH 21/28] don't repeat exceptions at each layer --- core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java | 6 ++---- core/src/main/java/org/stellar/anchor/util/Sep1Helper.java | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index 58ffb601f0..b3f83fcf31 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -22,7 +22,6 @@ public class Sep1Service { */ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigException { if (sep1Config.isEnabled()) { - debugF("sep1Config: {}", sep1Config); switch (sep1Config.getType()) { case STRING: debugF("reading stellar.toml from config[sep1.toml.value]"); @@ -49,10 +48,9 @@ private String fetchTomlFromURL(String url) throws IOException { try { return NetUtil.fetch(url); } catch (IOException e) { - // Obfuscate the message and rethrow String obfuscatedMessage = String.format("Unable to fetch data from %s", url); - debugF(e.toString()); // Log the obfuscated message using the debugF method - throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause + debugF("Sep1Service fetchTomlFromURL Error fetching Toml from URL:{}", url); + throw new IOException(obfuscatedMessage); } } diff --git a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java index 804f9d5440..205e820a6c 100644 --- a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java +++ b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java @@ -18,7 +18,7 @@ public static TomlContent parse(String tomlString) throws InvalidConfigException } catch (Exception e) { // Obfuscate the message and rethrow String obfuscatedMessage = "Failed to parse TOML content. Invalid Config."; - debugF(e.toString()); // Log the exception + debugF(e.toString()); // Log the parsing exception throw new InvalidConfigException( obfuscatedMessage); // Preserve the original exception as the cause } @@ -35,7 +35,7 @@ public static class TomlContent { // Obfuscate the message and rethrow String obfuscatedMessage = String.format("An error occurred while fetching the TOML from %s", url); - Log.error(e.toString()); + Log.error("Sep1Service fetchTomlFromURL Error fetching Toml from URL:{}", url); throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause } } From 7a4cec4f654fc7824b21c95fed1c3d08dbcb2a8b Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 17:59:44 -0700 Subject: [PATCH 22/28] refactor sep1helper --- .../org/stellar/anchor/util/Sep1Helper.java | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java index 205e820a6c..307f501e70 100644 --- a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java +++ b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java @@ -4,12 +4,19 @@ import com.moandjiezana.toml.Toml; import java.io.IOException; -import java.net.URL; import org.stellar.anchor.api.exception.InvalidConfigException; public class Sep1Helper { public static TomlContent readToml(String url) throws IOException { - return new TomlContent(new URL(url)); + try { + String tomlValue = NetUtil.fetch(url); + return new TomlContent(tomlValue); + } catch (IOException e) { + String obfuscatedMessage = + String.format("An error occurred while fetching the TOML from %s", url); + Log.error("Sep1Service fetchTomlFromURL Error fetching Toml from URL:{}", url); + throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause + } } public static TomlContent parse(String tomlString) throws InvalidConfigException { @@ -18,7 +25,7 @@ public static TomlContent parse(String tomlString) throws InvalidConfigException } catch (Exception e) { // Obfuscate the message and rethrow String obfuscatedMessage = "Failed to parse TOML content. Invalid Config."; - debugF(e.toString()); // Log the parsing exception + debugF(obfuscatedMessage); // Log the parsing exception throw new InvalidConfigException( obfuscatedMessage); // Preserve the original exception as the cause } @@ -27,19 +34,6 @@ public static TomlContent parse(String tomlString) throws InvalidConfigException public static class TomlContent { private final Toml toml; - TomlContent(URL url) throws IOException { - try { - String tomlValue = NetUtil.fetch(url.toString()); - toml = new Toml().read(tomlValue); - } catch (IOException e) { - // Obfuscate the message and rethrow - String obfuscatedMessage = - String.format("An error occurred while fetching the TOML from %s", url); - Log.error("Sep1Service fetchTomlFromURL Error fetching Toml from URL:{}", url); - throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause - } - } - TomlContent(String tomlString) { toml = new Toml().read(tomlString); } From d726f7730ac7c11095504f9db86ce0b121e44bed Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 19:28:01 -0700 Subject: [PATCH 23/28] addressing review comments --- .../org/stellar/anchor/sep1/Sep1Service.java | 12 +------ .../java/org/stellar/anchor/util/NetUtil.java | 2 +- .../org/stellar/anchor/Sep1ServiceTest.kt | 36 +++++++++++++------ 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java index 6d7be80ebf..9edf5efcd3 100644 --- a/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java +++ b/core/src/main/java/org/stellar/anchor/sep1/Sep1Service.java @@ -37,7 +37,7 @@ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigExcep break; case URL: debugF("reading stellar.toml from {}", sep1Config.getValue()); - tomlValue = fetchTomlFromURL(sep1Config.getValue()); + tomlValue = NetUtil.fetch(sep1Config.getValue()); break; default: throw new InvalidConfigException( @@ -48,16 +48,6 @@ public Sep1Service(Sep1Config sep1Config) throws IOException, InvalidConfigExcep } } - private String fetchTomlFromURL(String url) throws IOException { - try { - return NetUtil.fetch(url); - } catch (IOException e) { - String obfuscatedMessage = String.format("Unable to fetch data from %s", url); - debugF("Sep1Service fetchTomlFromURL Error fetching Toml from URL:{}", url); - throw new IOException(obfuscatedMessage); - } - } - public String getStellarToml() { sep1TomlAccessedCounter.increment(); return tomlValue; diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index 036ebc28e7..bd7c240150 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -21,7 +21,7 @@ public static String fetch(String url) throws IOException { Request request = OkHttpUtil.buildGetRequest(url); Response response = getCall(request).execute(); String message = - String.format("NetUtil:fetch of %s unsuccessful. response: %s", url, response.toString()); + String.format("Error fetching from URL: %s response: %s", url, response.toString()); // Check if response was unsuccessful (ie not status code 2xx) and throw IOException if (!response.isSuccessful()) { diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index 6cce8c63c4..3006354167 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -7,6 +7,7 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.stellar.anchor.config.Sep1Config.TomlType.* import org.stellar.anchor.platform.config.PropertySep1Config @@ -17,7 +18,7 @@ import org.stellar.anchor.sep1.Sep1Service class Sep1ServiceTest { lateinit var sep1: Sep1Service - val stellarToml = + private val stellarToml = """ ACCOUNTS = [ "GCSGSR6KQQ5BP2FXVPWRL6SWPUSFWLVONLIBJZUKTVQB5FYJFVL6XOXE" ] VERSION = "0.1.0" @@ -73,11 +74,21 @@ class Sep1ServiceTest { assertEquals(sep1.stellarToml, stellarToml) } + // this test is not expected to raise an exception. given the re-direct to a malicious + // endpoint still returns a 200 the exception will be raised/obfuscated + // when the toml is parsed. @Test - fun `getStellarToml throws exception when redirect is encountered`() { + fun `getStellarToml fetches invalid data during malicious re-direct`() { val mockServer = MockWebServer() mockServer.start() val mockAnchorUrl = mockServer.url("").toString() + val metadata = + "{\n" + + " \"ami-id\": \"ami-12345678\",\n" + + " \"instance-id\": \"i-1234567890abcdef\",\n" + + " \"instance-type\": \"t2.micro\"\n" + + " // ... other metadata ...\n" + + "}" // Enqueue a response with a 302 status and a Location header to simulate a redirect. mockServer.enqueue( @@ -86,16 +97,12 @@ class Sep1ServiceTest { .setHeader("Location", mockServer.url("/new_location").toString()) ) - // Enqueue a response at the redirect location that provides a server error. - mockServer.enqueue(MockResponse().setResponseCode(500)) - - // Enqueue an empty response to prevent a timeout. - mockServer.enqueue(MockResponse()) + // Enqueue a response at the redirect location that simulates AWS metadata leak. + mockServer.enqueue(MockResponse().setResponseCode(200).setBody(metadata)) val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) - - val exception = assertThrows(IOException::class.java) { Sep1Service(config) } - assertEquals("Unable to fetch data from $mockAnchorUrl", exception.message) + val sep1 = Sep1Service(config) + assertEquals(sep1.getStellarToml(), metadata) mockServer.shutdown() } @@ -111,7 +118,14 @@ class Sep1ServiceTest { val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) val exception = assertThrows(IOException::class.java) { sep1 = Sep1Service(config) } - assertEquals("Unable to fetch data from $mockAnchorUrl", exception.message) + assertTrue(exception.message?.contains("NetUtil:fetch of") ?: false) + assertTrue( + exception.message?.contains( + "unsuccessful. response: Response{protocol=http/1.1, code=500, message=Server Error, url=http://localhost:" + ) + ?: false + ) + mockServer.shutdown() } } From 5ec4ad38c5ae8e0553ea1405065dc4adae33fc7b Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 21:13:22 -0700 Subject: [PATCH 24/28] update tests --- .../main/java/org/stellar/anchor/util/NetUtil.java | 4 ++-- .../test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index bd7c240150..b514396330 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -8,7 +8,6 @@ import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; -import java.util.Objects; import okhttp3.Call; import okhttp3.Request; import okhttp3.Response; @@ -33,7 +32,8 @@ public static String fetch(String url) throws IOException { debugF(message); throw new IOException(message); } - return Objects.requireNonNull(response.body()).string(); + + return response.body().string(); } catch (IOException e) { debugF(e.toString()); throw e; diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index 3006354167..12fb7cbe04 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -5,9 +5,9 @@ import java.nio.file.Files import java.nio.file.Path import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.Assertions.assertContains import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.stellar.anchor.config.Sep1Config.TomlType.* import org.stellar.anchor.platform.config.PropertySep1Config @@ -116,15 +116,8 @@ class Sep1ServiceTest { mockServer.enqueue(MockResponse().setResponseCode(500)) val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) - val exception = assertThrows(IOException::class.java) { sep1 = Sep1Service(config) } - assertTrue(exception.message?.contains("NetUtil:fetch of") ?: false) - assertTrue( - exception.message?.contains( - "unsuccessful. response: Response{protocol=http/1.1, code=500, message=Server Error, url=http://localhost:" - ) - ?: false - ) + assertContains(exception.message, "code=500, message=Server Error, url=http://localhost:") mockServer.shutdown() } From 90c584b4cb85b89e6b3cf5fd03e68724b4a7ccc2 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Thu, 10 Aug 2023 21:17:34 -0700 Subject: [PATCH 25/28] fix assert --- .../src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index 12fb7cbe04..5089b970b0 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -5,9 +5,9 @@ import java.nio.file.Files import java.nio.file.Path import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer -import org.junit.jupiter.api.Assertions.assertContains import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.stellar.anchor.config.Sep1Config.TomlType.* import org.stellar.anchor.platform.config.PropertySep1Config @@ -117,7 +117,9 @@ class Sep1ServiceTest { val config = PropertySep1Config(true, TomlConfig(URL, mockAnchorUrl)) val exception = assertThrows(IOException::class.java) { sep1 = Sep1Service(config) } - assertContains(exception.message, "code=500, message=Server Error, url=http://localhost:") + assertTrue( + exception.message?.contains("code=500, message=Server Error, url=http://localhost:") == true + ) mockServer.shutdown() } From 10e6562016eddb28efa7442e2c483b695db348af Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Fri, 11 Aug 2023 06:34:48 -0700 Subject: [PATCH 26/28] log exception --- core/src/main/java/org/stellar/anchor/util/Sep1Helper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java index 307f501e70..9aca02d869 100644 --- a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java +++ b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java @@ -14,7 +14,7 @@ public static TomlContent readToml(String url) throws IOException { } catch (IOException e) { String obfuscatedMessage = String.format("An error occurred while fetching the TOML from %s", url); - Log.error("Sep1Service fetchTomlFromURL Error fetching Toml from URL:{}", url); + Log.error(e.toString()); throw new IOException(obfuscatedMessage); // Preserve the original exception as the cause } } @@ -25,7 +25,7 @@ public static TomlContent parse(String tomlString) throws InvalidConfigException } catch (Exception e) { // Obfuscate the message and rethrow String obfuscatedMessage = "Failed to parse TOML content. Invalid Config."; - debugF(obfuscatedMessage); // Log the parsing exception + Log.error(e.toString()); // Log the parsing exception throw new InvalidConfigException( obfuscatedMessage); // Preserve the original exception as the cause } From 482057e1a8dc5fce03edd6a98896e8e6dd660a06 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Fri, 11 Aug 2023 09:41:34 -0700 Subject: [PATCH 27/28] add some docs to netutil and reduce unnecessary logging --- .../java/org/stellar/anchor/util/NetUtil.java | 42 ++++++++++--------- .../org/stellar/anchor/Sep1ServiceTest.kt | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index b514396330..e8c3545146 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -1,6 +1,5 @@ package org.stellar.anchor.util; -import static org.stellar.anchor.util.Log.debugF; import static org.stellar.anchor.util.StringHelper.isEmpty; import io.jsonwebtoken.lang.Strings; @@ -14,30 +13,33 @@ public class NetUtil { + /** + * Fetches the content from the specified URL using an HTTP GET request. + * + *

This method expects a response body to be present in the HTTP response. If the response is + * unsuccessful (i.e., not a 2xx status code) or if the response body is null, an IOException will + * be thrown. + * + * @param url The URL to fetch content from. + * @return The content of the response body as a string. + * @throws IOException If the response is unsuccessful, or if the response body is null. + */ public static String fetch(String url) throws IOException { - try { - Request request = OkHttpUtil.buildGetRequest(url); - Response response = getCall(request).execute(); - String message = - String.format("Error fetching from URL: %s response: %s", url, response.toString()); - - // Check if response was unsuccessful (ie not status code 2xx) and throw IOException - if (!response.isSuccessful()) { - debugF(message); - throw new IOException(message); - } + Request request = OkHttpUtil.buildGetRequest(url); + Response response = getCall(request).execute(); - if (response.body() == null) { - debugF(message); - throw new IOException(message); - } + // Check if response was unsuccessful (ie not status code 2xx) and throw IOException + if (!response.isSuccessful()) { + throw new IOException(response.toString()); + } - return response.body().string(); - } catch (IOException e) { - debugF(e.toString()); - throw e; + // Since fetch expects a response body, we will throw IOException if its null + if (response.body() == null) { + throw new IOException(response.toString()); } + + return response.body().string(); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") diff --git a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt index 5089b970b0..77c15e1689 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/Sep1ServiceTest.kt @@ -64,7 +64,7 @@ class Sep1ServiceTest { } @Test - fun `test Sep1Service reading toml from url`() { + fun `getStellarToml fetches data during re-direct`() { val mockServer = MockWebServer() mockServer.start() val mockAnchorUrl = mockServer.url("").toString() From a622baf046ccb6b79bebb74c3449e42cddc9b6c1 Mon Sep 17 00:00:00 2001 From: Reece Markowsky Date: Fri, 11 Aug 2023 10:46:12 -0700 Subject: [PATCH 28/28] rebased on main --- core/src/main/java/org/stellar/anchor/util/NetUtil.java | 1 - core/src/main/java/org/stellar/anchor/util/Sep1Helper.java | 1 - 2 files changed, 2 deletions(-) diff --git a/core/src/main/java/org/stellar/anchor/util/NetUtil.java b/core/src/main/java/org/stellar/anchor/util/NetUtil.java index fa03e2771e..e8c3545146 100644 --- a/core/src/main/java/org/stellar/anchor/util/NetUtil.java +++ b/core/src/main/java/org/stellar/anchor/util/NetUtil.java @@ -40,7 +40,6 @@ public static String fetch(String url) throws IOException { } return response.body().string(); - } @SuppressWarnings("BooleanMethodIsAlwaysInverted") diff --git a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java index e42b74349b..9aca02d869 100644 --- a/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java +++ b/core/src/main/java/org/stellar/anchor/util/Sep1Helper.java @@ -28,7 +28,6 @@ public static TomlContent parse(String tomlString) throws InvalidConfigException Log.error(e.toString()); // Log the parsing exception throw new InvalidConfigException( obfuscatedMessage); // Preserve the original exception as the cause - } }