620 lines
16 KiB
Go
620 lines
16 KiB
Go
package app
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// FTP Handler - implements basic FTP protocol with authentication logging
|
|
func (a *App) ftpHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("ftp_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "ftp", "connection_start", nil)
|
|
|
|
// Send FTP welcome banner
|
|
_, _ = conn.Write([]byte("220 Welcome to FTP Server\r\n"))
|
|
|
|
conn.SetDeadline(time.Now().Add(5 * time.Minute))
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
var username, password string
|
|
authenticated := false
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, " ", 2)
|
|
if len(parts) < 1 {
|
|
continue
|
|
}
|
|
|
|
cmd := strings.ToUpper(parts[0])
|
|
arg := ""
|
|
if len(parts) > 1 {
|
|
arg = parts[1]
|
|
}
|
|
|
|
switch cmd {
|
|
case "USER":
|
|
username = arg
|
|
a.logServiceEvent(sessionID, remote, "ftp", "username_attempt", map[string]string{"username": username})
|
|
_, _ = conn.Write([]byte("331 Password required for " + username + "\r\n"))
|
|
case "PASS":
|
|
password = arg
|
|
a.logServiceEvent(sessionID, remote, "ftp", "password_attempt", map[string]string{
|
|
"username": username,
|
|
"password": password,
|
|
})
|
|
_, _ = conn.Write([]byte("530 Login incorrect\r\n"))
|
|
case "QUIT":
|
|
_, _ = conn.Write([]byte("221 Goodbye\r\n"))
|
|
return
|
|
default:
|
|
if !authenticated {
|
|
_, _ = conn.Write([]byte("530 Please login with USER and PASS\r\n"))
|
|
} else {
|
|
_, _ = conn.Write([]byte("502 Command not implemented\r\n"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SMTP Handler - implements basic SMTP protocol
|
|
func (a *App) smtpHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("smtp_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "smtp", "connection_start", nil)
|
|
|
|
// Send SMTP welcome banner
|
|
_, _ = conn.Write([]byte("220 mail.example.com ESMTP Postfix\r\n"))
|
|
|
|
conn.SetDeadline(time.Now().Add(5 * time.Minute))
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, " ", 2)
|
|
if len(parts) < 1 {
|
|
continue
|
|
}
|
|
|
|
cmd := strings.ToUpper(parts[0])
|
|
arg := ""
|
|
if len(parts) > 1 {
|
|
arg = parts[1]
|
|
}
|
|
|
|
switch cmd {
|
|
case "HELO", "EHLO":
|
|
a.logServiceEvent(sessionID, remote, "smtp", "helo", map[string]string{"hostname": arg})
|
|
if cmd == "EHLO" {
|
|
_, _ = conn.Write([]byte("250-mail.example.com\r\n250-AUTH PLAIN LOGIN\r\n250 OK\r\n"))
|
|
} else {
|
|
_, _ = conn.Write([]byte("250 mail.example.com\r\n"))
|
|
}
|
|
case "AUTH":
|
|
a.handleSMTPAuth(conn, sessionID, remote, arg)
|
|
case "MAIL":
|
|
_, _ = conn.Write([]byte("250 OK\r\n"))
|
|
case "RCPT":
|
|
_, _ = conn.Write([]byte("250 OK\r\n"))
|
|
case "DATA":
|
|
_, _ = conn.Write([]byte("354 End data with <CR><LF>.<CR><LF>\r\n"))
|
|
case "QUIT":
|
|
_, _ = conn.Write([]byte("221 Bye\r\n"))
|
|
return
|
|
default:
|
|
_, _ = conn.Write([]byte("502 Command not implemented\r\n"))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleSMTPAuth(conn net.Conn, sessionID, remote, authLine string) {
|
|
parts := strings.Fields(authLine)
|
|
if len(parts) < 1 {
|
|
_, _ = conn.Write([]byte("501 Syntax error\r\n"))
|
|
return
|
|
}
|
|
|
|
method := strings.ToUpper(parts[0])
|
|
switch method {
|
|
case "PLAIN":
|
|
if len(parts) > 1 {
|
|
// Decode base64 auth
|
|
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
|
if err == nil {
|
|
authParts := strings.Split(string(decoded), "\x00")
|
|
if len(authParts) >= 3 {
|
|
username := authParts[1]
|
|
password := authParts[2]
|
|
a.logServiceEvent(sessionID, remote, "smtp", "auth_attempt", map[string]string{
|
|
"method": "PLAIN",
|
|
"username": username,
|
|
"password": password,
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
_, _ = conn.Write([]byte("334 \r\n"))
|
|
return
|
|
}
|
|
_, _ = conn.Write([]byte("535 Authentication failed\r\n"))
|
|
case "LOGIN":
|
|
_, _ = conn.Write([]byte("334 VXNlcm5hbWU6\r\n")) // "Username:" in base64
|
|
default:
|
|
_, _ = conn.Write([]byte("504 Authentication method not supported\r\n"))
|
|
}
|
|
}
|
|
|
|
// Telnet Handler
|
|
func (a *App) telnetHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("telnet_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "telnet", "connection_start", nil)
|
|
|
|
// Send telnet login prompt
|
|
_, _ = conn.Write([]byte("\r\nUbuntu 20.04.3 LTS\r\n\r\nlogin: "))
|
|
|
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
var username string
|
|
expectingPassword := false
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if !expectingPassword {
|
|
username = line
|
|
a.logServiceEvent(sessionID, remote, "telnet", "username_attempt", map[string]string{"username": username})
|
|
_, _ = conn.Write([]byte("Password: "))
|
|
expectingPassword = true
|
|
} else {
|
|
password := line
|
|
a.logServiceEvent(sessionID, remote, "telnet", "password_attempt", map[string]string{
|
|
"username": username,
|
|
"password": password,
|
|
})
|
|
_, _ = conn.Write([]byte("\r\nLogin incorrect\r\nlogin: "))
|
|
expectingPassword = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MySQL Handler
|
|
func (a *App) mysqlHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("mysql_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "mysql", "connection_start", nil)
|
|
|
|
// Send MySQL handshake packet
|
|
handshake := []byte{
|
|
0x4a, 0x00, 0x00, 0x00, // packet length + sequence
|
|
0x0a, // protocol version
|
|
}
|
|
handshake = append(handshake, []byte("5.7.34-0ubuntu0.18.04.1\x00")...) // server version
|
|
handshake = append(handshake, []byte{
|
|
0x01, 0x00, 0x00, 0x00, // connection id
|
|
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, // auth plugin data part 1
|
|
0x00, // filler
|
|
0xff, 0xf7, // capability flags lower 2 bytes
|
|
0x08, // character set
|
|
0x02, 0x00, // status flags
|
|
0x0f, 0x80, // capability flags upper 2 bytes
|
|
0x15, // auth plugin data length
|
|
}...)
|
|
|
|
_, _ = conn.Write(handshake)
|
|
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
buf := make([]byte, 1024)
|
|
n, err := conn.Read(buf)
|
|
if err == nil && n > 4 {
|
|
// Parse login packet (simplified)
|
|
payload := buf[4:n] // skip packet header
|
|
if len(payload) > 32 {
|
|
// Extract username (simplified parsing)
|
|
usernameStart := 32
|
|
usernameEnd := usernameStart
|
|
for usernameEnd < len(payload) && payload[usernameEnd] != 0 {
|
|
usernameEnd++
|
|
}
|
|
if usernameEnd < len(payload) {
|
|
username := string(payload[usernameStart:usernameEnd])
|
|
a.logServiceEvent(sessionID, remote, "mysql", "auth_attempt", map[string]string{
|
|
"username": username,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send error packet
|
|
errorPacket := []byte{
|
|
0x24, 0x00, 0x00, 0x01, // packet header
|
|
0xff, // error packet marker
|
|
0x10, 0x04, // error code
|
|
0x23, 0x48, 0x59, 0x30, 0x30, 0x30, // SQL state
|
|
}
|
|
errorPacket = append(errorPacket, []byte("Access denied for user")...)
|
|
_, _ = conn.Write(errorPacket)
|
|
}
|
|
|
|
// PostgreSQL Handler
|
|
func (a *App) postgresqlHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("postgresql_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "postgresql", "connection_start", nil)
|
|
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
buf := make([]byte, 1024)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if n >= 8 {
|
|
// Parse startup message
|
|
length := int(buf[0])<<24 | int(buf[1])<<16 | int(buf[2])<<8 | int(buf[3])
|
|
if length > 8 && n >= length {
|
|
params := string(buf[8:length])
|
|
// Extract username from parameters
|
|
if strings.Contains(params, "user") {
|
|
re := regexp.MustCompile(`user\x00([^\x00]+)`)
|
|
matches := re.FindStringSubmatch(params)
|
|
if len(matches) > 1 {
|
|
username := matches[1]
|
|
a.logServiceEvent(sessionID, remote, "postgresql", "auth_attempt", map[string]string{
|
|
"username": username,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send authentication request
|
|
authRequest := []byte{
|
|
0x52, // Authentication message type
|
|
0x00, 0x00, 0x00, 0x08, // message length
|
|
0x00, 0x00, 0x00, 0x03, // auth type (cleartext password)
|
|
}
|
|
_, _ = conn.Write(authRequest)
|
|
|
|
// Read password response
|
|
n, err = conn.Read(buf)
|
|
if err == nil && n > 5 && buf[0] == 0x70 { // password message
|
|
length := int(buf[1])<<24 | int(buf[2])<<16 | int(buf[3])<<8 | int(buf[4])
|
|
if length > 5 && n >= length {
|
|
password := string(buf[5 : length-1]) // exclude null terminator
|
|
a.logServiceEvent(sessionID, remote, "postgresql", "password_attempt", map[string]string{
|
|
"password": password,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Send error response
|
|
errorMsg := []byte{
|
|
0x45, // Error message type
|
|
0x00, 0x00, 0x00, 0x26, // message length
|
|
}
|
|
errorMsg = append(errorMsg, []byte("SFATAL\x00C28P01\x00Mpassword authentication failed\x00\x00")...)
|
|
_, _ = conn.Write(errorMsg)
|
|
}
|
|
|
|
// Redis Handler
|
|
func (a *App) redisHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("redis_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "redis", "connection_start", nil)
|
|
|
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// Parse Redis protocol
|
|
if strings.HasPrefix(line, "*") {
|
|
// Multi-bulk request
|
|
continue
|
|
} else if strings.HasPrefix(line, "$") {
|
|
// Bulk string length
|
|
continue
|
|
} else {
|
|
// Command
|
|
cmd := strings.ToUpper(line)
|
|
switch cmd {
|
|
case "AUTH":
|
|
// Next line should be password
|
|
if scanner.Scan() {
|
|
password := strings.TrimSpace(scanner.Text())
|
|
a.logServiceEvent(sessionID, remote, "redis", "auth_attempt", map[string]string{
|
|
"password": password,
|
|
})
|
|
}
|
|
_, _ = conn.Write([]byte("-ERR invalid password\r\n"))
|
|
case "PING":
|
|
_, _ = conn.Write([]byte("+PONG\r\n"))
|
|
case "INFO":
|
|
_, _ = conn.Write([]byte("+redis_version:6.2.6\r\n"))
|
|
case "QUIT":
|
|
_, _ = conn.Write([]byte("+OK\r\n"))
|
|
return
|
|
default:
|
|
_, _ = conn.Write([]byte("-ERR unknown command\r\n"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MongoDB Handler
|
|
func (a *App) mongodbHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("mongodb_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "mongodb", "connection_start", nil)
|
|
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
buf := make([]byte, 1024)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if n >= 16 {
|
|
// Parse MongoDB wire protocol message
|
|
// This is a simplified implementation
|
|
a.logServiceEvent(sessionID, remote, "mongodb", "protocol_attempt", map[string]string{
|
|
"bytes_received": strconv.Itoa(n),
|
|
})
|
|
}
|
|
|
|
// Send connection error
|
|
_, _ = conn.Write([]byte("connection refused"))
|
|
}
|
|
|
|
// POP3 Handler
|
|
func (a *App) pop3Handler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("pop3_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "pop3", "connection_start", nil)
|
|
|
|
_, _ = conn.Write([]byte("+OK POP3 server ready\r\n"))
|
|
|
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
var username string
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, " ", 2)
|
|
if len(parts) < 1 {
|
|
continue
|
|
}
|
|
|
|
cmd := strings.ToUpper(parts[0])
|
|
arg := ""
|
|
if len(parts) > 1 {
|
|
arg = parts[1]
|
|
}
|
|
|
|
switch cmd {
|
|
case "USER":
|
|
username = arg
|
|
a.logServiceEvent(sessionID, remote, "pop3", "username_attempt", map[string]string{"username": username})
|
|
_, _ = conn.Write([]byte("+OK\r\n"))
|
|
case "PASS":
|
|
password := arg
|
|
a.logServiceEvent(sessionID, remote, "pop3", "password_attempt", map[string]string{
|
|
"username": username,
|
|
"password": password,
|
|
})
|
|
_, _ = conn.Write([]byte("-ERR Authentication failed\r\n"))
|
|
case "QUIT":
|
|
_, _ = conn.Write([]byte("+OK Bye\r\n"))
|
|
return
|
|
default:
|
|
_, _ = conn.Write([]byte("-ERR Unknown command\r\n"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// IMAP Handler
|
|
func (a *App) imapHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("imap_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "imap", "connection_start", nil)
|
|
|
|
_, _ = conn.Write([]byte("* OK IMAP4rev1 Service Ready\r\n"))
|
|
|
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
tag := parts[0]
|
|
cmd := strings.ToUpper(parts[1])
|
|
|
|
switch cmd {
|
|
case "LOGIN":
|
|
if len(parts) >= 4 {
|
|
username := strings.Trim(parts[2], "\"")
|
|
password := strings.Trim(parts[3], "\"")
|
|
a.logServiceEvent(sessionID, remote, "imap", "login_attempt", map[string]string{
|
|
"username": username,
|
|
"password": password,
|
|
})
|
|
}
|
|
_, _ = conn.Write([]byte(tag + " NO LOGIN failed\r\n"))
|
|
case "CAPABILITY":
|
|
_, _ = conn.Write([]byte("* CAPABILITY IMAP4rev1 AUTH=PLAIN\r\n"))
|
|
_, _ = conn.Write([]byte(tag + " OK CAPABILITY completed\r\n"))
|
|
case "LOGOUT":
|
|
_, _ = conn.Write([]byte("* BYE IMAP4rev1 Server logging out\r\n"))
|
|
_, _ = conn.Write([]byte(tag + " OK LOGOUT completed\r\n"))
|
|
return
|
|
default:
|
|
_, _ = conn.Write([]byte(tag + " BAD Command not recognized\r\n"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// RDP Handler (simplified)
|
|
func (a *App) rdpHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("rdp_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "rdp", "connection_start", nil)
|
|
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
buf := make([]byte, 1024)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if n > 0 {
|
|
a.logServiceEvent(sessionID, remote, "rdp", "protocol_attempt", map[string]string{
|
|
"bytes_received": strconv.Itoa(n),
|
|
})
|
|
}
|
|
|
|
// Send RDP connection failure
|
|
_, _ = conn.Write([]byte{0x03, 0x00, 0x00, 0x0b, 0x02, 0xf0, 0x80, 0x04, 0x01, 0x00, 0x01})
|
|
}
|
|
|
|
// DNS Handler (UDP)
|
|
func (a *App) dnsHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("dns_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "dns", "query_attempt", nil)
|
|
|
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
buf := make([]byte, 512)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if n > 12 {
|
|
// Parse DNS query (simplified)
|
|
a.logServiceEvent(sessionID, remote, "dns", "query_received", map[string]string{
|
|
"query_size": strconv.Itoa(n),
|
|
})
|
|
}
|
|
}
|
|
|
|
// SNMP Handler (UDP)
|
|
func (a *App) snmpHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("snmp_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "snmp", "request_attempt", nil)
|
|
|
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
buf := make([]byte, 1024)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if n > 0 {
|
|
a.logServiceEvent(sessionID, remote, "snmp", "request_received", map[string]string{
|
|
"request_size": strconv.Itoa(n),
|
|
})
|
|
}
|
|
}
|
|
|
|
// LDAP Handler
|
|
func (a *App) ldapHandler(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("ldap_%x", time.Now().UnixNano())
|
|
|
|
a.logServiceEvent(sessionID, remote, "ldap", "connection_start", nil)
|
|
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
buf := make([]byte, 1024)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if n > 0 {
|
|
a.logServiceEvent(sessionID, remote, "ldap", "bind_attempt", map[string]string{
|
|
"request_size": strconv.Itoa(n),
|
|
})
|
|
}
|
|
|
|
// Send LDAP bind failure
|
|
_, _ = conn.Write([]byte{0x30, 0x0c, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0a, 0x01, 0x31, 0x04, 0x00, 0x04, 0x00})
|
|
}
|
|
|
|
// Helper function to log service events
|
|
func (a *App) logServiceEvent(sessionID, remote, service, eventType string, details map[string]string) {
|
|
if details == nil {
|
|
details = make(map[string]string)
|
|
}
|
|
details["session_id"] = sessionID
|
|
details["event_type"] = eventType
|
|
|
|
a.logEvent(Record{
|
|
Timestamp: time.Now().UTC(),
|
|
RemoteAddr: remoteIP(remote),
|
|
RemotePort: remotePort(remote),
|
|
Service: service,
|
|
Details: details,
|
|
RawPayload: fmt.Sprintf("%s: %s", eventType, sessionID),
|
|
})
|
|
}
|