diff options
Diffstat (limited to 'vendor/github.com/cretz/bine/control')
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_authenticate.go | 108 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_circuit.go | 38 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_conf.go | 65 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_event.go | 1218 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_hiddenservice.go | 23 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_misc.go | 92 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_onion.go | 201 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_protocolinfo.go | 76 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/cmd_stream.go | 31 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/conn.go | 102 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/doc.go | 10 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/keyval.go | 40 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/response.go | 106 | ||||
-rw-r--r-- | vendor/github.com/cretz/bine/control/status.go | 64 |
14 files changed, 2174 insertions, 0 deletions
diff --git a/vendor/github.com/cretz/bine/control/cmd_authenticate.go b/vendor/github.com/cretz/bine/control/cmd_authenticate.go new file mode 100644 index 0000000..c43d864 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_authenticate.go @@ -0,0 +1,108 @@ +package control + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "io/ioutil" + "strings" +) + +// Authenticate authenticates with the Tor instance using the "best" possible +// authentication method if not already authenticated and sets the Authenticated +// field. The password argument is optional, and will only be used if the +// "SAFECOOKIE" and "NULL" authentication methods are not available and +// "HASHEDPASSWORD" is. +func (c *Conn) Authenticate(password string) error { + if c.Authenticated { + return nil + } + // Determine the supported authentication methods, and the cookie path. + pi, err := c.ProtocolInfo() + if err != nil { + return err + } + // Get the bytes to pass to with authenticate + var authBytes []byte + if pi.HasAuthMethod("NULL") { + // No auth bytes + } else if pi.HasAuthMethod("SAFECOOKIE") { + if pi.CookieFile == "" { + return c.protoErr("Invalid (empty) COOKIEFILE") + } + cookie, err := ioutil.ReadFile(pi.CookieFile) + if err != nil { + return c.protoErr("Failed to read COOKIEFILE: %v", err) + } else if len(cookie) != 32 { + return c.protoErr("Invalid cookie file length: %v", len(cookie)) + } + + // Send an AUTHCHALLENGE command, and parse the response. + var clientNonce [32]byte + if _, err := rand.Read(clientNonce[:]); err != nil { + return c.protoErr("Failed to generate clientNonce: %v", err) + } + resp, err := c.SendRequest("AUTHCHALLENGE %s %s", "SAFECOOKIE", hex.EncodeToString(clientNonce[:])) + if err != nil { + return err + } + splitResp := strings.Split(resp.Reply, " ") + if len(splitResp) != 3 || !strings.HasPrefix(splitResp[1], "SERVERHASH=") || + !strings.HasPrefix(splitResp[2], "SERVERNONCE=") { + return c.protoErr("Invalid AUTHCHALLENGE response") + } + serverHash, err := hex.DecodeString(splitResp[1][11:]) + if err != nil { + return c.protoErr("Failed to decode ServerHash: %v", err) + } + if len(serverHash) != 32 { + return c.protoErr("Invalid ServerHash length: %d", len(serverHash)) + } + serverNonce, err := hex.DecodeString(splitResp[2][12:]) + if err != nil { + return c.protoErr("Failed to decode ServerNonce: %v", err) + } + if len(serverNonce) != 32 { + return c.protoErr("Invalid ServerNonce length: %d", len(serverNonce)) + } + + // Validate the ServerHash. + m := hmac.New(sha256.New, []byte("Tor safe cookie authentication server-to-controller hash")) + m.Write(cookie) + m.Write(clientNonce[:]) + m.Write(serverNonce) + dervServerHash := m.Sum(nil) + if !hmac.Equal(serverHash, dervServerHash) { + return c.protoErr("invalid ServerHash: mismatch") + } + + // Calculate the ClientHash, and issue the AUTHENTICATE. + m = hmac.New(sha256.New, []byte("Tor safe cookie authentication controller-to-server hash")) + m.Write(cookie) + m.Write(clientNonce[:]) + m.Write(serverNonce) + authBytes = m.Sum(nil) + } else if pi.HasAuthMethod("HASHEDPASSWORD") { + // Despite the name HASHEDPASSWORD, the raw password is actually sent. According to the code, this can either be + // a QuotedString, or base16 encoded, so go with the later since it's easier to handle. + if password == "" { + return c.protoErr("password auth needs a password") + } + authBytes = []byte(password) + } else { + return c.protoErr("No supported authentication methods") + } + // Send it + if err = c.sendAuthenticate(authBytes); err == nil { + c.Authenticated = true + } + return err +} + +func (c *Conn) sendAuthenticate(byts []byte) error { + if len(byts) == 0 { + return c.sendRequestIgnoreResponse("AUTHENTICATE") + } + return c.sendRequestIgnoreResponse("AUTHENTICATE %v", hex.EncodeToString(byts)) +} diff --git a/vendor/github.com/cretz/bine/control/cmd_circuit.go b/vendor/github.com/cretz/bine/control/cmd_circuit.go new file mode 100644 index 0000000..f796145 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_circuit.go @@ -0,0 +1,38 @@ +package control + +import ( + "strings" +) + +// ExtendCircuit invokes EXTENDCIRCUIT and returns the circuit ID on success. +func (c *Conn) ExtendCircuit(circuitID string, path []string, purpose string) (string, error) { + if circuitID == "" { + circuitID = "0" + } + cmd := "EXTENDCIRCUIT " + circuitID + if len(path) > 0 { + cmd += " " + strings.Join(path, ",") + } + if purpose != "" { + cmd += " purpose=" + purpose + } + resp, err := c.SendRequest(cmd) + if err != nil { + return "", err + } + return resp.Reply[strings.LastIndexByte(resp.Reply, ' ')+1:], nil +} + +// SetCircuitPurpose invokes SETCIRCUITPURPOSE. +func (c *Conn) SetCircuitPurpose(circuitID string, purpose string) error { + return c.sendRequestIgnoreResponse("SETCIRCUITPURPOSE %v purpose=%v", circuitID, purpose) +} + +// CloseCircuit invokes CLOSECIRCUIT. +func (c *Conn) CloseCircuit(circuitID string, flags []string) error { + cmd := "CLOSECIRCUIT " + circuitID + for _, flag := range flags { + cmd += " " + flag + } + return c.sendRequestIgnoreResponse(cmd) +} diff --git a/vendor/github.com/cretz/bine/control/cmd_conf.go b/vendor/github.com/cretz/bine/control/cmd_conf.go new file mode 100644 index 0000000..c0446e5 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_conf.go @@ -0,0 +1,65 @@ +package control + +import ( + "strings" + + "github.com/cretz/bine/torutil" +) + +// SetConf invokes SETCONF. +func (c *Conn) SetConf(entries ...*KeyVal) error { + return c.sendSetConf("SETCONF", entries) +} + +// ResetConf invokes RESETCONF. +func (c *Conn) ResetConf(entries ...*KeyVal) error { + return c.sendSetConf("RESETCONF", entries) +} + +func (c *Conn) sendSetConf(cmd string, entries []*KeyVal) error { + for _, entry := range entries { + cmd += " " + entry.Key + if entry.ValSet() { + cmd += "=" + torutil.EscapeSimpleQuotedStringIfNeeded(entry.Val) + } + } + return c.sendRequestIgnoreResponse(cmd) +} + +// GetConf invokes GETCONF and returns the values for the requested keys. +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([]*KeyVal, 0, len(data)) + for _, data := range data { + key, val, ok := torutil.PartitionString(data, '=') + entry := &KeyVal{Key: key} + if ok { + if entry.Val, err = torutil.UnescapeSimpleQuotedStringIfNeeded(val); err != nil { + return nil, err + } + if len(entry.Val) == 0 { + entry.ValSetAndEmpty = true + } + } + ret = append(ret, entry) + } + return ret, nil +} + +// SaveConf invokes SAVECONF. +func (c *Conn) SaveConf(force bool) error { + cmd := "SAVECONF" + if force { + cmd += " FORCE" + } + return c.sendRequestIgnoreResponse(cmd) +} + +// LoadConf invokes LOADCONF. +func (c *Conn) LoadConf(conf string) error { + return c.sendRequestIgnoreResponse("+LOADCONF\r\n%v\r\n.", conf) +} diff --git a/vendor/github.com/cretz/bine/control/cmd_event.go b/vendor/github.com/cretz/bine/control/cmd_event.go new file mode 100644 index 0000000..578a800 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_event.go @@ -0,0 +1,1218 @@ +package control + +import ( + "context" + "errors" + "strconv" + "strings" + "time" + + "github.com/cretz/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 } diff --git a/vendor/github.com/cretz/bine/control/cmd_hiddenservice.go b/vendor/github.com/cretz/bine/control/cmd_hiddenservice.go new file mode 100644 index 0000000..ab52142 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_hiddenservice.go @@ -0,0 +1,23 @@ +package control + +// GetHiddenServiceDescriptorAsync invokes HSFETCH. +func (c *Conn) GetHiddenServiceDescriptorAsync(address string, server string) error { + cmd := "HSFETCH " + address + if server != "" { + cmd += " SERVER=" + server + } + return c.sendRequestIgnoreResponse(cmd) +} + +// PostHiddenServiceDescriptorAsync invokes HSPOST. +func (c *Conn) PostHiddenServiceDescriptorAsync(desc string, servers []string, address string) error { + cmd := "+HSPOST" + for _, server := range servers { + cmd += " SERVER=" + server + } + if address != "" { + cmd += "HSADDRESS=" + address + } + cmd += "\r\n" + desc + "\r\n." + return c.sendRequestIgnoreResponse(cmd) +} diff --git a/vendor/github.com/cretz/bine/control/cmd_misc.go b/vendor/github.com/cretz/bine/control/cmd_misc.go new file mode 100644 index 0000000..6c3fa49 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_misc.go @@ -0,0 +1,92 @@ +package control + +import ( + "strings" + + "github.com/cretz/bine/torutil" +) + +// Signal invokes SIGNAL. +func (c *Conn) Signal(signal string) error { + return c.sendRequestIgnoreResponse("SIGNAL %v", signal) +} + +// Quit invokes QUIT. +func (c *Conn) Quit() error { + return c.sendRequestIgnoreResponse("QUIT") +} + +// MapAddresses invokes MAPADDRESS and returns mapped addresses. +func (c *Conn) MapAddresses(addresses ...*KeyVal) ([]*KeyVal, error) { + cmd := "MAPADDRESS" + for _, address := range addresses { + cmd += " " + address.Key + "=" + address.Val + } + resp, err := c.SendRequest(cmd) + if err != nil { + return nil, err + } + data := resp.DataWithReply() + ret := make([]*KeyVal, 0, len(data)) + for _, address := range data { + mappedAddress := &KeyVal{} + mappedAddress.Key, mappedAddress.Val, _ = torutil.PartitionString(address, '=') + ret = append(ret, mappedAddress) + } + return ret, nil +} + +// GetInfo invokes GETINTO and returns values for requested keys. +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([]*KeyVal, 0, len(resp.Data)) + for _, val := range resp.Data { + infoVal := &KeyVal{} + infoVal.Key, infoVal.Val, _ = torutil.PartitionString(val, '=') + if infoVal.Val, err = torutil.UnescapeSimpleQuotedStringIfNeeded(infoVal.Val); err != nil { + return nil, err + } + ret = append(ret, infoVal) + } + return ret, nil +} + +// PostDescriptor invokes POSTDESCRIPTOR. +func (c *Conn) PostDescriptor(descriptor string, purpose string, cache string) error { + cmd := "+POSTDESCRIPTOR" + if purpose != "" { + cmd += " purpose=" + purpose + } + if cache != "" { + cmd += " cache=" + cache + } + cmd += "\r\n" + descriptor + "\r\n." + return c.sendRequestIgnoreResponse(cmd) +} + +// UseFeatures invokes USEFEATURE. +func (c *Conn) UseFeatures(features ...string) error { + return c.sendRequestIgnoreResponse("USEFEATURE " + strings.Join(features, " ")) +} + +// ResolveAsync invokes RESOLVE. +func (c *Conn) ResolveAsync(address string, reverse bool) error { + cmd := "RESOLVE " + if reverse { + cmd += "mode=reverse " + } + return c.sendRequestIgnoreResponse(cmd + address) +} + +// TakeOwnership invokes TAKEOWNERSHIP. +func (c *Conn) TakeOwnership() error { + return c.sendRequestIgnoreResponse("TAKEOWNERSHIP") +} + +// DropGuards invokes DROPGUARDS. +func (c *Conn) DropGuards() error { + return c.sendRequestIgnoreResponse("DROPGUARDS") +} diff --git a/vendor/github.com/cretz/bine/control/cmd_onion.go b/vendor/github.com/cretz/bine/control/cmd_onion.go new file mode 100644 index 0000000..b298d36 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_onion.go @@ -0,0 +1,201 @@ +package control + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "fmt" + "strconv" + "strings" + + "github.com/cretz/bine/torutil" + "github.com/cretz/bine/torutil/ed25519" +) + +// KeyType is a key type for Key in AddOnion. +type KeyType string + +const ( + // KeyTypeNew is NEW. + KeyTypeNew KeyType = "NEW" + // KeyTypeRSA1024 is RSA1024. + KeyTypeRSA1024 KeyType = "RSA1024" + // KeyTypeED25519V3 is ED25519-V3. + KeyTypeED25519V3 KeyType = "ED25519-V3" +) + +// KeyAlgo is a key algorithm for GenKey on AddOnion. +type KeyAlgo string + +const ( + // KeyAlgoBest is BEST. + KeyAlgoBest KeyAlgo = "BEST" + // KeyAlgoRSA1024 is RSA1024. + KeyAlgoRSA1024 KeyAlgo = "RSA1024" + // KeyAlgoED25519V3 is ED25519-V3. + KeyAlgoED25519V3 KeyAlgo = "ED25519-V3" +) + +// Key is a type of key to use for AddOnion. Implementations include GenKey, +// RSAKey, and ED25519Key. +type Key interface { + // Type is the KeyType for AddOnion. + Type() KeyType + // Blob is the serialized key for AddOnion. + Blob() string +} + +// KeyFromString creates a Key for AddOnion based on a response string. +func KeyFromString(str string) (Key, error) { + typ, blob, _ := torutil.PartitionString(str, ':') + switch KeyType(typ) { + case KeyTypeNew: + return GenKeyFromBlob(blob), nil + case KeyTypeRSA1024: + return RSA1024KeyFromBlob(blob) + case KeyTypeED25519V3: + return ED25519KeyFromBlob(blob) + default: + return nil, fmt.Errorf("Unrecognized key type: %v", typ) + } +} + +// GenKey is a Key for AddOnion that asks Tor to generate a key for the given +// algorithm. +type GenKey KeyAlgo + +// GenKeyFromBlob creates a GenKey for the given response blob which is a +// KeyAlgo. +func GenKeyFromBlob(blob string) GenKey { return GenKey(KeyAlgo(blob)) } + +// Type implements Key.Type. +func (GenKey) Type() KeyType { return KeyTypeNew } + +// Blob implements Key.Blob. +func (g GenKey) Blob() string { return string(g) } + +// RSAKey is a Key for AddOnion that is a RSA-1024 key (i.e. v2). +type RSAKey struct{ *rsa.PrivateKey } + +// RSA1024KeyFromBlob creates a RSAKey for the given response blob. +func RSA1024KeyFromBlob(blob string) (*RSAKey, error) { + byts, err := base64.StdEncoding.DecodeString(blob) + if err != nil { + return nil, err + } + rsaKey, err := x509.ParsePKCS1PrivateKey(byts) + if err != nil { + return nil, err + } + return &RSAKey{rsaKey}, nil +} + +// Type implements Key.Type. +func (*RSAKey) Type() KeyType { return KeyTypeRSA1024 } + +// Blob implements Key.Blob. +func (r *RSAKey) Blob() string { + return base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(r.PrivateKey)) +} + +// ED25519Key is a Key for AddOnion that is a ed25519 key (i.e. v3). +type ED25519Key struct{ ed25519.KeyPair } + +// ED25519KeyFromBlob creates a ED25519Key for the given response blob. +func ED25519KeyFromBlob(blob string) (*ED25519Key, error) { + byts, err := base64.StdEncoding.DecodeString(blob) + if err != nil { + return nil, err + } + return &ED25519Key{ed25519.PrivateKey(byts).KeyPair()}, nil +} + +// Type implements Key.Type. +func (*ED25519Key) Type() KeyType { return KeyTypeED25519V3 } + +// Blob implements Key.Blob. +func (e *ED25519Key) Blob() string { return base64.StdEncoding.EncodeToString(e.PrivateKey()) } + +// AddOnionRequest is a set of request params for AddOnion. +type AddOnionRequest struct { + // Key is the key to use or GenKey if Tor should generate it. + Key Key + // Flags are ADD_ONION flags. + Flags []string + // MaxStreams is ADD_ONION MaxStreams. + MaxStreams int + // Ports are ADD_ONION Port values. Key is virtual port, Val is target + // port (or can be empty to use virtual port). + Ports []*KeyVal + // ClientAuths are ADD_ONION ClientAuth values. If value is empty string, + // Tor will generate the password. + ClientAuths map[string]string +} + +// AddOnionResponse is the response for AddOnion. +type AddOnionResponse struct { + // ServiceID is the ADD_ONION response ServiceID value. + ServiceID string + // Key is the ADD_ONION response PrivateKey value. + Key Key + // ClientAuths are the ADD_ONION response ClientAuth values. + ClientAuths map[string]string + // RawResponse is the raw ADD_ONION response. + RawResponse *Response +} + +// AddOnion invokes ADD_ONION and returns its response. +func (c *Conn) AddOnion(req *AddOnionRequest) (*AddOnionResponse, error) { + // Build command + if req.Key == nil { + return nil, c.protoErr("Key required") + } + cmd := "ADD_ONION " + string(req.Key.Type()) + ":" + req.Key.Blob() + if len(req.Flags) > 0 { + cmd += " Flags=" + strings.Join(req.Flags, ",") + } + if req.MaxStreams > 0 { + cmd += " MaxStreams=" + strconv.Itoa(req.MaxStreams) + } + for _, port := range req.Ports { + cmd += " Port=" + port.Key + if port.Val != "" { + cmd += "," + port.Val + } + } + for name, blob := range req.ClientAuths { + cmd += " ClientAuth=" + name + if blob != "" { + cmd += ":" + blob + } + } + // Invoke and read response + resp, err := c.SendRequest(cmd) + if err != nil { + return nil, err + } + ret := &AddOnionResponse{RawResponse: resp} + for _, data := range resp.Data { + key, val, _ := torutil.PartitionString(data, '=') + switch key { + case "ServiceID": + ret.ServiceID = val + case "PrivateKey": + if ret.Key, err = KeyFromString(val); err != nil { + return nil, err + } + case "ClientAuth": + name, pass, _ := torutil.PartitionString(val, ':') + if ret.ClientAuths == nil { + ret.ClientAuths = map[string]string{} + } + ret.ClientAuths[name] = pass + } + } + return ret, nil +} + +// DelOnion invokes DELONION. +func (c *Conn) DelOnion(serviceID string) error { + return c.sendRequestIgnoreResponse("DEL_ONION %v", serviceID) +} diff --git a/vendor/github.com/cretz/bine/control/cmd_protocolinfo.go b/vendor/github.com/cretz/bine/control/cmd_protocolinfo.go new file mode 100644 index 0000000..346b36d --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_protocolinfo.go @@ -0,0 +1,76 @@ +package control + +import ( + "strings" + + "github.com/cretz/bine/torutil" +) + +// ProtocolInfo is the protocol info result of Conn.ProtocolInfo. +type ProtocolInfo struct { + AuthMethods []string + CookieFile string + TorVersion string + RawResponse *Response +} + +// HasAuthMethod checks if ProtocolInfo contains the requested auth method. +func (p *ProtocolInfo) HasAuthMethod(authMethod string) bool { + for _, m := range p.AuthMethods { + if m == authMethod { + return true + } + } + return false +} + +// ProtocolInfo invokes PROTOCOLINFO on first invocation and returns a cached +// result on all others. +func (c *Conn) ProtocolInfo() (*ProtocolInfo, error) { + var err error + if c.protocolInfo == nil { + c.protocolInfo, err = c.sendProtocolInfo() + } + return c.protocolInfo, err +} + +func (c *Conn) sendProtocolInfo() (*ProtocolInfo, error) { + resp, err := c.SendRequest("PROTOCOLINFO") + if err != nil { + return nil, err + } + // Check data vals + ret := &ProtocolInfo{RawResponse: resp} + for _, piece := range resp.Data { + key, val, ok := torutil.PartitionString(piece, ' ') + if !ok { + continue + } + switch key { + case "PROTOCOLINFO": + if val != "1" { + return nil, c.protoErr("Invalid PIVERSION: %v", val) + } + case "AUTH": + methods, cookieFile, _ := torutil.PartitionString(val, ' ') + if !strings.HasPrefix(methods, "METHODS=") { + continue + } + if cookieFile != "" { + if !strings.HasPrefix(cookieFile, "COOKIEFILE=") { + continue + } + if ret.CookieFile, err = torutil.UnescapeSimpleQuotedString(cookieFile[11:]); err != nil { + continue + } + } + ret.AuthMethods = strings.Split(methods[8:], ",") + case "VERSION": + torVersion, _, _ := torutil.PartitionString(val, ' ') + if strings.HasPrefix(torVersion, "Tor=") { + ret.TorVersion, err = torutil.UnescapeSimpleQuotedString(torVersion[4:]) + } + } + } + return ret, nil +} diff --git a/vendor/github.com/cretz/bine/control/cmd_stream.go b/vendor/github.com/cretz/bine/control/cmd_stream.go new file mode 100644 index 0000000..6fde0a8 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/cmd_stream.go @@ -0,0 +1,31 @@ +package control + +import ( + "strconv" +) + +// AttachStream invokes ATTACHSTREAM. +func (c *Conn) AttachStream(streamID string, circuitID string, hopNum int) error { + if circuitID == "" { + circuitID = "0" + } + cmd := "ATTACHSTREAM " + streamID + " " + circuitID + if hopNum > 0 { + cmd += " HOP=" + strconv.Itoa(hopNum) + } + return c.sendRequestIgnoreResponse(cmd) +} + +// RedirectStream invokes REDIRECTSTREAM. +func (c *Conn) RedirectStream(streamID string, address string, port int) error { + cmd := "REDIRECTSTREAM " + streamID + " " + address + if port > 0 { + cmd += " " + strconv.Itoa(port) + } + return c.sendRequestIgnoreResponse(cmd) +} + +// CloseStream invokes CLOSESTREAM. +func (c *Conn) CloseStream(streamID string, reason string) error { + return c.sendRequestIgnoreResponse("CLOSESTREAM %v %v", streamID, reason) +} diff --git a/vendor/github.com/cretz/bine/control/conn.go b/vendor/github.com/cretz/bine/control/conn.go new file mode 100644 index 0000000..02995e8 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/conn.go @@ -0,0 +1,102 @@ +package control + +import ( + "fmt" + "io" + "net/textproto" + "sync" +) + +// Conn is the connection to the Tor control port. +type Conn struct { + // DebugWriter is the writer that debug logs for this library (not Tor + // itself) will be written to. If nil, no debug logs are generated/written. + DebugWriter io.Writer + + // This is the underlying connection. + conn *textproto.Conn + + // This is set lazily by ProtocolInfo(). + protocolInfo *ProtocolInfo + + // True if Authenticate has been called successfully. + Authenticated bool + + // The lock fot eventListeners + eventListenersLock sync.RWMutex + // The value slices can be traversed outside of lock, they are completely + // replaced on change, never mutated. But the map itself must be locked on + // when reading or writing. + eventListeners map[EventCode][]chan<- Event + + // This mutex is locked on when an entire response needs to be read. It + // helps synchronize accesses to the response by the asynchronous response + // listeners and the synchronous responses. + readLock sync.Mutex +} + +// NewConn creates a Conn from the given textproto connection. +func NewConn(conn *textproto.Conn) *Conn { + return &Conn{ + conn: conn, + eventListeners: map[EventCode][]chan<- Event{}, + } +} + +func (c *Conn) sendRequestIgnoreResponse(format string, args ...interface{}) error { + _, err := c.SendRequest(format, args...) + return err +} + +// SendRequest sends a synchronous request to Tor and awaits the response. If +// the response errors, the error result will be set, but the response will be +// set also. This is usually not directly used by callers, but instead called by +// higher-level methods. +func (c *Conn) SendRequest(format string, args ...interface{}) (*Response, error) { + if c.debugEnabled() { + c.debugf("Write line: %v", fmt.Sprintf(format, args...)) + } + id, err := c.conn.Cmd(format, args...) + if err != nil { + return nil, err + } + c.readLock.Lock() + defer c.readLock.Unlock() + c.conn.StartResponse(id) + defer c.conn.EndResponse(id) + // Get the first non-async response + var resp *Response + for { + if resp, err = c.ReadResponse(); err != nil || !resp.IsAsync() { + break + } + c.relayAsyncEvents(resp) + } + if err == nil && !resp.IsOk() { + err = resp.Err + } + return resp, err +} + +// Close sends a QUIT and closes the underlying Tor connection. This does not +// error if the QUIT is not accepted but does relay any error that occurs while +// closing the underlying connection. +func (c *Conn) Close() error { + // Ignore the response and ignore the error + c.Quit() + return c.conn.Close() +} + +func (c *Conn) debugEnabled() bool { + return c.DebugWriter != nil +} + +func (c *Conn) debugf(format string, args ...interface{}) { + if w := c.DebugWriter; w != nil { + fmt.Fprintf(w, format+"\n", args...) + } +} + +func (*Conn) protoErr(format string, args ...interface{}) textproto.ProtocolError { + return textproto.ProtocolError(fmt.Sprintf(format, args...)) +} diff --git a/vendor/github.com/cretz/bine/control/doc.go b/vendor/github.com/cretz/bine/control/doc.go new file mode 100644 index 0000000..2be3dc1 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/doc.go @@ -0,0 +1,10 @@ +// Package control implements a low-level client for the Tor control spec +// version 1. +// +// The primary entrypoint is the Conn struct, instantiated with NewConn. This is +// the low-level layer to the control port of an already-running Tor instance. +// Most developers will prefer the tor package adjacent to this one for a higher +// level abstraction over the process and this connection. +// +// Some of this code is lifted from https://github.com/yawning/bulb with thanks. +package control diff --git a/vendor/github.com/cretz/bine/control/keyval.go b/vendor/github.com/cretz/bine/control/keyval.go new file mode 100644 index 0000000..00cefd8 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/keyval.go @@ -0,0 +1,40 @@ +package control + +// KeyVal is a simple key-value struct. In cases where Val can be nil, an empty +// string represents that unless ValSetAndEmpty is true. +type KeyVal struct { + // Key is the always-present key + Key string + + // Val is the value. If it's an empty string and nils are accepted/supported + // where this is used, it means nil unless ValSetAndEmpty is true. + Val string + + // ValSetAndEmpty is true when Val is an empty string, the associated + // command supports nils, and Val should NOT be treated as nil. False + // otherwise. + ValSetAndEmpty bool +} + +// NewKeyVal creates a new key-value pair. +func NewKeyVal(key string, val string) *KeyVal { + return &KeyVal{Key: key, Val: val} +} + +// KeyVals creates multiple new key-value pairs from the given strings. The +// provided set of strings must have a length that is a multiple of 2. +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 +} + +// ValSet returns true if Val is either non empty or ValSetAndEmpty is true. +func (k *KeyVal) ValSet() bool { + return len(k.Val) > 0 || k.ValSetAndEmpty +} diff --git a/vendor/github.com/cretz/bine/control/response.go b/vendor/github.com/cretz/bine/control/response.go new file mode 100644 index 0000000..705905e --- /dev/null +++ b/vendor/github.com/cretz/bine/control/response.go @@ -0,0 +1,106 @@ +package control + +import ( + "net/textproto" + "strconv" + "strings" +) + +// Response is a response to a control port command or an asynchronous event. +type Response struct { + // Err is the status code and string representation associated with a + // response. Responses that have completed successfully will also have Err + // set to indicate such. + Err *textproto.Error + + // Reply is the text on the EndReplyLine of the response. + Reply string + + // Data is the MidReplyLines/DataReplyLines of the response. Dot encoded + // data is "decoded" and presented as a single string (terminal ".CRLF" + // removed, all intervening CRs stripped). + Data []string + + // RawLines is all of the lines of a response, without CRLFs. + RawLines []string +} + +// IsOk returns true if the response status code indicates success or an +// asynchronous event. +func (r *Response) IsOk() bool { + switch r.Err.Code { + case StatusOk, StatusOkUnnecessary, StatusAsyncEvent: + return true + default: + return false + } +} + +// DataWithReply returns a combination of Data and Reply to give a full set of +// the lines of the response. +func (r *Response) DataWithReply() []string { + ret := make([]string, len(r.Data)+1) + copy(ret, r.Data) + ret[len(ret)-1] = r.Reply + return ret +} + +// IsAsync returns true if the response is an asynchronous event. +func (r *Response) IsAsync() bool { + return r.Err.Code == StatusAsyncEvent +} + +// ReadResponse returns the next response object. +func (c *Conn) ReadResponse() (*Response, error) { + var resp *Response + var statusCode int + for { + line, err := c.conn.ReadLine() + if err != nil { + return nil, err + } + c.debugf("Read line: %v", line) + + // Parse the line that was just read. + if len(line) < 4 { + return nil, c.protoErr("Truncated response: %v", line) + } + if code, err := strconv.Atoi(line[0:3]); err != nil || code < 100 { + return nil, c.protoErr("Invalid status code: %v", line[0:3]) + } else if resp == nil { + resp = &Response{} + statusCode = code + } else if code != statusCode { + // The status code should stay fixed for all lines of the response, since events can't be interleaved with + // response lines. + return nil, c.protoErr("Status code changed: %v != %v", code, statusCode) + } + resp.RawLines = append(resp.RawLines, line) + switch line[3] { + case ' ': + // Final line in the response. + resp.Reply = line[4:] + resp.Err = statusCodeToError(statusCode, resp.Reply) + return resp, nil + case '-': + // Continuation, keep reading. + resp.Data = append(resp.Data, line[4:]) + case '+': + // A "dot-encoded" payload follows. + dotBody, err := c.conn.ReadDotBytes() + if err != nil { + return nil, err + } + dotBodyStr := strings.TrimRight(string(dotBody), "\n\r") + // c.debugf("Read dot body:\n---\n%v\n---", dotBodyStr) + resp.Data = append(resp.Data, line[4:]+"\r\n"+dotBodyStr) + dotLines := strings.Split(dotBodyStr, "\n") + for _, dotLine := range dotLines[:len(dotLines)-1] { + resp.RawLines = append(resp.RawLines, dotLine) + } + resp.RawLines = append(resp.RawLines, ".") + default: + return nil, c.protoErr("Invalid separator: '%v'", line[3]) + } + } +} diff --git a/vendor/github.com/cretz/bine/control/status.go b/vendor/github.com/cretz/bine/control/status.go new file mode 100644 index 0000000..8be3464 --- /dev/null +++ b/vendor/github.com/cretz/bine/control/status.go @@ -0,0 +1,64 @@ +package control + +import ( + "fmt" + "net/textproto" + "strings" +) + +// The various control port StatusCode constants. +const ( + StatusOk = 250 + StatusOkUnnecessary = 251 + + StatusErrResourceExhausted = 451 + StatusErrSyntaxError = 500 + StatusErrUnrecognizedCmd = 510 + StatusErrUnimplementedCmd = 511 + StatusErrSyntaxErrorArg = 512 + StatusErrUnrecognizedCmdArg = 513 + StatusErrAuthenticationRequired = 514 + StatusErrBadAuthentication = 515 + StatusErrUnspecifiedTorError = 550 + StatusErrInternalError = 551 + StatusErrUnrecognizedEntity = 552 + StatusErrInvalidConfigValue = 553 + StatusErrInvalidDescriptor = 554 + StatusErrUnmanagedEntity = 555 + + StatusAsyncEvent = 650 +) + +var statusCodeStringMap = map[int]string{ + StatusOk: "OK", + StatusOkUnnecessary: "Operation was unnecessary", + + StatusErrResourceExhausted: "Resource exhausted", + StatusErrSyntaxError: "Syntax error: protocol", + StatusErrUnrecognizedCmd: "Unrecognized command", + StatusErrUnimplementedCmd: "Unimplemented command", + StatusErrSyntaxErrorArg: "Syntax error in command argument", + StatusErrUnrecognizedCmdArg: "Unrecognized command argument", + StatusErrAuthenticationRequired: "Authentication required", + StatusErrBadAuthentication: "Bad authentication", + StatusErrUnspecifiedTorError: "Unspecified Tor error", + StatusErrInternalError: "Internal error", + StatusErrUnrecognizedEntity: "Unrecognized entity", + StatusErrInvalidConfigValue: "Invalid configuration value", + StatusErrInvalidDescriptor: "Invalid descriptor", + StatusErrUnmanagedEntity: "Unmanaged entity", + + StatusAsyncEvent: "Asynchronous event notification", +} + +func statusCodeToError(code int, reply string) *textproto.Error { + err := new(textproto.Error) + err.Code = code + if msg, ok := statusCodeStringMap[code]; ok { + trimmedReply := strings.TrimSpace(strings.TrimPrefix(reply, msg)) + err.Msg = fmt.Sprintf("%s: %s", msg, trimmedReply) + } else { + err.Msg = fmt.Sprintf("Unknown status code (%03d): %s", code, reply) + } + return err +} |