123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- package tor
- import (
- "net"
- "net/textproto"
- "testing"
- "github.com/stretchr/testify/require"
- )
- // TestParseTorVersion is a series of tests for different version strings that
- // check the correctness of determining whether they support creating v3 onion
- // services through Tor control's port.
- func TestParseTorVersion(t *testing.T) {
- t.Parallel()
- tests := []struct {
- version string
- valid bool
- }{
- {
- version: "0.3.3.6",
- valid: true,
- },
- {
- version: "0.3.3.7",
- valid: true,
- },
- {
- version: "0.3.4.6",
- valid: true,
- },
- {
- version: "0.4.3.6",
- valid: true,
- },
- {
- version: "0.4.0.5",
- valid: true,
- },
- {
- version: "1.3.3.6",
- valid: true,
- },
- {
- version: "0.3.3.6-rc",
- valid: true,
- },
- {
- version: "0.3.3.7-rc",
- valid: true,
- },
- {
- version: "0.3.3.5-rc",
- valid: false,
- },
- {
- version: "0.3.3.5",
- valid: false,
- },
- {
- version: "0.3.2.6",
- valid: false,
- },
- {
- version: "0.1.3.6",
- valid: false,
- },
- {
- version: "0.0.6.3",
- valid: false,
- },
- }
- for i, test := range tests {
- err := supportsV3(test.version)
- if test.valid != (err == nil) {
- t.Fatalf("test %d with version string %v failed: %v", i,
- test.version, err)
- }
- }
- }
- // testProxy emulates a Tor daemon and contains the info used for the tor
- // controller to make connections.
- type testProxy struct {
- // server is the proxy listener.
- server net.Listener
- // serverConn is the established connection from the server side.
- serverConn net.Conn
- // serverAddr is the tcp address the proxy is listening on.
- serverAddr string
- // clientConn is the established connection from the client side.
- clientConn *textproto.Conn
- }
- // cleanUp is used after each test to properly close the ports/connections.
- func (tp *testProxy) cleanUp() {
- // Don't bother cleaning if there's no a server created.
- if tp.server == nil {
- return
- }
- if err := tp.clientConn.Close(); err != nil {
- log.Errorf("closing client conn got err: %v", err)
- }
- if err := tp.server.Close(); err != nil {
- log.Errorf("closing proxy server got err: %v", err)
- }
- }
- // createTestProxy creates a proxy server to listen on a random address,
- // creates a server and a client connection, and initializes a testProxy using
- // these params.
- func createTestProxy(t *testing.T) *testProxy {
- // Set up the proxy to listen on given port.
- //
- // NOTE: we use a port 0 here to indicate we want a free port selected
- // by the system.
- proxy, err := net.Listen("tcp", ":0")
- require.NoError(t, err, "failed to create proxy")
- t.Logf("created proxy server to listen on address: %v", proxy.Addr())
- // Accept the connection inside a goroutine.
- serverChan := make(chan net.Conn, 1)
- go func(result chan net.Conn) {
- conn, err := proxy.Accept()
- require.NoError(t, err, "failed to accept")
- result <- conn
- }(serverChan)
- // Create the connection using tor controller.
- client, err := textproto.Dial("tcp", proxy.Addr().String())
- require.NoError(t, err, "failed to create connection")
- tc := &testProxy{
- server: proxy,
- serverConn: <-serverChan,
- serverAddr: proxy.Addr().String(),
- clientConn: client,
- }
- return tc
- }
- // TestReadResponse constructs a series of possible responses returned by Tor
- // and asserts the readResponse can handle them correctly.
- func TestReadResponse(t *testing.T) {
- // Create mock server and client connection.
- proxy := createTestProxy(t)
- t.Cleanup(proxy.cleanUp)
- server := proxy.serverConn
- // Create a dummy tor controller.
- c := &Controller{conn: proxy.clientConn}
- testCase := []struct {
- name string
- serverResp string
- // expectedReply is the reply we expect the readResponse to
- // return.
- expectedReply string
- // expectedCode is the code we expect the server to return.
- expectedCode int
- // returnedCode is the code we expect the readResponse to
- // return.
- returnedCode int
- // expectedErr is the error we expect the readResponse to
- // return.
- expectedErr error
- }{
- {
- // Test a simple response.
- name: "succeed on 250",
- serverResp: "250 OK\n",
- expectedReply: "OK",
- expectedCode: 250,
- returnedCode: 250,
- expectedErr: nil,
- },
- {
- // Test a mid reply(-) response.
- name: "succeed on mid reply line",
- serverResp: "250-field=value\n" +
- "250 OK\n",
- expectedReply: "field=value\nOK",
- expectedCode: 250,
- returnedCode: 250,
- expectedErr: nil,
- },
- {
- // Test a data reply(+) response.
- name: "succeed on data reply line",
- serverResp: "250+field=\n" +
- "line1\n" +
- "line2\n" +
- ".\n" +
- "250 OK\n",
- expectedReply: "field=line1,line2\nOK",
- expectedCode: 250,
- returnedCode: 250,
- expectedErr: nil,
- },
- {
- // Test a mixed reply response.
- name: "succeed on mixed reply line",
- serverResp: "250-field=value\n" +
- "250+field=\n" +
- "line1\n" +
- "line2\n" +
- ".\n" +
- "250 OK\n",
- expectedReply: "field=value\nfield=line1,line2\nOK",
- expectedCode: 250,
- returnedCode: 250,
- expectedErr: nil,
- },
- {
- // Test unexpected code.
- name: "fail on codes not matched",
- serverResp: "250 ERR\n",
- expectedReply: "ERR",
- expectedCode: 500,
- returnedCode: 250,
- expectedErr: errCodeNotMatch,
- },
- {
- // Test short response error.
- name: "fail on short response",
- serverResp: "123\n250 OK\n",
- expectedReply: "",
- expectedCode: 250,
- returnedCode: 0,
- expectedErr: textproto.ProtocolError(
- "short line: 123"),
- },
- {
- // Test short response error.
- name: "fail on invalid response",
- serverResp: "250?OK\n",
- expectedReply: "",
- expectedCode: 250,
- returnedCode: 250,
- expectedErr: textproto.ProtocolError(
- "invalid line: 250?OK"),
- },
- }
- for _, tc := range testCase {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- // Let the server mocks a given response.
- _, err := server.Write([]byte(tc.serverResp))
- require.NoError(t, err, "server failed to write")
- // Read the response and checks all expectations
- // satisfied.
- code, reply, err := c.readResponse(tc.expectedCode)
- require.Equal(t, tc.expectedErr, err)
- require.Equal(t, tc.returnedCode, code)
- require.Equal(t, tc.expectedReply, reply)
- // Check that the read buffer is cleaned.
- require.Zero(t, c.conn.R.Buffered(),
- "read buffer not empty")
- })
- }
- }
- // TestReconnectTCMustBeRunning checks that the tor controller must be running
- // while calling Reconnect.
- func TestReconnectTCMustBeRunning(t *testing.T) {
- // Create a dummy controller.
- c := &Controller{}
- // Reconnect should fail because the TC is not started.
- require.Equal(t, errTCNotStarted, c.Reconnect())
- // Set the started flag.
- c.started = 1
- // Set the stopped flag so the TC is stopped.
- c.stopped = 1
- // Reconnect should fail because the TC is stopped.
- require.Equal(t, errTCStopped, c.Reconnect())
- }
- // TestReconnectSucceed tests a reconnection will succeed when the tor
- // controller is up and running.
- func TestReconnectSucceed(t *testing.T) {
- // Create mock server and client connection.
- proxy := createTestProxy(t)
- t.Cleanup(proxy.cleanUp)
- // Create a tor controller and mark the controller as started.
- c := &Controller{
- conn: proxy.clientConn,
- started: 1,
- controlAddr: proxy.serverAddr,
- }
- // Accept the connection inside a goroutine. We will also write some
- // data so that the reconnection can succeed. We will mock three writes
- // and two reads inside our proxy server,
- // - write protocol info
- // - read auth info
- // - write auth challenge
- // - read auth challenge
- // - write OK
- go func() {
- // Accept the new connection.
- server, err := proxy.server.Accept()
- require.NoError(t, err, "failed to accept")
- // Write the protocol info.
- resp := "250-PROTOCOLINFO 1\n" +
- "250-AUTH METHODS=NULL\n" +
- "250 OK\n"
- _, err = server.Write([]byte(resp))
- require.NoErrorf(t, err, "failed to write protocol info")
- // Read the auth info from the client.
- buf := make([]byte, 65535)
- _, err = server.Read(buf)
- require.NoError(t, err)
- // Write the auth challenge.
- resp = "250 AUTHCHALLENGE SERVERHASH=fake\n"
- _, err = server.Write([]byte(resp))
- require.NoErrorf(t, err, "failed to write auth challenge")
- // Read the auth challenge resp from the client.
- _, err = server.Read(buf)
- require.NoError(t, err)
- // Write OK resp.
- resp = "250 OK\n"
- _, err = server.Write([]byte(resp))
- require.NoErrorf(t, err, "failed to write response auth")
- }()
- // Reconnect should succeed.
- require.NoError(t, c.Reconnect())
- // Check that the old connection is closed.
- _, err := proxy.clientConn.ReadLine()
- require.Contains(t, err.Error(), "use of closed network connection")
- // Check that the connection has been updated.
- 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)
- }
- }
|