package de.upapp import org.bouncycastle.tls.CipherSuite import org.bouncycastle.tls.PSKTlsServer import org.bouncycastle.tls.ProtocolVersion import org.bouncycastle.tls.TlsPSKIdentityManager import org.bouncycastle.tls.TlsServerProtocol import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto import org.json.JSONArray import org.json.JSONObject import java.io.DataInputStream import java.io.EOFException import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.security.SecureRandom import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.Locale import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import kotlin.concurrent.thread const val DEMO_VIN = "WVWZZZAAZLD000042" const val DEMO_CREDENTIALS = "demo,demo" /** Car end of a tunnel, for the demo and tests. [serve] handles each connection that the phone opens. */ class CarSide(serve: (port: Int, InputStream, OutputStream) -> Unit) { private val toPhone = Pipe() private val toCar = Pipe() val tunnel = Tunnel(toPhone.input, toCar.output) init { thread(isDaemon = true, name = "car") { val pipes = ConcurrentHashMap() val chunk = ByteArray(4096) var buf = ByteArray(0) while (true) { val n = toCar.input.read(chunk) if (n < 0) break buf += chunk.copyOf(n) while (true) { val (s, size) = decode(buf) ?: break buf = buf.copyOfRange(size, buf.size) when (s.type) { HEARTBEAT -> toPhone.put(frame(0, HEARTBEAT)) OPEN -> { val pipe = Pipe().also { pipes[s.port] = it } val remote = (s.data[0].toInt() and 0xFF) or ((s.data[1].toInt() and 0xFF) shl 8) val out = object : OutputStream() { override fun write(b: Int) = write(byteArrayOf(b.toByte()), 0, 1) override fun write(b: ByteArray, off: Int, len: Int) = toPhone.put(frame(s.port, TRANSMIT, u16(len) + b.copyOfRange(off, off + len))) } thread(isDaemon = true) { try { serve(remote, pipe.input, out) } catch (_: Exception) { } if (pipes.remove(s.port) != null) toPhone.put(frame(s.port, CLOSE)) } } TRANSMIT -> pipes[s.port]?.put(s.data) CLOSE -> pipes.remove(s.port)?.end() ?: Unit } } } pipes.values.forEach { it.end() } } } } private class Request(val method: String, val path: String, val headers: Map, val body: String) private fun readRequest(input: InputStream): Request? { val first = try { input.line() } catch (_: EOFException) { return null } val (method, path) = first.split(' ') val headers = generateSequence { input.line().ifEmpty { null } } .associate { it.substringBefore(':').trim().lowercase() to it.substringAfter(':').trim() } val body = ByteArray(headers["content-length"]?.toInt() ?: 0).also { DataInputStream(input).readFully(it) } return Request(method, path, headers, body.decodeToString()) } private fun respond(out: OutputStream, status: Int, body: Any? = null, headers: List = emptyList()) { val text = body?.toString().orEmpty().toByteArray() val head = (listOf("HTTP/1.1 $status X", "Content-Length: ${text.size}") + headers).joinToString("\r\n") + "\r\n\r\n" out.write(head.toByteArray() + text) out.flush() } /** A simulated e-up! that serves the REST API and events in memory. */ class DemoCar { private val docs = LinkedHashMap() private val listeners = CopyOnWriteArrayList() val tunnel = CarSide(::serve).tunnel init { val now = LocalDateTime.now() add("/car/info/", "info", "vehicleIdenticationNumber" to DEMO_VIN, "vehicleType" to "VW120", "language" to "de", "vehicleDate" to now.format(DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH)), "vehicleTime" to now.format(DateTimeFormatter.ofPattern("HH:mm:ss"))) add("/car/batteries/", "BEV battery", "soc" to 64.0, "socUnit" to "percent") add("/car/ranges/", "BEV range", "value" to 151.0, "valueUnit" to "km") add("/car/environments/", "environment", "outsideTemperature" to 12.5, "outsideTemperatureUnit" to "celsius", "lightIntensity" to 40) add("/chargingmanager/batteryCharges/", "batteryCharge", "state" to "running") add("/chargingmanager/batteryPlugs/", "batteryPlug", "plug" to "plugged") add("/chargingmanager/batteryClimates/", "batteryClimate", "state" to "idle") add("/chargingmanager/profiles/", "Optionen", "operations" to JSONArray(listOf("climateExtSupply", "climate")), "maxCurrent" to 16, "minLevel" to 30, "targetLevel" to 0, "temperature" to 21.5) val home = createProfile("Zuhause", 80, 16, listOf("charge", "climate")) val work = createProfile("Arbeit", 100, 10, listOf("charge")) add("/chargingmanager/timers/", "Timer1", "state" to TIMER_ON, "departureTime" to "05:30:00", "cyclic" to true, "weekdays" to JSONArray(listOf("monday", "tuesday", "wednesday", "thursday", "friday")), "profile" to link(home)) add("/chargingmanager/timers/", "Timer2", "state" to TIMER_OFF, "departureTime" to "08:00:00", "cyclic" to true, "weekdays" to JSONArray(listOf("saturday")), "profile" to link(work)) add("/chargingmanager/timers/", "Timer3", "state" to TIMER_OFF, "departureTime" to "16:45:00", "cyclic" to false, "departureDate" to now.plusDays(3).format(DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH)), "weekdays" to JSONArray(), "profile" to link(home)) thread(isDaemon = true, name = "demo charging") { simulateCharging() } } private fun add(collection: String, name: String, vararg fields: Pair): JSONObject { val id = UUID.randomUUID().toString() val doc = JSONObject().put("id", id).put("name", name).put("uri", collection + id) fields.forEach { (k, v) -> doc.put(k, v) } docs[doc.getString("uri")] = doc return doc } private fun createProfile(name: String, target: Int, current: Int, operations: List): JSONObject { val provider = add("/chargingmanager/providers/", "", "cyclic" to true, "preferredTimeStart" to "21:00:00", "preferredTimeEnd" to "05:00:00", "weekdays" to JSONArray(listOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"))) return add("/chargingmanager/profiles/", name, "operations" to JSONArray(operations), "maxCurrent" to current, "minLevel" to 0, "targetLevel" to target, "temperature" to 21.0, "powerProvider" to link(provider)) } private fun link(doc: JSONObject) = JSONObject().put("id", doc["id"]).put("name", doc["name"]).put("uri", doc["uri"]) private fun collection(path: String) = JSONArray(docs.filterKeys { it.startsWith(path) && it != path }.values) private fun first(collection: String) = docs.entries.first { it.key.startsWith(collection) }.value private fun simulateCharging() { while (true) { Thread.sleep(3_000) synchronized(this) { val charge = first("/chargingmanager/batteryCharges/") if (charge["state"] != "running") return@synchronized val battery = first("/car/batteries/") val soc = battery.getDouble("soc") + 1 battery.put("soc", soc) first("/car/ranges/").put("value", (soc * 2.4).toInt().toDouble()) if (soc >= 80) charge.put("state", "completed") listOf("/car/batteries/", "/car/ranges/", "/chargingmanager/batteryCharges/").forEach(::notify) } } } private fun notify(collection: String) { val message = JSONObject().put("type", "data").put("event", "$collection#42").put("data", collection(collection)) listeners.forEach { out -> runCatching { writeFrame(out, WS_TEXT, message.toString().toByteArray(), mask = false) }.onFailure { listeners.remove(out) } } } private fun serve(port: Int, input: InputStream, output: OutputStream) { if (port == 80) { val r = readRequest(input) ?: return if (r.path == "/car/info/vin") respond(output, 200, DEMO_VIN) else respond(output, 404) return } val tls = TlsServerProtocol(input, output) tls.accept(object : PSKTlsServer(BcTlsCrypto(SecureRandom()), object : TlsPSKIdentityManager { override fun getHint(): ByteArray = DEMO_VIN.toByteArray() override fun getPSK(identity: ByteArray) = DEMO_CREDENTIALS.substringAfter(',').toByteArray() }) { override fun getSupportedVersions(): Array = ProtocolVersion.TLSv12.only() override fun getSupportedCipherSuites() = intArrayOf(CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256) }) val inp = tls.inputStream val out = tls.outputStream while (true) { val r = readRequest(inp) ?: break if (r.headers["upgrade"].equals("websocket", ignoreCase = true)) { respond(out, 101, null, listOf("Upgrade: websocket", "Connection: Upgrade", "Sec-WebSocket-Accept: ${wsAccept(r.headers.getValue("sec-websocket-key"))}")) events(inp, out) break } synchronized(this) { handle(r, out) } } tls.close() } private fun events(input: InputStream, output: OutputStream) { listeners.add(output) try { while (true) { val (_, data) = readMessage(input, output, mask = false) val m = JSONObject(data.decodeToString()) val reply = JSONObject().put("type", m.getString("type")).put("event", m.getString("event")).put("status", "ok") writeFrame(output, WS_TEXT, reply.toString().toByteArray(), mask = false) } } finally { listeners.remove(output) } } private fun handle(r: Request, out: OutputStream) { val path = r.path val doc = docs[path] when { r.method == "GET" && path == "/" -> respond(out, 200, JSONObject()) r.method == "GET" && doc != null -> respond(out, 200, JSONObject().put("status", "ok").put("data", doc)) r.method == "GET" && path.endsWith("/") && docs.keys.any { it.startsWith(path) } -> respond(out, 200, JSONObject().put("data", collection(path))) r.method == "POST" && path == "/chargingmanager/profiles/" -> { val b = JSONObject(r.body) val created = createProfile(b.getString("name"), b.getInt("targetLevel"), b.getInt("maxCurrent"), b.getJSONArray("operations").let { a -> List(a.length()) { a.getString(it) } }) respond(out, 201, created) notify("/chargingmanager/profiles/") notify("/chargingmanager/providers/") } r.method == "POST" && doc != null -> { val b = JSONObject(r.body) b.keys().forEach { k -> val v = b.get(k) val target = (v as? String)?.let { id -> docs.values.firstOrNull { it.optString("id") == id } } doc.put(k, if (k in setOf("profile", "powerProvider") && target != null) link(target) else v) } respond(out, 200, JSONObject().put("status", "ok")) notify(path.substringBeforeLast('/') + "/") } r.method == "DELETE" && doc != null -> { docs.remove(path) doc.optJSONObject("powerProvider")?.let { docs.remove(it.getString("uri")) } respond(out, 200) notify("/chargingmanager/profiles/") notify("/chargingmanager/providers/") } else -> respond(out, 404) } } }