Api.kt
| 1 | package org.opentracker |
| 2 | |
| 3 | import java.io.IOException |
| 4 | import java.net.HttpURLConnection |
| 5 | import java.net.URL |
| 6 | import org.json.JSONObject |
| 7 | |
| 8 | class HttpError(val status: Int, message: String) : IOException(message) |
| 9 | |
| 10 | /** Blocking HTTP calls. Run them off the main thread. Connections stay open between calls (keep-alive). */ |
| 11 | class Api(private val baseUrl: String, private val token: String? = null) { |
| 12 | |
| 13 | /** A JSON array of points, as the server's `Point` type. */ |
| 14 | fun upload(points: String) { |
| 15 | call("POST", "/api/points", points) |
| 16 | } |
| 17 | |
| 18 | /** Checks the token. */ |
| 19 | fun device() { |
| 20 | call("GET", "/api/device", null) |
| 21 | } |
| 22 | |
| 23 | fun pair(code: String, verifier: String): String { |
| 24 | val body = JSONObject().put("code", code).put("verifier", verifier).toString() |
| 25 | return JSONObject(call("POST", "/api/devices/pair", body)).getString("token") |
| 26 | } |
| 27 | |
| 28 | private fun call(method: String, path: String, body: String?): String { |
| 29 | val conn = URL(baseUrl + path).openConnection() as HttpURLConnection |
| 30 | try { |
| 31 | conn.requestMethod = method |
| 32 | conn.connectTimeout = 15_000 |
| 33 | conn.readTimeout = 30_000 |
| 34 | token?.let { conn.setRequestProperty("Authorization", "Bearer $it") } |
| 35 | if (body != null) { |
| 36 | conn.doOutput = true |
| 37 | conn.setRequestProperty("Content-Type", "application/json") |
| 38 | conn.outputStream.use { it.write(body.toByteArray()) } |
| 39 | } |
| 40 | val status = conn.responseCode |
| 41 | if (status !in 200..299) { |
| 42 | val msg = conn.errorStream?.bufferedReader()?.use { it.readText() }.orEmpty() |
| 43 | throw HttpError(status, msg.ifEmpty { "HTTP $status" }) |
| 44 | } |
| 45 | return conn.inputStream.bufferedReader().use { it.readText() } |
| 46 | } finally { |
| 47 | conn.disconnect() |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | companion object { |
| 52 | /** `track.example.com` becomes `https://track.example.com`. Returns null for an unusable address. */ |
| 53 | fun normalize(input: String): String? { |
| 54 | var url = input.trim().trimEnd('/') |
| 55 | if (url.isEmpty()) return null |
| 56 | if (!url.contains("://")) url = "https://$url" |
| 57 | val parsed = runCatching { URL(url) }.getOrNull() ?: return null |
| 58 | if (parsed.protocol !in listOf("http", "https") || parsed.host.isEmpty()) return null |
| 59 | return url |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 |