Merge pull request #6829 from cutiful/master

tor: fix parsing replies
This commit is contained in:
Oliver Gugger 2022-08-17 11:54:29 +02:00 committed by GitHub
commit 909ba573ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 112 additions and 12 deletions

View File

@ -25,6 +25,10 @@ transaction](https://github.com/lightningnetwork/lnd/pull/6730).
* [The macaroon key store implementation was refactored to be more generally * [The macaroon key store implementation was refactored to be more generally
usable](https://github.com/lightningnetwork/lnd/pull/6509). usable](https://github.com/lightningnetwork/lnd/pull/6509).
* [Fixed a bug where cookie authentication with Tor would fail if the cookie
path contained spaces](https://github.com/lightningnetwork/lnd/pull/6829).
Now it parses Tor control port messages correctly.
## `lncli` ## `lncli`
* [Add an `insecure` flag to skip tls auth as well as a `metadata` string slice * [Add an `insecure` flag to skip tls auth as well as a `metadata` string slice
flag](https://github.com/lightningnetwork/lnd/pull/6818) that allows the flag](https://github.com/lightningnetwork/lnd/pull/6818) that allows the
@ -41,6 +45,7 @@ transaction](https://github.com/lightningnetwork/lnd/pull/6730).
# Contributors (Alphabetical Order) # Contributors (Alphabetical Order)
* Carla Kirk-Cohen * Carla Kirk-Cohen
* cutiful
* Daniel McNally * Daniel McNally
* Elle Mouton * Elle Mouton
* ErikEk * ErikEk

View File

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/textproto" "net/textproto"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -77,6 +78,17 @@ var (
// errTCNotStarted is used when we require the tor controller to be // errTCNotStarted is used when we require the tor controller to be
// not stopped while it is. // not stopped while it is.
errTCStopped = errors.New("tor controller must not be stopped") errTCStopped = errors.New("tor controller must not be stopped")
// replyFieldRegexp is the regular expression used to find fields in a
// reply. Parameters within a reply should be of the form KEY=VALUE or
// KEY="VALUE", where quoted values might contain spaces, newlines and
// quoted pairs. If the parameter doesn't contain "=", then we can
// assume it doesn't provide any relevant information that isn't already
// known. Read more on this topic:
// https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n188
replyFieldRegexp = regexp.MustCompile(
`[^" \r\n]+=(?:"(?:[^"\\]|\\[\0-\x7F])*"|[^" \r\n]*)`,
)
) )
// Controller is an implementation of the Tor Control protocol. This is used in // Controller is an implementation of the Tor Control protocol. This is used in
@ -367,29 +379,51 @@ func (c *Controller) readResponse(expected int) (int, string, error) {
return code, reply, nil return code, reply, nil
} }
// unescapeValue removes escape codes from the value in the Tor reply. A
// backslash followed by any character represents that character, so we remove
// any backslash not preceded by another backslash.
func unescapeValue(value string) string {
newString := ""
justRemovedBackslash := false
for _, char := range value {
if char == '\\' && !justRemovedBackslash {
justRemovedBackslash = true
continue
}
newString += string(char)
justRemovedBackslash = false
}
return newString
}
// parseTorReply parses the reply from the Tor server after receiving a command // parseTorReply parses the reply from the Tor server after receiving a command
// from a controller. This will parse the relevant reply parameters into a map // from a controller. This will parse the relevant reply parameters into a map
// of keys and values. // of keys and values.
func parseTorReply(reply string) map[string]string { func parseTorReply(reply string) map[string]string {
params := make(map[string]string) params := make(map[string]string)
// Replies can either span single or multiple lines, so we'll default // Find all fields of a reply. The -1 indicates that we want this to
// to stripping whitespace and newlines in order to retrieve the // find all instances of the regexp.
// individual contents of it. The -1 indicates that we want this to span contents := replyFieldRegexp.FindAllString(reply, -1)
// across all instances of a newline.
contents := strings.Split(strings.Replace(reply, "\n", " ", -1), " ")
for _, content := range contents { for _, content := range contents {
// Each parameter within the reply should be of the form // Each parameter within the reply should be of the form
// "KEY=VALUE". If the parameter doesn't contain "=", then we // KEY=VALUE or KEY="VALUE".
// can assume it does not provide any other relevant information
// already known.
keyValue := strings.SplitN(content, "=", 2) keyValue := strings.SplitN(content, "=", 2)
if len(keyValue) != 2 {
continue
}
key := keyValue[0] key := keyValue[0]
value := keyValue[1] value := keyValue[1]
// Quoted strings need extra processing.
if strings.HasPrefix(value, `"`) {
// Remove quotes around the value.
value = value[1 : len(value)-1]
// Unescape the value.
value = unescapeValue(value)
}
params[key] = value params[key] = value
} }

View File

@ -359,3 +359,64 @@ func TestReconnectSucceed(t *testing.T) {
// Check that the connection has been updated. // Check that the connection has been updated.
require.NotEqual(t, proxy.clientConn, c.conn) require.NotEqual(t, proxy.clientConn, c.conn)
} }
// TestParseTorReply tests that Tor replies are parsed correctly.
func TestParseTorReply(t *testing.T) {
testCase := []struct {
reply string
expectedParams map[string]string
}{
{
// Test a regular reply.
reply: `VERSION Tor="0.4.7.8"`,
expectedParams: map[string]string{
"Tor": "0.4.7.8",
},
},
{
// Test a reply with multiple values, one of them
// containing spaces.
reply: `AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD` +
` COOKIEFILE="/path/with/spaces/Tor Browser/c` +
`ontrol_auth_cookie"`,
expectedParams: map[string]string{
"METHODS": "COOKIE,SAFECOOKIE,HASHEDPASSWORD",
"COOKIEFILE": "/path/with/spaces/Tor Browser/" +
"control_auth_cookie",
},
},
{
// Test a multiline reply.
reply: "ServiceID=id\r\nOK",
expectedParams: map[string]string{"ServiceID": "id"},
},
{
// Test a reply with invalid parameters.
reply: "AUTH =invalid",
expectedParams: map[string]string{},
},
{
// Test escaping arbitrary characters.
reply: `PARAM="esca\ped \"doub\lequotes\""`,
expectedParams: map[string]string{
`PARAM`: `escaped "doublequotes"`,
},
},
{
// Test escaping backslashes. Each single backslash
// should be removed, each double backslash replaced
// with a single one. Note that the single backslash
// before the space escapes the space character, so
// there's two spaces in a row.
reply: `PARAM="escaped \\ \ \\\\"`,
expectedParams: map[string]string{
`PARAM`: `escaped \ \\`,
},
},
}
for _, tc := range testCase {
params := parseTorReply(tc.reply)
require.Equal(t, tc.expectedParams, params)
}
}