242 lines
6.2 KiB
Go
242 lines
6.2 KiB
Go
package torgo
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// A Controller instance is a control port connection that provides methods for
|
|
// communicating with Tor.
|
|
type Controller struct {
|
|
// Array of available authentication methods.
|
|
AuthMethods []string
|
|
// Cookie file path (empty if not available).
|
|
CookieFile string
|
|
// Text is a textproto.Conn to the control port.
|
|
Text *textproto.Conn
|
|
}
|
|
|
|
// NewController returns a new Controller instance connecting to the control
|
|
// port at addr.
|
|
func NewController(addr string) (*Controller, error) {
|
|
text, err := textproto.Dial("tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := &Controller{Text: text}
|
|
err = c.getProtocolInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Make a textproto request and expect the command to have a valid 250 status.
|
|
func (c *Controller) makeRequest(request string) (int, string, error) {
|
|
id, err := c.Text.Cmd(request)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
c.Text.StartResponse(id)
|
|
defer c.Text.EndResponse(id)
|
|
return c.Text.ReadResponse(250)
|
|
}
|
|
|
|
// Perform PROTOCOLINFO command and parse the response.
|
|
func (c *Controller) getProtocolInfo() error {
|
|
_, msg, err := c.makeRequest("PROTOCOLINFO 1")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines := strings.Split(msg, "\n")
|
|
authPrefix := "AUTH METHODS="
|
|
cookiePrefix := "COOKIEFILE="
|
|
for _, line := range lines {
|
|
// Check for AUTH METHODS line
|
|
if strings.HasPrefix(line, authPrefix) {
|
|
// This line should be in a format like:
|
|
/// AUTH METHODS=method1,method2,methodN COOKIEFILE=cookiefilepath
|
|
line = line[len(authPrefix):]
|
|
parts := strings.SplitN(line, " ", 2)
|
|
c.AuthMethods = strings.Split(parts[0], ",")
|
|
// Check for COOKIEFILE key/value
|
|
if len(parts) == 2 && strings.HasPrefix(parts[1], cookiePrefix) {
|
|
raw := parts[1][len(cookiePrefix):]
|
|
c.CookieFile, err = strconv.Unquote(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Perform GETINFO command based on key.
|
|
func (c *Controller) getInfo(key string) (string, error) {
|
|
_, msg, err := c.makeRequest("GETINFO " + key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
lines := strings.Split(msg, "\n")
|
|
for _, line := range lines {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if parts[0] == key {
|
|
return parts[1], nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf(key + " not found")
|
|
}
|
|
|
|
// Perform GETINFO command and convert response to int.
|
|
func (c *Controller) getInfoInt(key string) (int, error) {
|
|
s, err := c.getInfo(key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return strconv.Atoi(s)
|
|
}
|
|
|
|
// GetAddress returns the current external IP address.
|
|
func (c *Controller) GetAddress() (string, error) {
|
|
return c.getInfo("address")
|
|
}
|
|
|
|
// GetBytesRead returns total bytes downloaded.
|
|
func (c *Controller) GetBytesRead() (int, error) {
|
|
return c.getInfoInt("traffic/read")
|
|
}
|
|
|
|
// GetBytesWritten returns total bytes uploaded.
|
|
func (c *Controller) GetBytesWritten() (int, error) {
|
|
return c.getInfoInt("traffic/written")
|
|
}
|
|
|
|
// GetConfigFile return path to Tor config file.
|
|
func (c *Controller) GetConfigFile() (string, error) {
|
|
return c.getInfo("config-file")
|
|
}
|
|
|
|
// GetTorPid returns PID for current Tor process.
|
|
func (c *Controller) GetTorPid() (int, error) {
|
|
return c.getInfoInt("process/pid")
|
|
}
|
|
|
|
// GetVersion returns version of Tor server.
|
|
func (c *Controller) GetVersion() (string, error) {
|
|
return c.getInfo("version")
|
|
}
|
|
|
|
// AuthenticateNone authenticate to controller without password or cookie.
|
|
func (c *Controller) AuthenticateNone() error {
|
|
_, _, err := c.makeRequest("AUTHENTICATE")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AuthenticatePassword authenticate to controller with password.
|
|
func (c *Controller) AuthenticatePassword(password string) error {
|
|
quoted := strconv.Quote(password)
|
|
_, _, err := c.makeRequest("AUTHENTICATE " + quoted)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AuthenticateCookie authenticate to controller with cookie from current
|
|
// CookieFile path.
|
|
func (c *Controller) AuthenticateCookie() error {
|
|
rawCookie, err := ioutil.ReadFile(c.CookieFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cookie := hex.EncodeToString(rawCookie)
|
|
_, _, err = c.makeRequest("AUTHENTICATE " + cookie)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddOnion adds Onion hidden service. If no private key is supplied one will
|
|
// be generated and the PrivateKeyType and PrivateKey properties will be set
|
|
// with the newly generated one.
|
|
// The hidden service will use port mapping contained in Ports map supplied.
|
|
// ServiceID will be assigned based on the private key and will be the address
|
|
// of this hidden service (without the ".onion" ending).
|
|
func (c *Controller) AddOnion(onion *Onion) error {
|
|
if onion == nil {
|
|
return errors.New("torgo: onion cannot be nil")
|
|
}
|
|
if len(onion.Ports) == 0 {
|
|
return errors.New("torgo: onion requires at least one port mapping")
|
|
}
|
|
req := "ADD_ONION "
|
|
// If no key is supplied set PrivateKeyType to NEW
|
|
if len(onion.PrivateKey) == 0 {
|
|
if onion.PrivateKeyType == "" {
|
|
onion.PrivateKey = "BEST"
|
|
} else {
|
|
onion.PrivateKey = onion.PrivateKeyType
|
|
}
|
|
onion.PrivateKeyType = "NEW"
|
|
}
|
|
req += fmt.Sprintf("%s:%s ", onion.PrivateKeyType, onion.PrivateKey)
|
|
for remotePort, localAddr := range onion.Ports {
|
|
req += fmt.Sprintf("Port=%d,%s ", remotePort, localAddr)
|
|
}
|
|
_, msg, err := c.makeRequest(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines := strings.Split(msg, "\n")
|
|
for _, line := range lines {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if parts[0] == "ServiceID" {
|
|
onion.ServiceID = parts[1]
|
|
} else if parts[0] == "PrivateKey" {
|
|
key := strings.SplitN(parts[1], ":", 2)
|
|
onion.PrivateKeyType = key[0]
|
|
onion.PrivateKey = key[1]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteOnion deletes an onion by its serviceID (stop hidden service created
|
|
// by this controller).
|
|
func (c *Controller) DeleteOnion(serviceID string) error {
|
|
_, _, err := c.makeRequest("DEL_ONION " + serviceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Signal sends a signal to the server. Tor documentations defines the
|
|
// following signals :
|
|
// * RELOAD
|
|
// * SHUTDOWN
|
|
// * DUMP
|
|
// * DEBUG
|
|
// * HALT
|
|
// * CLEARDNSCACHE
|
|
// * NEWNYM
|
|
// * HEARTBEAT
|
|
// * DORMANT
|
|
// * ACTIVE
|
|
func (c *Controller) Signal(signal string) error {
|
|
_, _, err := c.makeRequest("SIGNAL " + signal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|