Work on protocol
This commit is contained in:
parent
0b0a87865f
commit
34ea0edde9
|
@ -0,0 +1,62 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ProtocolInfo struct {
|
||||
AuthMethods []string
|
||||
CookieFile string
|
||||
TorVersion string
|
||||
RawResponse *Response
|
||||
}
|
||||
|
||||
func (p *ProtocolInfo) HasAuthMethod(authMethod string) bool {
|
||||
for _, m := range p.AuthMethods {
|
||||
if m == authMethod {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Conn) RequestProtocolInfo() (*ProtocolInfo, error) {
|
||||
resp, err := c.SendRequest("PROTOCOLINFO")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check PIVERSION
|
||||
if len(resp.Data) == 0 || resp.Data[0] != "1" {
|
||||
return nil, newProtocolError("Invalid PIVERSION: %s", resp.Reply)
|
||||
}
|
||||
// Get other response vals
|
||||
ret := &ProtocolInfo{RawResponse: resp}
|
||||
for _, piece := range resp.Data {
|
||||
key, val, ok := partitionString(piece, ' ')
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "AUTH":
|
||||
methods, cookieFile, _ := partitionString(val, ' ')
|
||||
if !strings.HasPrefix(methods, "METHODS=") {
|
||||
continue
|
||||
}
|
||||
if cookieFile != "" {
|
||||
if !strings.HasPrefix(cookieFile, "COOKIEFILE=") {
|
||||
continue
|
||||
}
|
||||
if ret.CookieFile, err = parseQuotedString(cookieFile[11:]); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
ret.AuthMethods = strings.Split(methods[8:], ",")
|
||||
case "VERSION":
|
||||
torVersion, _, _ := partitionString(val, ' ')
|
||||
if strings.HasPrefix(torVersion, "Tor=") {
|
||||
ret.TorVersion, _ = parseQuotedString(torVersion[4:])
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/textproto"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
conn *textproto.Conn
|
||||
|
||||
asyncChansLock sync.RWMutex
|
||||
// Never mutated outside of lock, always created anew
|
||||
asyncChans []chan<- *Response
|
||||
}
|
||||
|
||||
func NewConn(conn *textproto.Conn) *Conn { return &Conn{conn: conn} }
|
||||
|
||||
func (c *Conn) SendSignal(signal string) error {
|
||||
_, err := c.SendRequest("SIGNAL %v", signal)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) SendRequest(format string, args ...interface{}) (*Response, error) {
|
||||
id, err := c.conn.Cmd(format, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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.onAsyncResponse(resp)
|
||||
}
|
||||
if err == nil && !resp.IsOk() {
|
||||
err = resp.Err
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
// We'll close all the chans first
|
||||
c.asyncChansLock.Lock()
|
||||
for _, ch := range c.asyncChans {
|
||||
close(ch)
|
||||
}
|
||||
c.asyncChans = nil
|
||||
c.asyncChansLock.Unlock()
|
||||
// Ignore the response and ignore the error
|
||||
c.SendRequest("QUIT")
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) AddAsyncChan(ch chan<- *Response) {
|
||||
c.asyncChansLock.Lock()
|
||||
chans := make([]chan<- *Response, len(c.asyncChans)+1)
|
||||
copy(chans, c.asyncChans)
|
||||
chans[len(chans)-1] = ch
|
||||
c.asyncChans = chans
|
||||
c.asyncChansLock.Unlock()
|
||||
}
|
||||
|
||||
// Does not close
|
||||
func (c *Conn) RemoveChan(ch chan<- *Response) bool {
|
||||
c.asyncChansLock.Lock()
|
||||
chans := make([]chan<- *Response, len(c.asyncChans)+1)
|
||||
copy(chans, c.asyncChans)
|
||||
index := -1
|
||||
for i, existing := range chans {
|
||||
if existing == ch {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index != -1 {
|
||||
chans = append(chans[:index], chans[index+1:]...)
|
||||
}
|
||||
c.asyncChans = chans
|
||||
c.asyncChansLock.Unlock()
|
||||
return index != -1
|
||||
}
|
||||
|
||||
func (c *Conn) onAsyncResponse(resp *Response) {
|
||||
c.asyncChansLock.RLock()
|
||||
chans := c.asyncChans
|
||||
c.asyncChansLock.RUnlock()
|
||||
// We will allow channels to block
|
||||
for _, ch := range chans {
|
||||
ch <- resp
|
||||
}
|
||||
}
|
||||
|
||||
func newProtocolError(format string, args ...interface{}) textproto.ProtocolError {
|
||||
return textproto.ProtocolError(fmt.Sprintf(format, args...))
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func partitionString(str string, ch byte) (string, string, bool) {
|
||||
index := strings.IndexByte(str, ch)
|
||||
if index == -1 {
|
||||
return str, "", false
|
||||
}
|
||||
return str[:index], str[index+1:], true
|
||||
}
|
||||
|
||||
func parseQuotedString(str string) (string, error) {
|
||||
if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' {
|
||||
return "", fmt.Errorf("Missing quotes")
|
||||
}
|
||||
return unescapeQuoted(str)
|
||||
}
|
||||
|
||||
func unescapeQuoted(str string) (string, error) {
|
||||
ret := ""
|
||||
escaping := false
|
||||
for _, c := range str {
|
||||
switch c {
|
||||
case '\\':
|
||||
if escaping {
|
||||
ret += "\\"
|
||||
}
|
||||
escaping = !escaping
|
||||
case '"':
|
||||
if !escaping {
|
||||
return "", fmt.Errorf("Unescaped quote")
|
||||
}
|
||||
ret += "\""
|
||||
escaping = false
|
||||
default:
|
||||
if escaping {
|
||||
return "", fmt.Errorf("Unexpected escape")
|
||||
}
|
||||
ret += string(c)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Response is a response to a control port command, or an asyncrhonous 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, StatusOkUnneccecary, StatusAsyncEvent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsAsync returns true if the response is an asyncrhonous event.
|
||||
func (r *Response) IsAsync() bool {
|
||||
return r.Err.Code == StatusAsyncEvent
|
||||
}
|
||||
|
||||
// ReadResponse returns the next response object. Calling this
|
||||
// simultaniously with Read, Request, or StartAsyncReader will lead to
|
||||
// undefined behavior
|
||||
func (c *Conn) ReadResponse() (*Response, error) {
|
||||
var resp *Response
|
||||
var statusCode int
|
||||
for {
|
||||
line, err := c.conn.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the line that was just read.
|
||||
if len(line) < 4 {
|
||||
return nil, newProtocolError("truncated response: '%s'", line)
|
||||
}
|
||||
if code, err := strconv.Atoi(line[0:3]); err != nil {
|
||||
return nil, newProtocolError("invalid status code: '%s'", line[0:3])
|
||||
} else if code < 100 {
|
||||
return nil, newProtocolError("invalid status code: '%s'", line[0:3])
|
||||
} else if resp == nil {
|
||||
resp = new(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, newProtocolError("status code changed: %03d != %03d", code, statusCode)
|
||||
}
|
||||
if resp.RawLines == nil {
|
||||
resp.RawLines = make([]string, 0, 1)
|
||||
}
|
||||
|
||||
if line[3] == ' ' {
|
||||
// Final line in the response.
|
||||
resp.Reply = line[4:]
|
||||
resp.Err = statusCodeToError(statusCode, resp.Reply)
|
||||
resp.RawLines = append(resp.RawLines, line)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if resp.Data == nil {
|
||||
resp.Data = make([]string, 0, 1)
|
||||
}
|
||||
switch line[3] {
|
||||
case '-':
|
||||
// Continuation, keep reading.
|
||||
resp.Data = append(resp.Data, line[4:])
|
||||
resp.RawLines = append(resp.RawLines, line)
|
||||
case '+':
|
||||
// A "dot-encoded" payload follows.
|
||||
resp.Data = append(resp.Data, line[4:])
|
||||
resp.RawLines = append(resp.RawLines, line)
|
||||
dotBody, err := c.conn.ReadDotBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Data = append(resp.Data, strings.TrimRight(string(dotBody), "\n\r"))
|
||||
dotLines := strings.Split(string(dotBody), "\n")
|
||||
for _, dotLine := range dotLines[:len(dotLines)-1] {
|
||||
resp.RawLines = append(resp.RawLines, dotLine)
|
||||
}
|
||||
resp.RawLines = append(resp.RawLines, ".")
|
||||
default:
|
||||
return nil, newProtocolError("invalid separator: '%c'", line[3])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The various control port StatusCode constants.
|
||||
const (
|
||||
StatusOk = 250
|
||||
StatusOkUnneccecary = 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",
|
||||
StatusOkUnneccecary: "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
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type Tor interface {
|
||||
Start(ctx context.Context, args []string) error
|
||||
}
|
||||
|
||||
type exeTor struct {
|
||||
exePath string
|
||||
}
|
||||
|
||||
func FromExePath(exePath string) Tor {
|
||||
return &exeTor{exePath}
|
||||
}
|
||||
|
||||
func (e *exeTor) Start(ctx context.Context, args []string) error {
|
||||
return exec.CommandContext(ctx, e.exePath, args...).Start()
|
||||
}
|
Loading…
Reference in New Issue