Skip to content

Commit 3dc18ac

Browse files
Merge pull request #3157 from Huge/fix/rpn-ack-malformed-2xx
iab: treat a missing or non-JSON 2xx billing response as an error
2 parents fd2ee1c + 304fd1b commit 3dc18ac

4 files changed

Lines changed: 58 additions & 10 deletions

File tree

‎app/src/play/java/com/celzero/bravedns/iab/RpnPurchaseAckServerResponse.kt‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import org.json.JSONObject
2525
* Sealed result returned by every Rethink server billing endpoint.
2626
*
2727
* Use [RpnPurchaseAckServerResponse.from] to deserialise raw HTTP response bodies.
28-
* A 2xx response with `"success" != false` is treated as [Ok]; everything
29-
* else (non-2xx, `"error"` key present, or `"success" == false`) is [Err].
28+
* A 2xx response with a JSON body and `"success" != false` is treated as [Ok];
29+
* everything else (non-2xx, missing or non-JSON body, `"error"` key present, or
30+
* `"success" == false`) is [Err].
3031
*/
3132
sealed class RpnPurchaseAckServerResponse {
3233
data class Ok(val payload: ResponseOk) : RpnPurchaseAckServerResponse()
@@ -48,7 +49,14 @@ sealed class RpnPurchaseAckServerResponse {
4849
return Err(payload)
4950
}
5051

51-
val json = body?.trimmedJson() ?: JSONObject()
52+
// The server always replies with a JSON object (it carries at least "ray"),
53+
// so a missing or non-JSON 2xx body (e.g. from a proxy) is not a success.
54+
val json = body?.trimmedJson()
55+
if (json == null) {
56+
val payload = ResponseErr(error = "malformed response", httpCode = httpCode)
57+
Logger.d(LOG_IAB, "PlayErr: $payload")
58+
return Err(payload)
59+
}
5260

5361
// If "error" key is present, or "success" is explicitly false → error
5462
return if (json.has("error") || !json.optBoolean("success", true)) {
@@ -63,10 +71,10 @@ sealed class RpnPurchaseAckServerResponse {
6371
}
6472
}
6573

66-
private fun String.trimmedJson(): JSONObject = try {
74+
private fun String.trimmedJson(): JSONObject? = try {
6775
JSONObject(this.trim())
6876
} catch (_: Exception) {
69-
JSONObject()
77+
null
7078
}
7179
}
7280
}

‎app/src/testPlay/java/com/celzero/bravedns/iab/RpnPurchaseAckServerResponseTest.kt‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,20 @@ class RpnPurchaseAckServerResponseTest {
111111
assertTrue(result is RpnPurchaseAckServerResponse.Ok)
112112
assertFalse((result as RpnPurchaseAckServerResponse.Ok).payload.isCancelled)
113113
}
114+
115+
@Test
116+
fun `2xx without a body is not ok`() {
117+
val result = RpnPurchaseAckServerResponse.from(null, 200)
118+
119+
assertTrue(result is RpnPurchaseAckServerResponse.Err)
120+
assertEquals(200, (result as RpnPurchaseAckServerResponse.Err).payload.httpCode)
121+
}
122+
123+
@Test
124+
fun `non-json 2xx body from a proxy is not ok`() {
125+
val result = RpnPurchaseAckServerResponse.from("<html>Login required</html>", 200)
126+
127+
assertTrue(result is RpnPurchaseAckServerResponse.Err)
128+
assertFalse((result as RpnPurchaseAckServerResponse.Err).payload.isPurchaseCancelled)
129+
}
114130
}

‎app/src/testWebsite/java/com/celzero/bravedns/iab/RpnPurchaseAckServerResponseTest.kt‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,20 @@ class RpnPurchaseAckServerResponseTest {
111111
assertTrue(result is RpnPurchaseAckServerResponse.Ok)
112112
assertFalse((result as RpnPurchaseAckServerResponse.Ok).payload.isCancelled)
113113
}
114+
115+
@Test
116+
fun `2xx without a body is not ok`() {
117+
val result = RpnPurchaseAckServerResponse.from(null, 200)
118+
119+
assertTrue(result is RpnPurchaseAckServerResponse.Err)
120+
assertEquals(200, (result as RpnPurchaseAckServerResponse.Err).payload.httpCode)
121+
}
122+
123+
@Test
124+
fun `non-json 2xx body from a proxy is not ok`() {
125+
val result = RpnPurchaseAckServerResponse.from("<html>Login required</html>", 200)
126+
127+
assertTrue(result is RpnPurchaseAckServerResponse.Err)
128+
assertFalse((result as RpnPurchaseAckServerResponse.Err).payload.isPurchaseCancelled)
129+
}
114130
}

‎app/src/website/java/com/celzero/bravedns/iab/RpnPurchaseAckServerResponse.kt‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import org.json.JSONObject
2525
* Sealed result returned by every Rethink server billing endpoint.
2626
*
2727
* Use [RpnPurchaseAckServerResponse.from] to deserialise raw HTTP response bodies.
28-
* A 2xx response with `"success" != false` is treated as [Ok]; everything
29-
* else (non-2xx, `"error"` key present, or `"success" == false`) is [Err].
28+
* A 2xx response with a JSON body and `"success" != false` is treated as [Ok];
29+
* everything else (non-2xx, missing or non-JSON body, `"error"` key present, or
30+
* `"success" == false`) is [Err].
3031
*/
3132
sealed class RpnPurchaseAckServerResponse {
3233
data class Ok(val payload: ResponseOk) : RpnPurchaseAckServerResponse()
@@ -48,7 +49,14 @@ sealed class RpnPurchaseAckServerResponse {
4849
return Err(payload)
4950
}
5051

51-
val json = body?.trimmedJson() ?: JSONObject()
52+
// The server always replies with a JSON object (it carries at least "ray"),
53+
// so a missing or non-JSON 2xx body (e.g. from a proxy) is not a success.
54+
val json = body?.trimmedJson()
55+
if (json == null) {
56+
val payload = ResponseErr(error = "malformed response", httpCode = httpCode)
57+
Logger.d(LOG_IAB, "PlayErr: $payload")
58+
return Err(payload)
59+
}
5260

5361
// If "error" key is present, or "success" is explicitly false → error
5462
return if (json.has("error") || !json.optBoolean("success", true)) {
@@ -63,10 +71,10 @@ sealed class RpnPurchaseAckServerResponse {
6371
}
6472
}
6573

66-
private fun String.trimmedJson(): JSONObject = try {
74+
private fun String.trimmedJson(): JSONObject? = try {
6775
JSONObject(this.trim())
6876
} catch (_: Exception) {
69-
JSONObject()
77+
null
7078
}
7179
}
7280
}

0 commit comments

Comments
 (0)