Ability to read websocket join messages

This commit is contained in:
Paul-Henri Froidmont 2025-08-23 01:42:56 +02:00
parent aae3db841b
commit fadef26425
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
8 changed files with 158 additions and 52 deletions

View file

@ -24,6 +24,9 @@ class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
def prepended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.prependedAll(mod))
def apended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.appendedAll(mod))
class HtmlTag(val name: String, val void: Boolean = false):
def apply(mods: Mod*): HtmlElement = HtmlElement(this, mods.toVector)

View file

@ -2,22 +2,27 @@ package scalive
import zio.json.*
import java.util.Base64
import scala.util.Random
final case class Socket[Cmd](lv: LiveView[Cmd]):
lv.el.syncAll()
private var clientInitialized = false
val id: String = "scl-123"
val id: String = s"phx-${Base64.getEncoder().encodeToString(Random().nextBytes(8))}"
private val token = Token.sign("secret", id, "")
private val element = lv.el.prepended(idAttr := id, dataAttr("phx-session") := token)
element.syncAll()
def receiveCommand(cmd: Cmd): Unit =
lv.handleCommand(cmd)
def renderHtml(rootLayout: HtmlElement => HtmlElement = identity): String =
lv.el.syncAll()
HtmlBuilder.build(rootLayout(lv.el))
element.syncAll()
HtmlBuilder.build(rootLayout(element))
def syncClient: Unit =
lv.el.syncAll()
println(DiffBuilder.build(lv.el, trackUpdates = clientInitialized).toJsonPretty)
element.syncAll()
println(DiffBuilder.build(element, trackUpdates = clientInitialized).toJsonPretty)
clientInitialized = true
lv.el.setAllUnchanged()
element.setAllUnchanged()

View file

@ -0,0 +1,59 @@
package scalive
import zio.json.*
import java.time.Instant
import java.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import scala.concurrent.duration.Duration
import scala.util.Random
final case class Token[T] private (
version: Int,
liveViewId: String,
payload: T,
issuedAt: Long,
salt: String)
derives JsonCodec
object Token:
private val version = 1
def sign[T: JsonCodec](secret: String, liveViewId: String, payload: T)
: String = // TODO use messagepack and add salt
val salt = Random.nextString(16)
val token =
Token(version, liveViewId, payload, Instant.now().toEpochMilli(), salt).toJson.getBytes()
val tokenHash = hash(secret, token)
s"${base64Encode(token)}.${base64Encode(tokenHash)}"
private def hash(secret: String, value: Array[Byte]): Array[Byte] =
val mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"))
mac.doFinal(value)
private def base64Encode(value: Array[Byte]): String =
Base64.getEncoder().encodeToString(value)
private def base64Decode(value: String): Array[Byte] =
Base64.getDecoder().decode(value)
def verify[T: JsonCodec](secret: String, token: String, maxAge: Duration)
: Either[String, (liveViewId: String, payload: T)] =
val tokenBase64 = token.takeWhile(_ != '.')
val hashBase64 = token.drop(tokenBase64.length)
val tokenBytes = base64Decode(tokenBase64)
val tokenValue = String.valueOf(tokenBytes).fromJson[Token[T]]
val currentHash = hash(secret, tokenBytes)
if base64Decode(hashBase64) == currentHash then
tokenValue.flatMap(t =>
if (t.issuedAt + maxAge.toMillis) > Instant.now().toEpochMilli() then Left("Token expired")
else Right(t.liveViewId, t.payload)
)
else Left("Invalid signature")
end Token