package directstream import ( "errors" "fmt" "net" "strings" "time" ) const ( SessionEventConnected = "connected" SessionEventDisconnected = "disconnected" ) // SessionEvent is emitted by tera-sessiond only after a ticket-bound Direct // Mode attachment has been authenticated. Source is the normalized client IP // supplied by the trusted same-origin gateway. type SessionEvent struct { Type string `json:"type"` Source string `json:"source"` Timestamp time.Time `json:"timestamp"` } func validateSessionEvent(event SessionEvent) error { if event.Type != SessionEventConnected && event.Type != SessionEventDisconnected { return errors.New("direct stream session event type is invalid") } if net.ParseIP(strings.TrimSpace(event.Source)) == nil { return errors.New("direct stream session event source must be an IP address") } if event.Timestamp.IsZero() { return errors.New("direct stream session event timestamp is required") } return nil } // SessionEventMessage is shared by the WebSocket welcome frame and the // authoritative per-port log writer so the operator sees the same identity // data that is retained for the channel. func SessionEventMessage(claims TicketClaims, event SessionEvent) string { timestamp := event.Timestamp.UTC().Format(time.RFC3339) if event.Type == SessionEventConnected { return fmt.Sprintf( "Welcome to TERA CORA-X48 PRO K\nConnected from: %s\nSerial channel: %s\nUser: %s\nAccess: %s\nSession: %s\nConnected at: %s", event.Source, claims.Channel, claims.Actor, claims.Access, claims.SessionID, timestamp, ) } return fmt.Sprintf( "TERA CORA-X48 PRO K session disconnected\nConnected from: %s\nSerial channel: %s\nUser: %s\nAccess: %s\nSession: %s\nDisconnected at: %s", event.Source, claims.Channel, claims.Actor, claims.Access, claims.SessionID, timestamp, ) } func welcomeFrame(claims TicketClaims, event SessionEvent) []byte { message := strings.ReplaceAll(SessionEventMessage(claims, event), "\n", "\r\n") return []byte("\r\n" + message + "\r\n\r\n") }