diff --git a/control/cmd_conf.go b/control/cmd_conf.go index 39cda49..bb5080a 100644 --- a/control/cmd_conf.go +++ b/control/cmd_conf.go @@ -6,48 +6,41 @@ import ( "github.com/cretz/bine/util" ) -type ConfEntry struct { - Key string - Value *string -} - -func NewConfEntry(key string, value *string) *ConfEntry { - return &ConfEntry{Key: key, Value: value} -} - -func (c *Conn) SetConf(entries ...*ConfEntry) error { +func (c *Conn) SetConf(entries ...*KeyVal) error { return c.sendSetConf("SETCONF", entries) } -func (c *Conn) ResetConf(entries ...*ConfEntry) error { +func (c *Conn) ResetConf(entries ...*KeyVal) error { return c.sendSetConf("RESETCONF", entries) } -func (c *Conn) sendSetConf(cmd string, entries []*ConfEntry) error { +func (c *Conn) sendSetConf(cmd string, entries []*KeyVal) error { for _, entry := range entries { cmd += " " + entry.Key - if entry.Value != nil { - cmd += "=" + util.EscapeSimpleQuotedStringIfNeeded(*entry.Value) + if entry.ValSet() { + cmd += "=" + util.EscapeSimpleQuotedStringIfNeeded(entry.Val) } } return c.sendRequestIgnoreResponse(cmd) } -func (c *Conn) GetConf(keys ...string) ([]*ConfEntry, error) { +func (c *Conn) GetConf(keys ...string) ([]*KeyVal, error) { resp, err := c.SendRequest("GETCONF %v", strings.Join(keys, " ")) if err != nil { return nil, err } data := resp.DataWithReply() - ret := make([]*ConfEntry, 0, len(data)) + ret := make([]*KeyVal, 0, len(data)) for _, data := range data { key, val, ok := util.PartitionString(data, '=') - entry := &ConfEntry{Key: key} + entry := &KeyVal{Key: key} if ok { - if val, err = util.UnescapeSimpleQuotedStringIfNeeded(val); err != nil { + if entry.Val, err = util.UnescapeSimpleQuotedStringIfNeeded(val); err != nil { return nil, err } - entry.Value = &val + if len(entry.Val) == 0 { + entry.ValSetAndEmpty = true + } } ret = append(ret, entry) } diff --git a/control/cmd_event.go b/control/cmd_event.go index 9c3f41c..97e2a7b 100644 --- a/control/cmd_event.go +++ b/control/cmd_event.go @@ -1,6 +1,7 @@ package control import ( + "strconv" "strings" "time" @@ -10,8 +11,21 @@ import ( type EventCode string const ( - EventCodeAddrMap EventCode = "ADDRMAP" - EventCodeCirc EventCode = "CIRC" + EventCodeAddrMap EventCode = "ADDRMAP" + EventCodeBandwidth EventCode = "BW" + EventCodeCircuit EventCode = "CIRC" + EventCodeDescChanged EventCode = "DESCCHANGED" + EventCodeLogDebug EventCode = "DEBUG" + EventCodeLogErr EventCode = "ERR" + EventCodeLogInfo EventCode = "INFO" + EventCodeLogNotice EventCode = "NOTICE" + EventCodeLogWarn EventCode = "WARN" + EventCodeNewDesc EventCode = "NEWDESC" + EventCodeORConn EventCode = "ORCONN" + EventCodeStatusClient EventCode = "STATUS_CLIENT" + EventCodeStatusGeneral EventCode = "STATUS_GENERAL" + EventCodeStatusServer EventCode = "STATUS_SERVER" + EventCodeStream EventCode = "STREAM" ) func (c *Conn) AddEventListener(events []EventCode, ch chan<- Event) error { @@ -67,6 +81,41 @@ func (c *Conn) sendSetEvents() error { return c.sendRequestIgnoreResponse(cmd) } +func (c *Conn) relayAsyncEvents(resp *Response) { + code, data, _ := util.PartitionString(resp.Reply, ' ') + // Only relay if there are chans + c.eventListenersLock.RLock() + chans := c.eventListeners[EventCode(code)] + c.eventListenersLock.RUnlock() + if len(chans) == 0 { + return + } + // Parse the event + // TODO: more events + var event Event + switch EventCode(code) { + case EventCodeCircuit: + event = ParseCircuitEvent(data) + } + if 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 @@ -78,7 +127,12 @@ func parseISOTime2Frac(str string) time.Time { return ret } +type Event interface { + Code() EventCode +} + type CircuitEvent struct { + Raw string CircuitID string Status string Path []string @@ -91,7 +145,6 @@ type CircuitEvent struct { RemoteReason string SocksUsername string SocksPassword string - Raw string } func ParseCircuitEvent(raw string) *CircuitEvent { @@ -102,9 +155,7 @@ func ParseCircuitEvent(raw string) *CircuitEvent { var attr string first := true for ok { - if attr, raw, ok = util.PartitionString(raw, ' '); !ok { - break - } + attr, raw, ok = util.PartitionString(raw, ' ') key, val, _ := util.PartitionString(attr, '=') switch key { case "BUILD_FLAGS": @@ -135,32 +186,198 @@ func ParseCircuitEvent(raw string) *CircuitEvent { return event } -type Event interface { - Code() EventCode +func (*CircuitEvent) Code() EventCode { return EventCodeCircuit } + +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 } -func (*CircuitEvent) Code() EventCode { return EventCodeCirc } - -func (c *Conn) relayAsyncEvents(resp *Response) { - code, data, _ := util.PartitionString(resp.Reply, ' ') - // Only relay if there are chans - c.eventListenersLock.RLock() - chans := c.eventListeners[EventCode(code)] - c.eventListenersLock.RUnlock() - if len(chans) == 0 { - return +func ParseStreamEvent(raw string) *StreamEvent { + event := &StreamEvent{Raw: raw} + event.StreamID, raw, _ = util.PartitionString(raw, ' ') + event.Status, raw, _ = util.PartitionString(raw, ' ') + event.CircuitID, raw, _ = util.PartitionString(raw, ' ') + var ok bool + event.TargetAddress, raw, ok = util.PartitionString(raw, ' ') + if target, port, hasPort := util.PartitionStringFromEnd(event.TargetAddress, ':'); hasPort { + event.TargetAddress = target + event.TargetPort, _ = strconv.Atoi(port) } - // Parse the event - // TODO: more events - var event Event - switch EventCode(code) { - case EventCodeCirc: - event = ParseCircuitEvent(data) - } - if event != nil { - for _, ch := range chans { - // Just send, if closed or blocking, that's not our problem - ch <- event + var attr string + for ok { + attr, raw, ok = util.PartitionString(raw, ' ') + key, val, _ := util.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 := util.PartitionStringFromEnd(event.SourceAddress, ':'); hasPort { + event.SourceAddress = source + event.SourcePort, _ = strconv.Atoi(port) + } + case "PURPOSE": + event.Purpose = val } } + return event } + +func (*StreamEvent) Code() EventCode { return EventCodeStream } + +type ORConnEvent struct { + Raw string + Target string + Status string + Reason string + NumCircuits int + ConnID string +} + +func ParseORConnEvent(raw string) *ORConnEvent { + event := &ORConnEvent{Raw: raw} + event.Target, raw, _ = util.PartitionString(raw, ' ') + var ok bool + event.Status, raw, ok = util.PartitionString(raw, ' ') + var attr string + for ok { + attr, raw, ok = util.PartitionString(raw, ' ') + key, val, _ := util.PartitionString(attr, '=') + switch key { + case "REASON": + event.Reason = val + case "NCIRCS": + event.NumCircuits, _ = strconv.Atoi(val) + case "ID": + event.ConnID = val + } + } + return event +} + +func (*ORConnEvent) Code() EventCode { return EventCodeORConn } + +type BandwidthEvent struct { + Raw string + BytesRead int64 + BytesWritten int64 +} + +func ParseBandwidthEvent(raw string) *BandwidthEvent { + event := &BandwidthEvent{Raw: raw} + var temp string + temp, raw, _ = util.PartitionString(raw, ' ') + event.BytesRead, _ = strconv.ParseInt(temp, 10, 64) + temp, raw, _ = util.PartitionString(raw, ' ') + event.BytesWritten, _ = strconv.ParseInt(temp, 10, 64) + return event +} + +func (*BandwidthEvent) Code() EventCode { return EventCodeBandwidth } + +type LogEvent struct { + Severity EventCode + Raw string +} + +func ParseLogEvent(severity EventCode, raw string) *LogEvent { + return &LogEvent{Severity: severity, Raw: raw} +} + +func (l *LogEvent) Code() EventCode { return l.Severity } + +type NewDescEvent struct { + Raw string + Descs []string +} + +func ParseNewDescEvent(raw string) *NewDescEvent { + return &NewDescEvent{Raw: raw, Descs: strings.Split(raw, " ")} +} + +func (*NewDescEvent) Code() EventCode { return EventCodeNewDesc } + +type AddrMapEvent struct { + Raw string + Address string + NewAddress string + ErrorCode string + // Zero if no expire + Expires time.Time + // Sans double quotes + Cached string +} + +func ParseAddrMapEvent(raw string) *AddrMapEvent { + event := &AddrMapEvent{Raw: raw} + event.Address, raw, _ = util.PartitionString(raw, ' ') + event.NewAddress, raw, _ = util.PartitionString(raw, ' ') + var ok bool + // Skip local expiration, use UTC one later + _, raw, ok = util.PartitionString(raw, ' ') + var attr string + for ok { + attr, raw, ok = util.PartitionString(raw, ' ') + key, val, _ := util.PartitionString(attr, '=') + switch key { + case "error": + event.ErrorCode = val + case "EXPIRES": + event.Expires = parseISOTime(val) + case "CACHED": + event.Cached, _ = util.UnescapeSimpleQuotedStringIfNeeded(val) + } + } + return event +} + +func (*AddrMapEvent) Code() EventCode { return EventCodeAddrMap } + +type DescChangedEvent struct { + Raw string +} + +func ParseDescChangedEvent(raw string) *DescChangedEvent { + return &DescChangedEvent{Raw: raw} +} + +func (*DescChangedEvent) Code() EventCode { return EventCodeDescChanged } + +type StatusEvent struct { + Raw string + Type EventCode + Severity string + Action string + Arguments map[string]string +} + +func ParseStatusEvent(typ EventCode, raw string) *StatusEvent { + event := &StatusEvent{Raw: raw, Type: typ, Arguments: map[string]string{}} + event.Severity, raw, _ = util.PartitionString(raw, ' ') + var ok bool + event.Action, raw, ok = util.PartitionString(raw, ' ') + var attr string + for ok { + attr, raw, ok = util.PartitionString(raw, ' ') + key, val, _ := util.PartitionString(attr, '=') + event.Arguments[key], _ = util.UnescapeSimpleQuotedStringIfNeeded(val) + } + return event +} + +func (s *StatusEvent) Code() EventCode { return s.Type } diff --git a/control/cmd_misc.go b/control/cmd_misc.go index 64a0bf2..73cf3ba 100644 --- a/control/cmd_misc.go +++ b/control/cmd_misc.go @@ -14,48 +14,34 @@ func (c *Conn) Quit() error { return c.sendRequestIgnoreResponse("QUIT") } -type MappedAddress struct { - Old string - New string -} - -func NewMappedAddress(old string, new string) *MappedAddress { - return &MappedAddress{Old: old, New: new} -} - -func (c *Conn) MapAddresses(addresses []*MappedAddress) ([]*MappedAddress, error) { +func (c *Conn) MapAddresses(addresses ...*KeyVal) ([]*KeyVal, error) { cmd := "MAPADDRESS" for _, address := range addresses { - cmd += " " + address.New + "=" + address.Old + cmd += " " + address.Key + "=" + address.Val } resp, err := c.SendRequest(cmd) if err != nil { return nil, err } data := resp.DataWithReply() - ret := make([]*MappedAddress, 0, len(data)) + ret := make([]*KeyVal, 0, len(data)) for _, address := range data { - mappedAddress := &MappedAddress{} - mappedAddress.Old, mappedAddress.New, _ = util.PartitionString(address, '=') + mappedAddress := &KeyVal{} + mappedAddress.Key, mappedAddress.Val, _ = util.PartitionString(address, '=') ret = append(ret, mappedAddress) } return ret, nil } -type InfoValue struct { - Key string - Value string -} - -func (c *Conn) GetInfo(keys ...string) ([]*InfoValue, error) { +func (c *Conn) GetInfo(keys ...string) ([]*KeyVal, error) { resp, err := c.SendRequest("GETINFO %v", strings.Join(keys, " ")) if err != nil { return nil, err } - ret := make([]*InfoValue, 0, len(resp.Data)) + ret := make([]*KeyVal, 0, len(resp.Data)) for _, val := range resp.Data { - infoVal := &InfoValue{} - infoVal.Key, infoVal.Value, _ = util.PartitionString(val, '=') + infoVal := &KeyVal{} + infoVal.Key, infoVal.Val, _ = util.PartitionString(val, '=') ret = append(ret, infoVal) } return ret, nil diff --git a/control/controltest/cmd_authenticate_test.go b/control/controltest/cmd_authenticate_test.go index b962edc..0eb9564 100644 --- a/control/controltest/cmd_authenticate_test.go +++ b/control/controltest/cmd_authenticate_test.go @@ -30,6 +30,6 @@ func TestAuthenticateHashedPassword(t *testing.T) { // Verify auth methods before auth info, err := conn.ProtocolInfo() ctx.Require.NoError(err) - ctx.Require.ElementsMatch([]string{"HASHESPASSWORD"}, info.AuthMethods) + ctx.Require.ElementsMatch([]string{"HASHEDPASSWORD"}, info.AuthMethods) ctx.Require.NoError(conn.Authenticate("testpass")) } diff --git a/control/controltest/cmd_conf_test.go b/control/controltest/cmd_conf_test.go index 619d5d4..395eea1 100644 --- a/control/controltest/cmd_conf_test.go +++ b/control/controltest/cmd_conf_test.go @@ -15,19 +15,18 @@ func TestGetSetAndResetConf(t *testing.T) { entries, err := conn.GetConf("LogMessageDomains", "ProtocolWarnings") ctx.Require.NoError(err) ctx.Require.Len(entries, 2) - ctx.Require.Contains(entries, &control.ConfEntry{Key: "LogMessageDomains", Value: &val}) - ctx.Require.Contains(entries, &control.ConfEntry{Key: "ProtocolWarnings", Value: &val}) + ctx.Require.Contains(entries, control.NewKeyVal("LogMessageDomains", val)) + ctx.Require.Contains(entries, control.NewKeyVal("ProtocolWarnings", val)) } assertConfVals("0") // Change em both to 1 one := "1" - err := conn.SetConf(&control.ConfEntry{Key: "LogMessageDomains", Value: &one}, - &control.ConfEntry{Key: "ProtocolWarnings", Value: &one}) + err := conn.SetConf(control.KeyVals("LogMessageDomains", "1", "ProtocolWarnings", "1")...) ctx.Require.NoError(err) // Check again assertConfVals(one) // Reset em both - err = conn.ResetConf(&control.ConfEntry{Key: "LogMessageDomains"}, &control.ConfEntry{Key: "ProtocolWarnings"}) + err = conn.ResetConf(control.KeyVals("LogMessageDomains", "", "ProtocolWarnings", "")...) ctx.Require.NoError(err) // Make sure both back to zero assertConfVals("0") @@ -41,7 +40,7 @@ func TestLoadConf(t *testing.T) { ctx.Require.NoError(err) ctx.Require.Len(vals, 1) ctx.Require.Equal("config-text", vals[0].Key) - confText := vals[0].Value + confText := vals[0].Val // Append new conf val and load ctx.Require.NotContains(confText, "LogMessageDomains") confText += "\r\nLogMessageDomains 1" @@ -51,7 +50,7 @@ func TestLoadConf(t *testing.T) { ctx.Require.NoError(err) ctx.Require.Len(vals, 1) ctx.Require.Equal("config-text", vals[0].Key) - ctx.Require.Contains(vals[0].Value, "LogMessageDomains 1") + ctx.Require.Contains(vals[0].Val, "LogMessageDomains 1") } func TestSaveConf(t *testing.T) { @@ -62,7 +61,7 @@ func TestSaveConf(t *testing.T) { ctx.Require.NoError(err) ctx.Require.Len(vals, 1) ctx.Require.Equal("config-file", vals[0].Key) - confFile := vals[0].Value + confFile := vals[0].Val // Save it ctx.Require.NoError(conn.SaveConf(false)) // Read and make sure, say, the DataDirectory is accurate diff --git a/control/controltest/cmd_hiddenservice_test.go b/control/controltest/cmd_hiddenservice_test.go new file mode 100644 index 0000000..d846ce7 --- /dev/null +++ b/control/controltest/cmd_hiddenservice_test.go @@ -0,0 +1,17 @@ +package controltest + +import ( + "testing" + "time" + + "github.com/cretz/bine/control" +) + +func TestGetHiddenServiceDescriptorAsync(t *testing.T) { + ctx, conn := NewTestContextAuthenticated(t) + defer ctx.CloseConnected(conn) + // Enable the network + ctx.Require.NoError(conn.SetConf(control.NewKeyVal("DisableNetwork", "0"))) + ctx.Require.NoError(conn.GetHiddenServiceDescriptorAsync("facebookcorewwwi", "")) + time.Sleep(60 * time.Second) +} diff --git a/control/controltest/cmd_misc_test.go b/control/controltest/cmd_misc_test.go new file mode 100644 index 0000000..b6c5df3 --- /dev/null +++ b/control/controltest/cmd_misc_test.go @@ -0,0 +1,9 @@ +package controltest + +import "testing" + +func TestSignal(t *testing.T) { + ctx, conn := NewTestContextAuthenticated(t) + defer ctx.CloseConnected(conn) + ctx.Require.NoError(conn.Signal("HEARTBEAT")) +} diff --git a/control/keyval.go b/control/keyval.go new file mode 100644 index 0000000..1576901 --- /dev/null +++ b/control/keyval.go @@ -0,0 +1,28 @@ +package control + +type KeyVal struct { + Key string + Val string + // If true and the associated command supports nil vals, then an empty string for val is NOT considered nil like it + // otherwise would. This is ignored for commands that don't support nil vals. + ValSetAndEmpty bool +} + +func NewKeyVal(key string, val string) *KeyVal { + return &KeyVal{Key: key, Val: val} +} + +func KeyVals(keysAndVals ...string) []*KeyVal { + if len(keysAndVals)%2 != 0 { + panic("Expected multiple of 2") + } + ret := make([]*KeyVal, len(keysAndVals)/2) + for i := 0; i < len(ret); i++ { + ret[i] = NewKeyVal(keysAndVals[i*2], keysAndVals[i*2+1]) + } + return ret +} + +func (k *KeyVal) ValSet() bool { + return len(k.Val) > 0 || k.ValSetAndEmpty +} diff --git a/util/strings.go b/util/strings.go index a94916c..ddc31f5 100644 --- a/util/strings.go +++ b/util/strings.go @@ -13,6 +13,14 @@ func PartitionString(str string, ch byte) (string, string, bool) { return str[:index], str[index+1:], true } +func PartitionStringFromEnd(str string, ch byte) (string, string, bool) { + index := strings.LastIndexByte(str, ch) + if index == -1 { + return str, "", false + } + return str[:index], str[index+1:], true +} + func EscapeSimpleQuotedStringIfNeeded(str string) string { if strings.ContainsAny(str, " \\\"\r\n") { return EscapeSimpleQuotedString(str)