diff --git a/control/cmd_authenticate.go b/control/cmd_authenticate.go index 3d01aa0..518f809 100644 --- a/control/cmd_authenticate.go +++ b/control/cmd_authenticate.go @@ -98,11 +98,9 @@ func (c *Conn) Authenticate(password string) error { return err } -func (c *Conn) sendAuthenticate(byts []byte) (err error) { +func (c *Conn) sendAuthenticate(byts []byte) error { if len(byts) == 0 { - _, err = c.SendRequest("AUTHENTICATE") - } else { - _, err = c.SendRequest("AUTHENTICATE %v", hex.EncodeToString(byts)) + return c.sendRequestIgnoreResponse("AUTHENTICATE") } - return + return c.sendRequestIgnoreResponse("AUTHENTICATE %v", hex.EncodeToString(byts)) } diff --git a/control/cmd_circuit.go b/control/cmd_circuit.go new file mode 100644 index 0000000..6c162b7 --- /dev/null +++ b/control/cmd_circuit.go @@ -0,0 +1,35 @@ +package control + +import ( + "strings" +) + +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 +} + +func (c *Conn) SetCircuitPurpose(circuitID string, purpose string) error { + return c.sendRequestIgnoreResponse("SETCIRCUITPURPOSE %v purpose=%v", circuitID, purpose) +} + +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/control/cmd_conf.go b/control/cmd_conf.go index e3d43b0..4096593 100644 --- a/control/cmd_conf.go +++ b/control/cmd_conf.go @@ -30,8 +30,7 @@ func (c *Conn) sendSetConf(cmd string, entries []*ConfEntry) error { cmd += "=" + util.EscapeSimpleQuotedStringIfNeeded(*entry.Value) } } - _, err := c.SendRequest(cmd) - return err + return c.sendRequestIgnoreResponse(cmd) } func (c *Conn) GetConf(keys ...string) ([]*ConfEntry, error) { @@ -60,6 +59,9 @@ func (c *Conn) SaveConf(force bool) error { if force { cmd += " FORCE" } - _, err := c.SendRequest(cmd) - return err + return c.sendRequestIgnoreResponse(cmd) +} + +func (c *Conn) LoadConf(conf string) error { + return c.sendRequestIgnoreResponse("+LOADCONF\r\n%v\r\n.", conf) } diff --git a/control/cmd_event.go b/control/cmd_event.go index 5701756..9c3f41c 100644 --- a/control/cmd_event.go +++ b/control/cmd_event.go @@ -64,8 +64,7 @@ func (c *Conn) sendSetEvents() error { cmd += " " + string(event) } c.eventListenersLock.RUnlock() - _, err := c.SendRequest(cmd) - return err + return c.sendRequestIgnoreResponse(cmd) } // zero on fail diff --git a/control/cmd_hiddenservice.go b/control/cmd_hiddenservice.go new file mode 100644 index 0000000..7f9150f --- /dev/null +++ b/control/cmd_hiddenservice.go @@ -0,0 +1,21 @@ +package control + +func (c *Conn) GetHiddenServiceDescriptorAsync(address string, server string) error { + cmd := "HSFETCH " + address + if server != "" { + cmd += " SERVER=" + server + } + return c.sendRequestIgnoreResponse(cmd) +} + +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/control/cmd_misc.go b/control/cmd_misc.go index 2251a61..aa852d3 100644 --- a/control/cmd_misc.go +++ b/control/cmd_misc.go @@ -1,10 +1,17 @@ package control -import "github.com/cretz/bine/util" +import ( + "strings" + + "github.com/cretz/bine/util" +) func (c *Conn) Signal(signal string) error { - _, err := c.SendRequest("SIGNAL %v", signal) - return err + return c.sendRequestIgnoreResponse("SIGNAL %v", signal) +} + +func (c *Conn) Quit() error { + return c.sendRequestIgnoreResponse("QUIT") } type MappedAddress struct { @@ -34,3 +41,55 @@ func (c *Conn) MapAddresses(addresses []*MappedAddress) ([]*MappedAddress, error } return ret, nil } + +type InfoValue struct { + Key string + Value string +} + +func (c *Conn) GetInfo(keys ...string) ([]*InfoValue, error) { + resp, err := c.SendRequest("GETCONF %v", strings.Join(keys, " ")) + if err != nil { + return nil, err + } + ret := make([]*InfoValue, 0, len(resp.Data)) + for _, val := range resp.Data { + infoVal := &InfoValue{} + infoVal.Key, infoVal.Value, _ = util.PartitionString(val, '=') + ret = append(ret, infoVal) + } + return ret, nil +} + +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) +} + +func (c *Conn) UseFeatures(features ...string) error { + return c.sendRequestIgnoreResponse("USEFEATURE " + strings.Join(features, " ")) +} + +// TODO: can this take multiple +func (c *Conn) ResolveAsync(address string, reverse bool) error { + cmd := "RESOLVE " + if reverse { + cmd += "mode=reverse " + } + return c.sendRequestIgnoreResponse(cmd + address) +} + +func (c *Conn) TakeOwnership() error { + return c.sendRequestIgnoreResponse("TAKEOWNERSHIP") +} + +func (c *Conn) DropGuards() error { + return c.sendRequestIgnoreResponse("DROPGUARDS") +} diff --git a/control/cmd_onion.go b/control/cmd_onion.go new file mode 100644 index 0000000..4e5cdb3 --- /dev/null +++ b/control/cmd_onion.go @@ -0,0 +1,153 @@ +package control + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "fmt" + "strconv" + "strings" + + "github.com/cretz/bine/util" + "golang.org/x/crypto/ed25519" +) + +type KeyType string + +const ( + KeyTypeNew KeyType = "NEW" + KeyTypeRSA1024 KeyType = "RSA1024" + KeyTypeED25519V3 KeyType = "ED25519-V3" +) + +type KeyAlgo string + +const ( + KeyAlgoBest KeyAlgo = "BEST" + KeyAlgoRSA1024 KeyAlgo = "RSA1024" + KeyAlgoED25519V3 KeyAlgo = "ED25519-V3" +) + +type Key interface { + Type() KeyType + Blob() string +} + +func KeyFromString(str string) (Key, error) { + typ, blob, _ := util.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) + } +} + +type GenKey KeyAlgo + +func GenKeyFromBlob(blob string) GenKey { return GenKey(KeyAlgo(blob)) } +func (GenKey) Type() KeyType { return KeyTypeNew } +func (g GenKey) Blob() string { return string(g) } + +type RSAKey struct{ *rsa.PrivateKey } + +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 +} +func (*RSAKey) Type() KeyType { return KeyTypeRSA1024 } +func (r *RSAKey) Blob() string { + return base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(r.PrivateKey)) +} + +type ED25519Key ed25519.PrivateKey + +func ED25519KeyFromBlob(blob string) (ED25519Key, error) { + byts, err := base64.StdEncoding.DecodeString(blob) + if err != nil { + return nil, err + } + return ED25519Key(ed25519.PrivateKey(byts)), nil +} +func (ED25519Key) Type() KeyType { return KeyTypeED25519V3 } +func (e ED25519Key) Blob() string { return base64.StdEncoding.EncodeToString(e) } + +type AddOnionRequest struct { + Key Key + Flags []string + MaxStreams int + Ports map[string]string + ClientAuths map[string]string +} + +type AddOnionResponse struct { + ServiceID string + Key Key + ClientAuths map[string]string + RawResponse *Response +} + +func (c *Conn) AddOnion(req *AddOnionRequest) (*AddOnionResponse, error) { + // Build command + if req.Key == nil { + return nil, c.protoErr("Key required") + } + cmd := "ADDONION " + 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 virt, target := range req.Ports { + cmd += " Port=" + virt + if target != "" { + cmd += "," + target + } + } + 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, _ := util.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, _ := util.PartitionString(val, ':') + if ret.ClientAuths == nil { + ret.ClientAuths = map[string]string{} + } + ret.ClientAuths[name] = pass + } + } + return ret, nil +} + +func (c *Conn) DelOnion(serviceID string) error { + return c.sendRequestIgnoreResponse("DELONION %v", serviceID) +} diff --git a/control/cmd_stream.go b/control/cmd_stream.go new file mode 100644 index 0000000..86ca50f --- /dev/null +++ b/control/cmd_stream.go @@ -0,0 +1,28 @@ +package control + +import ( + "strconv" +) + +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) +} + +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) +} + +func (c *Conn) CloseStream(streamID string, reason string) error { + return c.sendRequestIgnoreResponse("CLOSESTREAM %v %v", streamID, reason) +} diff --git a/control/conn.go b/control/conn.go index 4be92dc..d5ffdfa 100644 --- a/control/conn.go +++ b/control/conn.go @@ -34,6 +34,11 @@ func NewConn(conn *textproto.Conn) *Conn { } } +func (c *Conn) sendRequestIgnoreResponse(format string, args ...interface{}) error { + _, err := c.SendRequest(format, args...) + return err +} + func (c *Conn) SendRequest(format string, args ...interface{}) (*Response, error) { if c.debugEnabled() { c.debugf("Write line: %v", fmt.Sprintf(format, args...)) @@ -58,11 +63,6 @@ func (c *Conn) SendRequest(format string, args ...interface{}) (*Response, error return resp, err } -func (c *Conn) Quit() error { - _, err := c.SendRequest("QUIT") - return err -} - func (c *Conn) Close() error { // We'll close all the chans first c.asyncChansLock.Lock()