121 lines
4.1 KiB
Go
121 lines
4.1 KiB
Go
package companion
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var scumLoginLogLine = regexp.MustCompile(`^\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}: '([0-9.]+) (\d{1,50}):([^']{1,80})\(\d+\)' logged (in|out)(?: .*)?$`)
|
|
|
|
// ConsoleRecord is supplied by Run's stdout/stderr stream, not by the server
|
|
// execution log. The channel never accepts a file path or a raw log archive.
|
|
type ConsoleRecord struct {
|
|
ServerID string
|
|
Stream string
|
|
Sequence uint64
|
|
OccurredAt time.Time
|
|
Text string
|
|
}
|
|
type SemanticEvent struct {
|
|
ServerID string
|
|
Sequence uint64
|
|
Type string
|
|
PlayerID string
|
|
DisplayName string
|
|
OccurredAt time.Time
|
|
NetworkCorrelation string
|
|
}
|
|
type EventDiagnostic struct {
|
|
ServerID string
|
|
Sequence uint64
|
|
Code string
|
|
}
|
|
type SemanticEventBatch struct {
|
|
ServerID string
|
|
FirstSequence uint64
|
|
Events []SemanticEvent
|
|
Diagnostics []EventDiagnostic
|
|
}
|
|
|
|
// ParseConsoleRecords accepts only bounded stdout/stderr records. Unknown
|
|
// formats produce a bounded diagnostic and are skipped; they never fabricate
|
|
// events or carry raw console text across the plugin boundary.
|
|
func ParseConsoleRecords(serverID string, records []ConsoleRecord, correlationSecret string) SemanticEventBatch {
|
|
batch := SemanticEventBatch{ServerID: serverID}
|
|
if len(records) > 100 {
|
|
records = records[:100]
|
|
}
|
|
for _, record := range records {
|
|
if record.ServerID != serverID || (record.Stream != "stdout" && record.Stream != "stderr") || record.Sequence == 0 || record.OccurredAt.IsZero() || len(record.Text) > 1024 {
|
|
batch.Diagnostics = appendDiagnostic(batch.Diagnostics, EventDiagnostic{ServerID: serverID, Sequence: record.Sequence, Code: "invalid-console-record"})
|
|
continue
|
|
}
|
|
if batch.FirstSequence == 0 {
|
|
batch.FirstSequence = record.Sequence
|
|
}
|
|
event, ok := parseConsoleRecord(record, correlationSecret)
|
|
if !ok {
|
|
batch.Diagnostics = appendDiagnostic(batch.Diagnostics, EventDiagnostic{ServerID: serverID, Sequence: record.Sequence, Code: "unknown-console-format"})
|
|
continue
|
|
}
|
|
batch.Events = append(batch.Events, event)
|
|
}
|
|
return batch
|
|
}
|
|
func parseConsoleRecord(record ConsoleRecord, secret string) (SemanticEvent, bool) {
|
|
if event, ok := parseLoginLogRecord(record, secret); ok {
|
|
return event, true
|
|
}
|
|
fields := strings.Fields(record.Text)
|
|
if len(fields) < 3 || fields[0] != "SCUM" || (fields[1] != "LOGIN" && fields[1] != "LOGOUT") || !steamID64(fields[2]) {
|
|
return SemanticEvent{}, false
|
|
}
|
|
eventType := "scum.login"
|
|
if fields[1] == "LOGOUT" {
|
|
eventType = "scum.logout"
|
|
}
|
|
event := SemanticEvent{ServerID: record.ServerID, Sequence: record.Sequence, Type: eventType, PlayerID: fields[2], OccurredAt: record.OccurredAt}
|
|
if len(fields) == 4 && secret != "" {
|
|
event.NetworkCorrelation = networkCorrelation(record.ServerID, fields[3], secret)
|
|
}
|
|
return event, true
|
|
}
|
|
|
|
func parseLoginLogRecord(record ConsoleRecord, secret string) (SemanticEvent, bool) {
|
|
match := scumLoginLogLine.FindStringSubmatch(record.Text)
|
|
if match == nil || !steamID64(match[2]) {
|
|
return SemanticEvent{}, false
|
|
}
|
|
eventType := "scum.login"
|
|
if match[4] == "out" {
|
|
eventType = "scum.logout"
|
|
}
|
|
event := SemanticEvent{ServerID: record.ServerID, Sequence: record.Sequence, Type: eventType, PlayerID: match[2], DisplayName: match[3], OccurredAt: record.OccurredAt}
|
|
if secret != "" {
|
|
event.NetworkCorrelation = networkCorrelation(record.ServerID, match[1], secret)
|
|
}
|
|
return event, true
|
|
}
|
|
|
|
func networkCorrelation(serverID, value, secret string) string {
|
|
digest := sha256.Sum256([]byte(serverID + "\x00" + secret + "\x00" + value))
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
func appendDiagnostic(existing []EventDiagnostic, diagnostic EventDiagnostic) []EventDiagnostic {
|
|
if len(existing) >= 32 {
|
|
return existing
|
|
}
|
|
return append(existing, diagnostic)
|
|
}
|
|
func VerifiedSemanticEventProducer() SemanticEventProducerAvailability {
|
|
return SemanticEventProducerAvailability{Available: true, Reason: "Run stdout/stderr semantic parser is available"}
|
|
}
|
|
|
|
type SemanticEventProducerAvailability struct {
|
|
Available bool
|
|
Reason string
|
|
}
|