1219 lines
34 KiB
Go
1219 lines
34 KiB
Go
package control
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.openprivacy.ca/openprivacy/bine/torutil"
|
|
)
|
|
|
|
// EventCode represents an asynchronous event code (ref control spec 4.1).
|
|
type EventCode string
|
|
|
|
// Event codes
|
|
const (
|
|
EventCodeAddrMap EventCode = "ADDRMAP"
|
|
EventCodeBandwidth EventCode = "BW"
|
|
EventCodeBuildTimeoutSet EventCode = "BUILDTIMEOUT_SET"
|
|
EventCodeCellStats EventCode = "CELL_STATS"
|
|
EventCodeCircuit EventCode = "CIRC"
|
|
EventCodeCircuitBandwidth EventCode = "CIRC_BW"
|
|
EventCodeCircuitMinor EventCode = "CIRC_MINOR"
|
|
EventCodeClientsSeen EventCode = "CLIENTS_SEEN"
|
|
EventCodeConfChanged EventCode = "CONF_CHANGED"
|
|
EventCodeConnBandwidth EventCode = "CONN_BW"
|
|
EventCodeDescChanged EventCode = "DESCCHANGED"
|
|
EventCodeGuard EventCode = "GUARD"
|
|
EventCodeHSDesc EventCode = "HS_DESC"
|
|
EventCodeHSDescContent EventCode = "HS_DESC_CONTENT"
|
|
EventCodeLogDebug EventCode = "DEBUG"
|
|
EventCodeLogErr EventCode = "ERR"
|
|
EventCodeLogInfo EventCode = "INFO"
|
|
EventCodeLogNotice EventCode = "NOTICE"
|
|
EventCodeLogWarn EventCode = "WARN"
|
|
EventCodeNetworkLiveness EventCode = "NETWORK_LIVENESS"
|
|
EventCodeNetworkStatus EventCode = "NS"
|
|
EventCodeNewConsensus EventCode = "NEWCONSENSUS"
|
|
EventCodeNewDesc EventCode = "NEWDESC"
|
|
EventCodeORConn EventCode = "ORCONN"
|
|
EventCodeSignal EventCode = "SIGNAL"
|
|
EventCodeStatusClient EventCode = "STATUS_CLIENT"
|
|
EventCodeStatusGeneral EventCode = "STATUS_GENERAL"
|
|
EventCodeStatusServer EventCode = "STATUS_SERVER"
|
|
EventCodeStream EventCode = "STREAM"
|
|
EventCodeStreamBandwidth EventCode = "STREAM_BW"
|
|
EventCodeTokenBucketEmpty EventCode = "TB_EMPTY"
|
|
EventCodeTransportLaunched EventCode = "TRANSPORT_LAUNCHED"
|
|
)
|
|
|
|
// EventCodeUnrecognized is a special event code that is only used with
|
|
// AddEventListener and RemoveEventListener to listen for events that are
|
|
// unrecognized by this library (i.e. UnrecognizedEvent).
|
|
var EventCodeUnrecognized EventCode = "<unrecognized>"
|
|
|
|
var recognizedEventCodes = []EventCode{
|
|
EventCodeAddrMap,
|
|
EventCodeBandwidth,
|
|
EventCodeBuildTimeoutSet,
|
|
EventCodeCellStats,
|
|
EventCodeCircuit,
|
|
EventCodeCircuitBandwidth,
|
|
EventCodeCircuitMinor,
|
|
EventCodeClientsSeen,
|
|
EventCodeConfChanged,
|
|
EventCodeConnBandwidth,
|
|
EventCodeDescChanged,
|
|
EventCodeGuard,
|
|
EventCodeHSDesc,
|
|
EventCodeHSDescContent,
|
|
EventCodeLogDebug,
|
|
EventCodeLogErr,
|
|
EventCodeLogInfo,
|
|
EventCodeLogNotice,
|
|
EventCodeLogWarn,
|
|
EventCodeNetworkLiveness,
|
|
EventCodeNetworkStatus,
|
|
EventCodeNewConsensus,
|
|
EventCodeNewDesc,
|
|
EventCodeORConn,
|
|
EventCodeSignal,
|
|
EventCodeStatusClient,
|
|
EventCodeStatusGeneral,
|
|
EventCodeStatusServer,
|
|
EventCodeStream,
|
|
EventCodeStreamBandwidth,
|
|
EventCodeTokenBucketEmpty,
|
|
EventCodeTransportLaunched,
|
|
}
|
|
|
|
var recognizedEventCodesByCode = mapEventCodes()
|
|
|
|
func mapEventCodes() map[EventCode]struct{} {
|
|
ret := make(map[EventCode]struct{}, len(recognizedEventCodes))
|
|
for _, eventCode := range recognizedEventCodes {
|
|
ret[eventCode] = struct{}{}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// EventCodes returns a new slice of all event codes that are recognized (i.e.
|
|
// does not include EventCodeUnrecognized).
|
|
func EventCodes() []EventCode {
|
|
ret := make([]EventCode, len(recognizedEventCodes))
|
|
copy(ret, recognizedEventCodes)
|
|
return ret
|
|
}
|
|
|
|
// ErrEventWaitSynchronousResponseOccurred is returned from EventWait (see docs)
|
|
var ErrEventWaitSynchronousResponseOccurred = errors.New("Synchronous event occurred during EventWait")
|
|
|
|
// EventWait waits for the predicate to be satisified or a non-event message to
|
|
// come through. If a non-event comes through, the error
|
|
// ErrEventWaitSynchronousResponseOccurred is returned. If there is an error in
|
|
// the predicate or if the context completes or there is an error internally
|
|
// handling the event, the error is returned. Otherwise, the event that true was
|
|
// returned from the predicate for is returned.
|
|
func (c *Conn) EventWait(
|
|
ctx context.Context, events []EventCode, predicate func(Event) (bool, error),
|
|
) (Event, error) {
|
|
eventCh := make(chan Event, 10)
|
|
defer close(eventCh)
|
|
if err := c.AddEventListener(eventCh, events...); err != nil {
|
|
return nil, err
|
|
}
|
|
defer c.RemoveEventListener(eventCh, events...)
|
|
eventCtx, eventCancel := context.WithCancel(ctx)
|
|
defer eventCancel()
|
|
errCh := make(chan error, 1)
|
|
go func() { errCh <- c.HandleEvents(eventCtx) }()
|
|
for {
|
|
select {
|
|
case <-eventCtx.Done():
|
|
return nil, eventCtx.Err()
|
|
case err := <-errCh:
|
|
return nil, err
|
|
case event := <-eventCh:
|
|
if ok, err := predicate(event); err != nil {
|
|
return nil, err
|
|
} else if ok {
|
|
return event, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// HandleEvents loops until the context is closed dispatching async events. Can
|
|
// dispatch events even after context is done and of course during synchronous
|
|
// request. This will always end with an error, either from ctx.Done() or from
|
|
// an error reading/handling the event.
|
|
func (c *Conn) HandleEvents(ctx context.Context) error {
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
for ctx.Err() == nil {
|
|
if err := c.HandleNextEvent(); err != nil {
|
|
errCh <- err
|
|
break
|
|
}
|
|
}
|
|
}()
|
|
select {
|
|
case err := <-errCh:
|
|
return err
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
// HandleNextEvent attempts to read and handle the next event. It will return on
|
|
// first message seen, event or not. Otherwise it will wait until there is a
|
|
// message read.
|
|
func (c *Conn) HandleNextEvent() error {
|
|
c.readLock.Lock()
|
|
defer c.readLock.Unlock()
|
|
// We'll just peek for the next 3 bytes and see if they are async
|
|
byts, err := c.conn.R.Peek(3)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
statusCode, err := strconv.Atoi(string(byts))
|
|
if err != nil || statusCode != StatusAsyncEvent {
|
|
return err
|
|
}
|
|
// Read the entire thing and handle it
|
|
resp, err := c.ReadResponse()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.relayAsyncEvents(resp)
|
|
return nil
|
|
}
|
|
|
|
// AddEventListener adds the given channel as an event listener for the given
|
|
// events. Then Tor is notified about which events should be listened to.
|
|
// Callers are expected to call RemoveEventListener for the channel and all
|
|
// event codes used here before closing the channel. If no events are provided,
|
|
// this is essentially a no-op. The EventCodeUnrecognized event code can be used
|
|
// to listen for unrecognized events.
|
|
func (c *Conn) AddEventListener(ch chan<- Event, events ...EventCode) error {
|
|
c.addEventListenerToMap(ch, events...)
|
|
// If there is an error updating the events, remove what we just added
|
|
err := c.sendSetEvents()
|
|
if err != nil {
|
|
c.removeEventListenerFromMap(ch, events...)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RemoveEventListener removes the given channel from being sent to by the given
|
|
// event codes. It is not an error to remove a channel from events
|
|
// AddEventListener was not called for. Tor is notified about events which may
|
|
// no longer be listened to. If no events are provided, this is essentially a
|
|
// no-op.
|
|
func (c *Conn) RemoveEventListener(ch chan<- Event, events ...EventCode) error {
|
|
c.removeEventListenerFromMap(ch, events...)
|
|
return c.sendSetEvents()
|
|
}
|
|
|
|
func (c *Conn) addEventListenerToMap(ch chan<- Event, events ...EventCode) {
|
|
c.eventListenersLock.Lock()
|
|
defer c.eventListenersLock.Unlock()
|
|
for _, event := range events {
|
|
// Must completely replace the array, never mutate it
|
|
prevArr := c.eventListeners[event]
|
|
newArr := make([]chan<- Event, len(prevArr)+1)
|
|
copy(newArr, prevArr)
|
|
newArr[len(newArr)-1] = ch
|
|
c.eventListeners[event] = newArr
|
|
}
|
|
}
|
|
|
|
func (c *Conn) removeEventListenerFromMap(ch chan<- Event, events ...EventCode) {
|
|
c.eventListenersLock.Lock()
|
|
defer c.eventListenersLock.Unlock()
|
|
for _, event := range events {
|
|
arr := c.eventListeners[event]
|
|
index := -1
|
|
for i, listener := range arr {
|
|
if listener == ch {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
if index != -1 {
|
|
if len(arr) == 1 {
|
|
delete(c.eventListeners, event)
|
|
} else {
|
|
// Must completely replace the array, never mutate it
|
|
newArr := make([]chan<- Event, len(arr)-1)
|
|
copy(newArr, arr[:index])
|
|
copy(newArr[index:], arr[index+1:])
|
|
c.eventListeners[event] = newArr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Conn) sendSetEvents() error {
|
|
c.eventListenersLock.RLock()
|
|
cmd := "SETEVENTS"
|
|
for event := range c.eventListeners {
|
|
cmd += " " + string(event)
|
|
}
|
|
c.eventListenersLock.RUnlock()
|
|
return c.sendRequestIgnoreResponse(cmd)
|
|
}
|
|
|
|
func (c *Conn) relayAsyncEvents(resp *Response) {
|
|
var code, data string
|
|
var dataArray []string
|
|
if len(resp.Data) == 1 {
|
|
// On single line, part up to space, newline, or EOL is the code, rest is data
|
|
if index := strings.Index(resp.Data[0], " "); index != -1 {
|
|
code, data = resp.Data[0][:index], resp.Data[0][index+1:]
|
|
} else if index := strings.Index(resp.Data[0], "\r\n"); index != -1 {
|
|
code, data = resp.Data[0][:index], resp.Data[0][index+2:]
|
|
} else {
|
|
code, data = resp.Data[0], ""
|
|
}
|
|
} else if len(resp.Data) > 0 {
|
|
// If there are multiple lines, the entire first line is the code
|
|
code, dataArray = resp.Data[0], resp.Data[1:]
|
|
} else {
|
|
// Otherwise, the reply line has the data
|
|
code, data, _ = torutil.PartitionString(resp.Reply, ' ')
|
|
}
|
|
// Only relay if there are chans
|
|
eventCode := EventCode(code)
|
|
c.eventListenersLock.RLock()
|
|
chans := c.eventListeners[eventCode]
|
|
if _, ok := recognizedEventCodesByCode[eventCode]; !ok {
|
|
chans = append(chans, c.eventListeners[EventCodeUnrecognized]...)
|
|
}
|
|
c.eventListenersLock.RUnlock()
|
|
if len(chans) == 0 {
|
|
return
|
|
}
|
|
// Parse the event and only send if known event
|
|
if event := ParseEvent(eventCode, data, dataArray); event != nil {
|
|
for _, ch := range chans {
|
|
// Just send, if closed or blocking, that's not our problem
|
|
ch <- event
|
|
}
|
|
}
|
|
}
|
|
|
|
// Zero on fail
|
|
func parseISOTime(str string) time.Time {
|
|
// Essentially time.RFC3339 but without 'T' or TZ info
|
|
const layout = "2006-01-02 15:04:05"
|
|
ret, err := time.Parse(layout, str)
|
|
if err != nil {
|
|
ret = time.Time{}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Zero on fail
|
|
func parseISOTime2Frac(str string) time.Time {
|
|
// Essentially time.RFC3339Nano but without TZ info
|
|
const layout = "2006-01-02T15:04:05.999999999"
|
|
ret, err := time.Parse(layout, str)
|
|
if err != nil {
|
|
ret = time.Time{}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Event is the base interface for all known asynchronous events.
|
|
type Event interface {
|
|
Code() EventCode
|
|
}
|
|
|
|
// ParseEvent returns an Event for the given code and data info. Raw is the raw
|
|
// single line if it is a single-line event (even if it has newlines), dataArray
|
|
// is the array of lines for multi-line events. Only one of the two needs to be
|
|
// set. The response is never nil, but may be UnrecognizedEvent. Format errors
|
|
// are ignored per the Tor spec.
|
|
func ParseEvent(code EventCode, raw string, dataArray []string) Event {
|
|
switch code {
|
|
case EventCodeAddrMap:
|
|
return ParseAddrMapEvent(raw)
|
|
case EventCodeBandwidth:
|
|
return ParseBandwidthEvent(raw)
|
|
case EventCodeBuildTimeoutSet:
|
|
return ParseBuildTimeoutSetEvent(raw)
|
|
case EventCodeCellStats:
|
|
return ParseCellStatsEvent(raw)
|
|
case EventCodeCircuit:
|
|
return ParseCircuitEvent(raw)
|
|
case EventCodeCircuitBandwidth:
|
|
return ParseCircuitBandwidthEvent(raw)
|
|
case EventCodeCircuitMinor:
|
|
return ParseCircuitMinorEvent(raw)
|
|
case EventCodeClientsSeen:
|
|
return ParseClientsSeenEvent(raw)
|
|
case EventCodeConfChanged:
|
|
return ParseConfChangedEvent(dataArray)
|
|
case EventCodeConnBandwidth:
|
|
return ParseConnBandwidthEvent(raw)
|
|
case EventCodeDescChanged:
|
|
return ParseDescChangedEvent(raw)
|
|
case EventCodeGuard:
|
|
return ParseGuardEvent(raw)
|
|
case EventCodeHSDesc:
|
|
return ParseHSDescEvent(raw)
|
|
case EventCodeHSDescContent:
|
|
return ParseHSDescContentEvent(raw)
|
|
case EventCodeLogDebug, EventCodeLogErr, EventCodeLogInfo, EventCodeLogNotice, EventCodeLogWarn:
|
|
return ParseLogEvent(code, raw)
|
|
case EventCodeNetworkLiveness:
|
|
return ParseNetworkLivenessEvent(raw)
|
|
case EventCodeNetworkStatus:
|
|
return ParseNetworkStatusEvent(raw)
|
|
case EventCodeNewConsensus:
|
|
return ParseNewConsensusEvent(raw)
|
|
case EventCodeNewDesc:
|
|
return ParseNewDescEvent(raw)
|
|
case EventCodeORConn:
|
|
return ParseORConnEvent(raw)
|
|
case EventCodeSignal:
|
|
return ParseSignalEvent(raw)
|
|
case EventCodeStatusClient, EventCodeStatusGeneral, EventCodeStatusServer:
|
|
return ParseStatusEvent(code, raw)
|
|
case EventCodeStream:
|
|
return ParseStreamEvent(raw)
|
|
case EventCodeStreamBandwidth:
|
|
return ParseStreamBandwidthEvent(raw)
|
|
case EventCodeTokenBucketEmpty:
|
|
return ParseTokenBucketEmptyEvent(raw)
|
|
case EventCodeTransportLaunched:
|
|
return ParseTransportLaunchedEvent(raw)
|
|
default:
|
|
return ParseUnrecognizedEvent(code, raw, dataArray)
|
|
}
|
|
}
|
|
|
|
// CircuitEvent is CIRC in spec.
|
|
type CircuitEvent struct {
|
|
Raw string
|
|
CircuitID string
|
|
Status string
|
|
Path []string
|
|
BuildFlags []string
|
|
Purpose string
|
|
HSState string
|
|
RendQuery string
|
|
TimeCreated time.Time
|
|
Reason string
|
|
RemoteReason string
|
|
SocksUsername string
|
|
SocksPassword string
|
|
}
|
|
|
|
// ParseCircuitEvent parses the event.
|
|
func ParseCircuitEvent(raw string) *CircuitEvent {
|
|
event := &CircuitEvent{Raw: raw}
|
|
event.CircuitID, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var ok bool
|
|
event.Status, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
first := true
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "BUILD_FLAGS":
|
|
event.BuildFlags = strings.Split(val, ",")
|
|
case "PURPOSE":
|
|
event.Purpose = val
|
|
case "HS_STATE":
|
|
event.HSState = val
|
|
case "REND_QUERY":
|
|
event.RendQuery = val
|
|
case "TIME_CREATED":
|
|
event.TimeCreated = parseISOTime2Frac(val)
|
|
case "REASON":
|
|
event.Reason = val
|
|
case "REMOTE_REASON":
|
|
event.RemoteReason = val
|
|
case "SOCKS_USERNAME":
|
|
event.SocksUsername = val
|
|
case "SOCKS_PASSWORD":
|
|
event.SocksPassword = val
|
|
default:
|
|
if first {
|
|
event.Path = strings.Split(val, ",")
|
|
}
|
|
}
|
|
first = false
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*CircuitEvent) Code() EventCode { return EventCodeCircuit }
|
|
|
|
// StreamEvent is STREAM in spec.
|
|
type StreamEvent struct {
|
|
Raw string
|
|
StreamID string
|
|
Status string
|
|
CircuitID string
|
|
TargetAddress string
|
|
TargetPort int
|
|
Reason string
|
|
RemoteReason string
|
|
Source string
|
|
SourceAddress string
|
|
SourcePort int
|
|
Purpose string
|
|
}
|
|
|
|
// ParseStreamEvent parses the event.
|
|
func ParseStreamEvent(raw string) *StreamEvent {
|
|
event := &StreamEvent{Raw: raw}
|
|
event.StreamID, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.Status, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.CircuitID, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var ok bool
|
|
event.TargetAddress, raw, ok = torutil.PartitionString(raw, ' ')
|
|
if target, port, hasPort := torutil.PartitionStringFromEnd(event.TargetAddress, ':'); hasPort {
|
|
event.TargetAddress = target
|
|
event.TargetPort, _ = strconv.Atoi(port)
|
|
}
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "REASON":
|
|
event.Reason = val
|
|
case "REMOTE_REASON":
|
|
event.RemoteReason = val
|
|
case "SOURCE":
|
|
event.Source = val
|
|
case "SOURCE_ADDR":
|
|
event.SourceAddress = val
|
|
if source, port, hasPort := torutil.PartitionStringFromEnd(event.SourceAddress, ':'); hasPort {
|
|
event.SourceAddress = source
|
|
event.SourcePort, _ = strconv.Atoi(port)
|
|
}
|
|
case "PURPOSE":
|
|
event.Purpose = val
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*StreamEvent) Code() EventCode { return EventCodeStream }
|
|
|
|
// ORConnEvent is ORCONN in spec.
|
|
type ORConnEvent struct {
|
|
Raw string
|
|
Target string
|
|
Status string
|
|
Reason string
|
|
NumCircuits int
|
|
ConnID string
|
|
}
|
|
|
|
// ParseORConnEvent parses the event.
|
|
func ParseORConnEvent(raw string) *ORConnEvent {
|
|
event := &ORConnEvent{Raw: raw}
|
|
event.Target, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var ok bool
|
|
event.Status, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "REASON":
|
|
event.Reason = val
|
|
case "NCIRCS":
|
|
event.NumCircuits, _ = strconv.Atoi(val)
|
|
case "ID":
|
|
event.ConnID = val
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*ORConnEvent) Code() EventCode { return EventCodeORConn }
|
|
|
|
// BandwidthEvent is BW in spec.
|
|
type BandwidthEvent struct {
|
|
Raw string
|
|
BytesRead int64
|
|
BytesWritten int64
|
|
}
|
|
|
|
// ParseBandwidthEvent parses the event.
|
|
func ParseBandwidthEvent(raw string) *BandwidthEvent {
|
|
event := &BandwidthEvent{Raw: raw}
|
|
var temp string
|
|
temp, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.BytesRead, _ = strconv.ParseInt(temp, 10, 64)
|
|
temp, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.BytesWritten, _ = strconv.ParseInt(temp, 10, 64)
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*BandwidthEvent) Code() EventCode { return EventCodeBandwidth }
|
|
|
|
// LogEvent is DEBUG, ERR, INFO, NOTICE, and WARN in spec.
|
|
type LogEvent struct {
|
|
Severity EventCode
|
|
Raw string
|
|
}
|
|
|
|
// ParseLogEvent parses the event.
|
|
func ParseLogEvent(severity EventCode, raw string) *LogEvent {
|
|
return &LogEvent{Severity: severity, Raw: raw}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (l *LogEvent) Code() EventCode { return l.Severity }
|
|
|
|
// NewDescEvent is NEWDESC in spec.
|
|
type NewDescEvent struct {
|
|
Raw string
|
|
Descs []string
|
|
}
|
|
|
|
// ParseNewDescEvent parses the event.
|
|
func ParseNewDescEvent(raw string) *NewDescEvent {
|
|
return &NewDescEvent{Raw: raw, Descs: strings.Split(raw, " ")}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*NewDescEvent) Code() EventCode { return EventCodeNewDesc }
|
|
|
|
// AddrMapEvent is ADDRMAP in spec.
|
|
type AddrMapEvent struct {
|
|
Raw string
|
|
Address string
|
|
NewAddress string
|
|
ErrorCode string
|
|
// Zero if no expire
|
|
Expires time.Time
|
|
// Sans double quotes
|
|
Cached string
|
|
}
|
|
|
|
// ParseAddrMapEvent parses the event.
|
|
func ParseAddrMapEvent(raw string) *AddrMapEvent {
|
|
event := &AddrMapEvent{Raw: raw}
|
|
event.Address, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.NewAddress, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var ok bool
|
|
// Skip local expiration, use UTC one later
|
|
_, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "error":
|
|
event.ErrorCode = val
|
|
case "EXPIRES":
|
|
val, _ = torutil.UnescapeSimpleQuotedString(val)
|
|
event.Expires = parseISOTime(val)
|
|
case "CACHED":
|
|
event.Cached, _ = torutil.UnescapeSimpleQuotedStringIfNeeded(val)
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*AddrMapEvent) Code() EventCode { return EventCodeAddrMap }
|
|
|
|
// DescChangedEvent is DESCCHANGED in spec.
|
|
type DescChangedEvent struct {
|
|
Raw string
|
|
}
|
|
|
|
// ParseDescChangedEvent parses the event.
|
|
func ParseDescChangedEvent(raw string) *DescChangedEvent {
|
|
return &DescChangedEvent{Raw: raw}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*DescChangedEvent) Code() EventCode { return EventCodeDescChanged }
|
|
|
|
// StatusEvent is STATUS_CLIENT, STATUS_GENERAL, and STATUS_SERVER in spec.
|
|
type StatusEvent struct {
|
|
Raw string
|
|
Type EventCode
|
|
Severity string
|
|
Action string
|
|
Arguments map[string]string
|
|
}
|
|
|
|
// ParseStatusEvent parses the event.
|
|
func ParseStatusEvent(typ EventCode, raw string) *StatusEvent {
|
|
event := &StatusEvent{Raw: raw, Type: typ, Arguments: map[string]string{}}
|
|
event.Severity, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var ok bool
|
|
event.Action, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
event.Arguments[key], _ = torutil.UnescapeSimpleQuotedStringIfNeeded(val)
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (s *StatusEvent) Code() EventCode { return s.Type }
|
|
|
|
// GuardEvent is GUARD in spec.
|
|
type GuardEvent struct {
|
|
Raw string
|
|
Type string
|
|
Name string
|
|
Status string
|
|
}
|
|
|
|
// ParseGuardEvent parses the event.
|
|
func ParseGuardEvent(raw string) *GuardEvent {
|
|
event := &GuardEvent{Raw: raw}
|
|
event.Type, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.Name, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.Status, raw, _ = torutil.PartitionString(raw, ' ')
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*GuardEvent) Code() EventCode { return EventCodeGuard }
|
|
|
|
// NetworkStatusEvent is NS in spec.
|
|
type NetworkStatusEvent struct {
|
|
Raw string
|
|
}
|
|
|
|
// ParseNetworkStatusEvent parses the event.
|
|
func ParseNetworkStatusEvent(raw string) *NetworkStatusEvent {
|
|
return &NetworkStatusEvent{Raw: raw}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*NetworkStatusEvent) Code() EventCode { return EventCodeNetworkStatus }
|
|
|
|
// StreamBandwidthEvent is STREAM_BW in spec.
|
|
type StreamBandwidthEvent struct {
|
|
Raw string
|
|
BytesRead int64
|
|
BytesWritten int64
|
|
Time time.Time
|
|
}
|
|
|
|
// ParseStreamBandwidthEvent parses the event.
|
|
func ParseStreamBandwidthEvent(raw string) *StreamBandwidthEvent {
|
|
event := &StreamBandwidthEvent{Raw: raw}
|
|
var temp string
|
|
temp, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.BytesRead, _ = strconv.ParseInt(temp, 10, 64)
|
|
temp, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.BytesWritten, _ = strconv.ParseInt(temp, 10, 64)
|
|
temp, raw, _ = torutil.PartitionString(raw, ' ')
|
|
temp, _ = torutil.UnescapeSimpleQuotedString(temp)
|
|
event.Time = parseISOTime2Frac(temp)
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*StreamBandwidthEvent) Code() EventCode { return EventCodeStreamBandwidth }
|
|
|
|
// ClientsSeenEvent is CLIENTS_SEEN in spec.
|
|
type ClientsSeenEvent struct {
|
|
Raw string
|
|
TimeStarted time.Time
|
|
CountrySummary map[string]int
|
|
IPVersions map[string]int
|
|
}
|
|
|
|
// ParseClientsSeenEvent parses the event.
|
|
func ParseClientsSeenEvent(raw string) *ClientsSeenEvent {
|
|
event := &ClientsSeenEvent{Raw: raw}
|
|
var temp string
|
|
var ok bool
|
|
temp, raw, ok = torutil.PartitionString(raw, ' ')
|
|
temp, _ = torutil.UnescapeSimpleQuotedString(temp)
|
|
event.TimeStarted = parseISOTime(temp)
|
|
strToMap := func(str string) map[string]int {
|
|
ret := map[string]int{}
|
|
for _, keyVal := range strings.Split(str, ",") {
|
|
key, val, _ := torutil.PartitionString(keyVal, '=')
|
|
ret[key], _ = strconv.Atoi(val)
|
|
}
|
|
return ret
|
|
}
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "CountrySummary":
|
|
event.CountrySummary = strToMap(val)
|
|
case "IPVersions":
|
|
event.IPVersions = strToMap(val)
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*ClientsSeenEvent) Code() EventCode { return EventCodeClientsSeen }
|
|
|
|
// NewConsensusEvent is NEWCONSENSUS in spec.
|
|
type NewConsensusEvent struct {
|
|
Raw string
|
|
}
|
|
|
|
// ParseNewConsensusEvent parses the event.
|
|
func ParseNewConsensusEvent(raw string) *NewConsensusEvent {
|
|
return &NewConsensusEvent{Raw: raw}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*NewConsensusEvent) Code() EventCode { return EventCodeNewConsensus }
|
|
|
|
// BuildTimeoutSetEvent is BUILDTIMEOUT_SET in spec.
|
|
type BuildTimeoutSetEvent struct {
|
|
Raw string
|
|
Type string
|
|
TotalTimes int
|
|
Timeout time.Duration
|
|
Xm int
|
|
Alpha float32
|
|
Quantile float32
|
|
TimeoutRate float32
|
|
CloseTimeout time.Duration
|
|
CloseRate float32
|
|
}
|
|
|
|
// ParseBuildTimeoutSetEvent parses the event.
|
|
func ParseBuildTimeoutSetEvent(raw string) *BuildTimeoutSetEvent {
|
|
event := &BuildTimeoutSetEvent{Raw: raw}
|
|
var ok bool
|
|
event.Type, raw, ok = torutil.PartitionString(raw, ' ')
|
|
_, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
parseFloat := func(val string) float32 {
|
|
f, _ := strconv.ParseFloat(val, 32)
|
|
return float32(f)
|
|
}
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "TOTAL_TIMES":
|
|
event.TotalTimes, _ = strconv.Atoi(val)
|
|
case "TIMEOUT_MS":
|
|
if ms, err := strconv.ParseInt(val, 10, 64); err == nil {
|
|
event.Timeout = time.Duration(ms) * time.Millisecond
|
|
}
|
|
case "XM":
|
|
event.Xm, _ = strconv.Atoi(val)
|
|
case "ALPHA":
|
|
event.Alpha = parseFloat(val)
|
|
case "CUTOFF_QUANTILE":
|
|
event.Quantile = parseFloat(val)
|
|
case "TIMEOUT_RATE":
|
|
event.TimeoutRate = parseFloat(val)
|
|
case "CLOSE_MS":
|
|
if ms, err := strconv.ParseInt(val, 10, 64); err == nil {
|
|
event.CloseTimeout = time.Duration(ms) * time.Millisecond
|
|
}
|
|
case "CLOSE_RATE":
|
|
event.CloseRate = parseFloat(val)
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*BuildTimeoutSetEvent) Code() EventCode { return EventCodeBuildTimeoutSet }
|
|
|
|
// SignalEvent is SIGNAL in spec.
|
|
type SignalEvent struct {
|
|
Raw string
|
|
}
|
|
|
|
// ParseSignalEvent parses the event.
|
|
func ParseSignalEvent(raw string) *SignalEvent {
|
|
return &SignalEvent{Raw: raw}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*SignalEvent) Code() EventCode { return EventCodeSignal }
|
|
|
|
// ConfChangedEvent is CONF_CHANGED in spec.
|
|
type ConfChangedEvent struct {
|
|
Raw []string
|
|
}
|
|
|
|
// ParseConfChangedEvent parses the event.
|
|
func ParseConfChangedEvent(raw []string) *ConfChangedEvent {
|
|
// TODO: break into KeyVal and unescape strings
|
|
return &ConfChangedEvent{Raw: raw}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*ConfChangedEvent) Code() EventCode { return EventCodeConfChanged }
|
|
|
|
// CircuitMinorEvent is CIRC_MINOR in spec.
|
|
type CircuitMinorEvent struct {
|
|
Raw string
|
|
CircuitID string
|
|
Event string
|
|
Path []string
|
|
BuildFlags []string
|
|
Purpose string
|
|
HSState string
|
|
RendQuery string
|
|
TimeCreated time.Time
|
|
OldPurpose string
|
|
OldHSState string
|
|
}
|
|
|
|
// ParseCircuitMinorEvent parses the event.
|
|
func ParseCircuitMinorEvent(raw string) *CircuitMinorEvent {
|
|
event := &CircuitMinorEvent{Raw: raw}
|
|
event.CircuitID, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var ok bool
|
|
event.Event, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
first := true
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "BUILD_FLAGS":
|
|
event.BuildFlags = strings.Split(val, ",")
|
|
case "PURPOSE":
|
|
event.Purpose = val
|
|
case "HS_STATE":
|
|
event.HSState = val
|
|
case "REND_QUERY":
|
|
event.RendQuery = val
|
|
case "TIME_CREATED":
|
|
event.TimeCreated = parseISOTime2Frac(val)
|
|
case "OLD_PURPOSE":
|
|
event.OldPurpose = val
|
|
case "OLD_HS_STATE":
|
|
event.OldHSState = val
|
|
default:
|
|
if first {
|
|
event.Path = strings.Split(val, ",")
|
|
}
|
|
}
|
|
first = false
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*CircuitMinorEvent) Code() EventCode { return EventCodeCircuitMinor }
|
|
|
|
// TransportLaunchedEvent is TRANSPORT_LAUNCHED in spec.
|
|
type TransportLaunchedEvent struct {
|
|
Raw string
|
|
Type string
|
|
Name string
|
|
Address string
|
|
Port int
|
|
}
|
|
|
|
// ParseTransportLaunchedEvent parses the event.
|
|
func ParseTransportLaunchedEvent(raw string) *TransportLaunchedEvent {
|
|
event := &TransportLaunchedEvent{Raw: raw}
|
|
event.Type, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.Name, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.Address, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var temp string
|
|
temp, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.Port, _ = strconv.Atoi(temp)
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*TransportLaunchedEvent) Code() EventCode { return EventCodeTransportLaunched }
|
|
|
|
// ConnBandwidthEvent is CONN_BW in spec.
|
|
type ConnBandwidthEvent struct {
|
|
Raw string
|
|
ConnID string
|
|
ConnType string
|
|
BytesRead int64
|
|
BytesWritten int64
|
|
}
|
|
|
|
// ParseConnBandwidthEvent parses the event.
|
|
func ParseConnBandwidthEvent(raw string) *ConnBandwidthEvent {
|
|
event := &ConnBandwidthEvent{Raw: raw}
|
|
ok := true
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "ID":
|
|
event.ConnID = val
|
|
case "TYPE":
|
|
event.ConnType = val
|
|
case "READ":
|
|
event.BytesRead, _ = strconv.ParseInt(val, 10, 64)
|
|
case "WRITTEN":
|
|
event.BytesWritten, _ = strconv.ParseInt(val, 10, 64)
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*ConnBandwidthEvent) Code() EventCode { return EventCodeConnBandwidth }
|
|
|
|
// CircuitBandwidthEvent is CIRC_BW in spec.
|
|
type CircuitBandwidthEvent struct {
|
|
Raw string
|
|
CircuitID string
|
|
BytesRead int64
|
|
BytesWritten int64
|
|
Time time.Time
|
|
}
|
|
|
|
// ParseCircuitBandwidthEvent parses the event.
|
|
func ParseCircuitBandwidthEvent(raw string) *CircuitBandwidthEvent {
|
|
event := &CircuitBandwidthEvent{Raw: raw}
|
|
ok := true
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "ID":
|
|
event.CircuitID = val
|
|
case "READ":
|
|
event.BytesRead, _ = strconv.ParseInt(val, 10, 64)
|
|
case "WRITTEN":
|
|
event.BytesWritten, _ = strconv.ParseInt(val, 10, 64)
|
|
case "TIME":
|
|
event.Time = parseISOTime2Frac(val)
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*CircuitBandwidthEvent) Code() EventCode { return EventCodeCircuitBandwidth }
|
|
|
|
// CellStatsEvent is CELL_STATS in spec.
|
|
type CellStatsEvent struct {
|
|
Raw string
|
|
CircuitID string
|
|
InboundQueueID string
|
|
InboundConnID string
|
|
InboundAdded map[string]int
|
|
InboundRemoved map[string]int
|
|
InboundTime map[string]int
|
|
OutboundQueueID string
|
|
OutboundConnID string
|
|
OutboundAdded map[string]int
|
|
OutboundRemoved map[string]int
|
|
OutboundTime map[string]int
|
|
}
|
|
|
|
// ParseCellStatsEvent parses the event.
|
|
func ParseCellStatsEvent(raw string) *CellStatsEvent {
|
|
event := &CellStatsEvent{Raw: raw}
|
|
ok := true
|
|
var attr string
|
|
toIntMap := func(val string) map[string]int {
|
|
ret := map[string]int{}
|
|
for _, v := range strings.Split(val, ",") {
|
|
key, val, _ := torutil.PartitionString(v, ':')
|
|
ret[key], _ = strconv.Atoi(val)
|
|
}
|
|
return ret
|
|
}
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "ID":
|
|
event.CircuitID = val
|
|
case "InboundQueue":
|
|
event.InboundQueueID = val
|
|
case "InboundConn":
|
|
event.InboundConnID = val
|
|
case "InboundAdded":
|
|
event.InboundAdded = toIntMap(val)
|
|
case "InboundRemoved":
|
|
event.InboundRemoved = toIntMap(val)
|
|
case "InboundTime":
|
|
event.OutboundTime = toIntMap(val)
|
|
case "OutboundQueue":
|
|
event.OutboundQueueID = val
|
|
case "OutboundConn":
|
|
event.OutboundConnID = val
|
|
case "OutboundAdded":
|
|
event.OutboundAdded = toIntMap(val)
|
|
case "OutboundRemoved":
|
|
event.OutboundRemoved = toIntMap(val)
|
|
case "OutboundTime":
|
|
event.OutboundTime = toIntMap(val)
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*CellStatsEvent) Code() EventCode { return EventCodeCellStats }
|
|
|
|
// TokenBucketEmptyEvent is TB_EMPTY in spec.
|
|
type TokenBucketEmptyEvent struct {
|
|
Raw string
|
|
BucketName string
|
|
ConnID string
|
|
ReadBucketEmpty time.Duration
|
|
WriteBucketEmpty time.Duration
|
|
LastRefil time.Duration
|
|
}
|
|
|
|
// ParseTokenBucketEmptyEvent parses the event.
|
|
func ParseTokenBucketEmptyEvent(raw string) *TokenBucketEmptyEvent {
|
|
event := &TokenBucketEmptyEvent{Raw: raw}
|
|
var ok bool
|
|
event.BucketName, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, _ := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "ID":
|
|
event.ConnID = val
|
|
case "READ":
|
|
i, _ := strconv.ParseInt(val, 10, 64)
|
|
event.ReadBucketEmpty = time.Duration(i) * time.Millisecond
|
|
case "WRITTEN":
|
|
i, _ := strconv.ParseInt(val, 10, 64)
|
|
event.WriteBucketEmpty = time.Duration(i) * time.Millisecond
|
|
case "LAST":
|
|
i, _ := strconv.ParseInt(val, 10, 64)
|
|
event.LastRefil = time.Duration(i) * time.Millisecond
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*TokenBucketEmptyEvent) Code() EventCode { return EventCodeTokenBucketEmpty }
|
|
|
|
// HSDescEvent is HS_DESC in spec.
|
|
type HSDescEvent struct {
|
|
Raw string
|
|
Action string
|
|
Address string
|
|
AuthType string
|
|
HSDir string
|
|
DescID string
|
|
Reason string
|
|
Replica int
|
|
HSDirIndex string
|
|
}
|
|
|
|
// ParseHSDescEvent parses the event.
|
|
func ParseHSDescEvent(raw string) *HSDescEvent {
|
|
event := &HSDescEvent{Raw: raw}
|
|
event.Action, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.Address, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.AuthType, raw, _ = torutil.PartitionString(raw, ' ')
|
|
var ok bool
|
|
event.HSDir, raw, ok = torutil.PartitionString(raw, ' ')
|
|
var attr string
|
|
first := true
|
|
for ok {
|
|
attr, raw, ok = torutil.PartitionString(raw, ' ')
|
|
key, val, valOk := torutil.PartitionString(attr, '=')
|
|
switch key {
|
|
case "REASON":
|
|
event.Reason = val
|
|
case "REPLICA":
|
|
event.Replica, _ = strconv.Atoi(val)
|
|
case "HSDIR_INDEX":
|
|
event.HSDirIndex = val
|
|
default:
|
|
if first && !valOk {
|
|
event.DescID = attr
|
|
}
|
|
}
|
|
first = false
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*HSDescEvent) Code() EventCode { return EventCodeHSDesc }
|
|
|
|
// HSDescContentEvent is HS_DESC_CONTENT in spec.
|
|
type HSDescContentEvent struct {
|
|
Raw string
|
|
Address string
|
|
DescID string
|
|
HSDir string
|
|
Descriptor string
|
|
}
|
|
|
|
// ParseHSDescContentEvent parses the event.
|
|
func ParseHSDescContentEvent(raw string) *HSDescContentEvent {
|
|
event := &HSDescContentEvent{Raw: raw}
|
|
event.Address, raw, _ = torutil.PartitionString(raw, ' ')
|
|
event.DescID, raw, _ = torutil.PartitionString(raw, ' ')
|
|
newlineIndex := strings.Index(raw, "\r\n")
|
|
if newlineIndex != -1 {
|
|
event.HSDir, event.Descriptor = raw[:newlineIndex], raw[newlineIndex+2:]
|
|
}
|
|
return event
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*HSDescContentEvent) Code() EventCode { return EventCodeHSDescContent }
|
|
|
|
// NetworkLivenessEvent is NETWORK_LIVENESS in spec.
|
|
type NetworkLivenessEvent struct {
|
|
Raw string
|
|
}
|
|
|
|
// ParseNetworkLivenessEvent parses the event.
|
|
func ParseNetworkLivenessEvent(raw string) *NetworkLivenessEvent {
|
|
return &NetworkLivenessEvent{Raw: raw}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (*NetworkLivenessEvent) Code() EventCode { return EventCodeNetworkLiveness }
|
|
|
|
// UnrecognizedEvent is any unrecognized event code.
|
|
type UnrecognizedEvent struct {
|
|
EventCode EventCode
|
|
RawSingleLine string
|
|
RawMultiLine []string
|
|
}
|
|
|
|
// ParseUnrecognizedEvent creates an unrecognized event with the given values.
|
|
func ParseUnrecognizedEvent(eventCode EventCode, rawSingleLine string, rawMultiLine []string) *UnrecognizedEvent {
|
|
return &UnrecognizedEvent{EventCode: eventCode, RawSingleLine: rawSingleLine, RawMultiLine: rawMultiLine}
|
|
}
|
|
|
|
// Code implements Event.Code
|
|
func (u *UnrecognizedEvent) Code() EventCode { return u.EventCode }
|