compat: stop mangling refusal messages with HTML escapes
The server's 426 body reached the user as "needs \u003e= 0.2.0, \u003c 1.0.0": Go escapes <, > and & by default for JSON destined for a page, which this is not. Disabled at the encoder. The client now parses the error field rather than pattern-matching it, so it survives whatever a future encoder decides to escape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9d6572bc33
commit
33a6acb0bf
@@ -4,6 +4,8 @@
|
|||||||
package app.echo_lot.protocol
|
package app.echo_lot.protocol
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
@@ -59,13 +61,16 @@ class ControlClient(
|
|||||||
if (conn.responseCode == 426) throw VersionRefused(extractError(body) ?: body.take(200))
|
if (conn.responseCode == 426) throw VersionRefused(extractError(body) ?: body.take(200))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pulls the "error" string out of a JSON body without pulling in a parser for one field. */
|
/**
|
||||||
private fun extractError(body: String): String? =
|
* Pulls the "error" string out of a JSON body.
|
||||||
Regex(""""error"\s*:\s*"((?:[^"\\]|\\.)*)"""").find(body)
|
*
|
||||||
?.groupValues?.get(1)
|
* Parsed rather than pattern-matched: an encoder may legitimately escape characters in the
|
||||||
?.replace("\\\"", "\"")
|
* message (Go escapes ">" by default), and a regex hands the user "needs \u003e= 0.2.0".
|
||||||
?.replace("\\n", "\n")
|
* The parser knows how to undo every escape; a regex would have to be taught each one.
|
||||||
?.replace("\\\\", "\\")
|
*/
|
||||||
|
private fun extractError(body: String): String? = runCatching {
|
||||||
|
json.parseToJsonElement(body).jsonObject["error"]?.jsonPrimitive?.content
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the response body, and turns a 426 into [VersionRefused] first.
|
* Reads the response body, and turns a 426 into [VersionRefused] first.
|
||||||
|
|||||||
@@ -430,7 +430,12 @@ func bearer(r *http.Request) string {
|
|||||||
func writeJSON(w http.ResponseWriter, code int, v any) {
|
func writeJSON(w http.ResponseWriter, code int, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
enc := json.NewEncoder(w)
|
||||||
|
// Go escapes <, > and & by default, for JSON embedded in HTML. This is an API, and the
|
||||||
|
// escaping is actively harmful here: a refusal message reading "needs >= 0.2.0" is what
|
||||||
|
// the user ends up seeing. Nothing we emit is ever interpolated into a page.
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
_ = enc.Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// enroll redeems a single-use enrollment token for a device credential (§2.1).
|
// enroll redeems a single-use enrollment token for a device credential (§2.1).
|
||||||
|
|||||||
Reference in New Issue
Block a user