package tor import ( "crypto/rand" "crypto/sha1" "encoding/hex" "fmt" "io" "io/ioutil" "strings" ) type TorLogLevel string const TorLogLevelDebug TorLogLevel = "debug" const TorLogLevelNotice TorLogLevel = "notice" const TorLogLevelInfo TorLogLevel = "info" const TorLogLevelWarn TorLogLevel = "warn" const TorLogLevelErr TorLogLevel = "err" // TorrcBuilder is a a helper for building torrc files type TorrcBuilder struct { lines []string } // NewTorrc creates a new torrc builder func NewTorrc() *TorrcBuilder { return &TorrcBuilder{ lines: []string{}, } } // WithSocksPort sets the SOCKS port of the tor process func (tb *TorrcBuilder) WithSocksPort(port int) *TorrcBuilder { tb.lines = append(tb.lines, fmt.Sprintf("SocksPort %v", port)) return tb } // WithControlPort sets the control port of the tor process func (tb *TorrcBuilder) WithControlPort(port int) *TorrcBuilder { tb.lines = append(tb.lines, fmt.Sprintf("ControlPort %v", port)) return tb } // WithLog sets the Log to file directive to the specified file with the specified log level func (tb *TorrcBuilder) WithLog(logfile string, level TorLogLevel) *TorrcBuilder { tb.lines = append(tb.lines, fmt.Sprintf("Log %v file %v", level, logfile)) return tb } func (tb *TorrcBuilder) WithSocksTimeout(timeOutSecs int) *TorrcBuilder { tb.lines = append(tb.lines, fmt.Sprintf("SocksTimeout %v", timeOutSecs)) return tb } // IncludeCustom appends to the torrc builder and allows the client to set any option they want, while benefiting // from other configuration options. func (tb *TorrcBuilder) IncludeCustom(lines []string) *TorrcBuilder { tb.lines = append(tb.lines, lines...) return tb } // WithCustom clobbers the torrc builder and allows the client to set any option they want, while benefiting // from other configuration options. func (tb *TorrcBuilder) WithCustom(lines []string) *TorrcBuilder { tb.lines = lines return tb } // WithOnionTrafficOnly ensures that the tor process only routes tor onion traffic. func (tb *TorrcBuilder) WithOnionTrafficOnly() *TorrcBuilder { for i, line := range tb.lines { if strings.HasPrefix(line, "SocksPort") { tb.lines[i] = fmt.Sprintf("%v OnionTrafficOnly", line) return tb } } return tb } // WithHashedPassword sets a password for the control port. func (tb *TorrcBuilder) WithHashedPassword(password string) *TorrcBuilder { var salt [8]byte if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil { panic("no randomness") } hashedpassword := generateHashedPassword(salt, password) tb.lines = append(tb.lines, fmt.Sprintf("HashedControlPassword %s", hashedpassword)) return tb } // Build finalizes the torrc contents and write a file func (tb *TorrcBuilder) Build(path string) error { return ioutil.WriteFile(path, []byte(strings.Join(tb.lines, "\n")), 0600) } // Preview provides a string representation of the torrc file without writing it to a file location. func (tb *TorrcBuilder) Preview() string { return strings.Join(tb.lines, "\n") } // GenerateHashedPassword calculates a hash in the same way tha tor --hash-password does // this function takes a salt as input which is not great from an api-misuse perspective, but // we make it private. func generateHashedPassword(salt [8]byte, password string) string { c := 96 count := (16 + (c & 15)) << ((c >> 4) + 6) tmp := append(salt[:], []byte(password)...) slen := len(tmp) d := sha1.New() for count != 0 { if count > slen { d.Write(tmp) count -= slen } else { d.Write(tmp[:count]) count = 0 } } hashed := d.Sum([]byte{}) return fmt.Sprintf("16:%s%s%s", strings.ToUpper(hex.EncodeToString(salt[:])), strings.ToUpper(hex.EncodeToString([]byte{byte(c)})), strings.ToUpper(hex.EncodeToString(hashed))) }