More actions

This commit is contained in:
Chad Retz 2018-05-11 14:43:58 -05:00
parent cd13abdf50
commit d340cb4c4f
9 changed files with 337 additions and 80 deletions

View File

@ -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)
}

View File

@ -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 }

View File

@ -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

View File

@ -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"))
}

View File

@ -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

View File

@ -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)
}

View File

@ -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"))
}

28
control/keyval.go Normal file
View File

@ -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
}

View File

@ -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)