Пример #1
0
        /// <summary>Connects to Tor control using a TCP client or throws <see cref="TorControlException"/>.</summary>
        /// <exception cref="TorControlException">When authentication fails for some reason.</exception>
        /// <seealso href="https://gitweb.torproject.org/torspec.git/tree/control-spec.txt">This method follows instructions in 3.23. TAKEOWNERSHIP.</seealso>
        private async Task <TorControlClient> InitTorControlAsync(CancellationToken token = default)
        {
            // Get cookie.
            string cookieString = ByteHelpers.ToHex(File.ReadAllBytes(Settings.CookieAuthFilePath));

            // Authenticate.
            TorControlClientFactory factory = new();
            TorControlClient        client  = await factory.ConnectAndAuthenticateAsync(Settings.ControlEndpoint, cookieString, token).ConfigureAwait(false);

            if (Settings.TerminateOnExit)
            {
                // This is necessary for the scenario when Tor was started by a previous WW instance with TerminateTorOnExit=false configuration option.
                TorControlReply takeReply = await client.TakeOwnershipAsync(token).ConfigureAwait(false);

                if (!takeReply)
                {
                    throw new TorControlException($"Failed to take ownership of the Tor instance. Reply: '{takeReply}'.");
                }

                TorControlReply resetReply = await client.ResetOwningControllerProcessConfAsync(token).ConfigureAwait(false);

                if (!resetReply)
                {
                    throw new TorControlException($"Failed to reset __OwningControllerProcess. Reply: '{resetReply}'.");
                }
            }

            return(client);
        }
Пример #2
0
    public async Task ParseCircEvent1Async()
    {
        string data = "650 CIRC 5 EXTENDED $51BD782616C3EBA543B0D4EE34D7C1CE1ED2291D~Geodude BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2021-06-10T06:26:32.440036 SOCKS_USERNAME=\"AXNH6A3AVT9863JK4VNWD\" SOCKS_PASSWORD=\"AXNH6A3AVT9863JK4VNWD\"\r\n";

        TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

        CircEvent circEvent = CircEvent.FromReply(rawReply);

        Assert.NotNull(circEvent.CircuitInfo);

        CircuitInfo info = circEvent.CircuitInfo;

        Assert.Equal("5", info.CircuitID);
        Assert.Equal(CircStatus.EXTENDED, info.CircStatus);

        CircPath circPath = Assert.Single(info.CircPaths);

        Assert.Equal("$51BD782616C3EBA543B0D4EE34D7C1CE1ED2291D", circPath.FingerPrint);
        Assert.Equal("Geodude", circPath.Nickname);

        Assert.Equal(new List <BuildFlag>()
        {
            BuildFlag.NEED_CAPACITY
        }, info.BuildFlags);
        Assert.Equal(Purpose.GENERAL, info.Purpose);
        Assert.Equal("2021-06-10T06:26:32.440036", info.TimeCreated);
        Assert.Equal("AXNH6A3AVT9863JK4VNWD", info.UserName);
        Assert.Equal("AXNH6A3AVT9863JK4VNWD", info.UserPassword);
    }
Пример #3
0
        public async Task ParseUnknownEventAsync()
        {
            // "XXX" is not a valid event name.
            string data = "650 XXX NOTICE\r\n";

            TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

            _ = Assert.Throws <NotSupportedException>(() => AsyncEventParser.Parse(rawReply));
        }
Пример #4
0
        public async Task EscapingQuotesTestAsync()
        {
            TorControlReply reply = await ParseAsync("250-PROTOCOLINFO 1\r\n250-VERSION Tor=\\\"0.4.3.5\\\"\r\n250 OK\r\n");

            Assert.True(reply.Success);
            Assert.Equal(3, reply.ResponseLines.Count);

            // We are expected to read: 'VERSION Tor=\"0.4.3.5\"' (with the backslashes).
            Assert.Equal(@"VERSION Tor=\""0.4.3.5\""", reply.ResponseLines[1]);
        }
Пример #5
0
    /// <exception cref="TorControlReplyParseException"/>
    public static OrConnEvent FromReply(TorControlReply reply)
    {
        if (reply.StatusCode != StatusCode.AsynchronousEventNotify)
        {
            throw new TorControlReplyParseException($"{nameof(OrConnEvent)}: Expected {StatusCode.AsynchronousEventNotify} status code.");
        }

        (string value, string remainder) = Tokenizer.ReadUntilSeparator(reply.ResponseLines[0]);

        if (value != EventName)
        {
            throw new TorControlReplyParseException($"{nameof(OrConnEvent)}: Expected '{EventName}' event name.");
        }

        string   target;
        OrStatus orStatus = OrStatus.UNKNOWN;
        Reason   reason   = Reason.UNKNOWN;
        int?     nCircs   = null;
        string?  connId   = null;

        // Mandatory piece of information per spec.
        (target, remainder) = Tokenizer.ReadUntilSeparator(remainder);
        (value, remainder)  = Tokenizer.ReadUntilSeparator(remainder);
        orStatus            = Tokenizer.ParseEnumValue(value, OrStatus.UNKNOWN);

        // Optional arguments.
        while (remainder != "")
        {
            string key;
            (key, value, remainder) = Tokenizer.ReadKeyValueAssignment(remainder, allowValueAsQuotedString: true);

            if (key == "REASON")
            {
                reason = Tokenizer.ParseEnumValue(value, Reason.UNKNOWN);
                continue;
            }
            else if (key == "NCIRCS")
            {
                nCircs = int.Parse(value);
                continue;
            }
            else if (key == "ID")
            {
                connId = value;
                continue;
            }

            Logger.LogError($"Failed to handle '{remainder}'.");
            break;
        }

        return(new OrConnEvent(target, orStatus, reason, nCircs, connId));
    }
Пример #6
0
        public async Task ParseSystemClientEventAsync()
        {
            string data = "650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=14 TAG=handshake SUMMARY=\"Handshaking with a relay\"\r\n";

            TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

            IAsyncEvent asyncEvent = AsyncEventParser.Parse(rawReply);

            BootstrapStatusEvent @event = Assert.IsType <BootstrapStatusEvent>(asyncEvent);

            Assert.NotNull(@event);
        }
Пример #7
0
        public async Task WellformedTestsAsync(StatusCode expectedStatusCode, int expectedReplyLines, string data)
        {
            TorControlReply reply = await ParseAsync(data);

            if (expectedStatusCode == StatusCode.OK)
            {
                Assert.True(reply.Success);
            }

            Assert.Equal(expectedStatusCode, reply.StatusCode);
            Assert.Equal(expectedReplyLines, reply.ResponseLines.Count);
        }
Пример #8
0
        public async Task ParseCircEventAsync()
        {
            string data = "650 CIRC 16 LAUNCHED BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2021-06-10T05:42:43.808915\r\n";

            TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

            IAsyncEvent asyncEvent = AsyncEventParser.Parse(rawReply);

            CircEvent @event = Assert.IsType <CircEvent>(asyncEvent);

            Assert.NotNull(@event);
        }
Пример #9
0
    public async Task ParseOrConnEventAsync()
    {
        string data = "650 ORCONN $A1B28D636A56AAFFE92ADCCA937AA4BD5333BB4C~bakunin4 LAUNCHED ID=5\r\n";

        TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

        OrConnEvent orConnEvent = OrConnEvent.FromReply(rawReply);

        Assert.Equal("$A1B28D636A56AAFFE92ADCCA937AA4BD5333BB4C~bakunin4", orConnEvent.Target);
        Assert.Equal(OrStatus.LAUNCHED, orConnEvent.OrStatus);
        Assert.Null(orConnEvent.NCircs);
        Assert.Equal("5", orConnEvent.ConnId);
    }
Пример #10
0
    public async Task AuthMethodHashedPasswordAsync()
    {
        string data = "250-PROTOCOLINFO 1\r\n250-AUTH METHODS=HASHEDPASSWORD\r\n250-VERSION Tor=\"0.4.3.5\"\r\n250 OK\r\n";

        TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

        ProtocolInfoReply reply = ProtocolInfoReply.FromReply(rawReply);

        Assert.Equal(1, reply.ProtocolVersion);
        Assert.Equal("0.4.3.5", reply.TorVersion);
        Assert.Single(reply.AuthMethods);
        Assert.Contains("HASHEDPASSWORD", reply.AuthMethods);
    }
Пример #11
0
        public async Task ParseCircuitEstablishedStatusEventAsync()
        {
            string data = "650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED\r\n";

            TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

            StatusEvent statusEvent = StatusEvent.FromReply(rawReply);

            Assert.Equal(StatusType.STATUS_CLIENT, statusEvent.Type);
            Assert.Equal(StatusSeverity.NOTICE, statusEvent.Severity);
            Assert.Equal("CIRCUIT_ESTABLISHED", statusEvent.Action);
            Assert.Empty(statusEvent.Arguments);
        }
Пример #12
0
        /// <exception cref="TorControlReplyParseException"/>
        public static IAsyncEvent Parse(TorControlReply reply)
        {
            if (reply.StatusCode != StatusCode.AsynchronousEventNotify)
            {
                throw new TorControlReplyParseException($"Event: Expected {StatusCode.AsynchronousEventNotify} status code.");
            }

            (string value, _) = Tokenizer.ReadUntilSeparator(reply.ResponseLines[0]);

            return(value switch
            {
                "STATUS_CLIENT" or "STATUS_SERVER" or "STATUS_GENERAL" => StatusEvent.FromReply(reply),
                "CIRC" => CircEvent.FromReply(reply),
                _ => throw new NotSupportedException("This should never happen."),
            });
Пример #13
0
    public async Task AuthMethodCookieAsync()
    {
        // Yes, Tor really returns: "C:\\Users\\Wasabi\\AppData\\Roaming\\WalletWasabi\\Client\\control_auth_cookie" path on Windows.
        string data = "250-PROTOCOLINFO 1\r\n250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"C:\\\\Users\\\\Wasabi\\\\AppData\\\\Roaming\\\\WalletWasabi\\\\Client\\\\control_auth_cookie\"\r\n250-VERSION Tor=\"0.4.5.7\"\r\n250 OK\r\n";

        TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

        ProtocolInfoReply reply = ProtocolInfoReply.FromReply(rawReply);

        Assert.Equal(1, reply.ProtocolVersion);
        Assert.Equal("0.4.5.7", reply.TorVersion);
        Assert.Equal(2, reply.AuthMethods.Length);
        Assert.Equal("COOKIE", reply.AuthMethods[0]);
        Assert.Equal("SAFECOOKIE", reply.AuthMethods[1]);
        Assert.Equal(@"C:\Users\Wasabi\AppData\Roaming\WalletWasabi\Client\control_auth_cookie", reply.CookieFilePath);
    }
Пример #14
0
        public async Task ParseBootstrapStatusEvent1Async()
        {
            string data = "650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=14 TAG=handshake SUMMARY=\"Handshaking with a relay\"\r\n";

            TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

            StatusEvent statusEvent = StatusEvent.FromReply(rawReply);

            Assert.Equal(StatusType.STATUS_CLIENT, statusEvent.Type);
            Assert.Equal(StatusSeverity.NOTICE, statusEvent.Severity);
            Assert.Equal("BOOTSTRAP", statusEvent.Action);
            Assert.Equal(3, statusEvent.Arguments.Count);
            Assert.Equal("14", statusEvent.Arguments["PROGRESS"]);
            Assert.Equal("handshake", statusEvent.Arguments["TAG"]);
            Assert.Equal("Handshaking with a relay", statusEvent.Arguments["SUMMARY"]);
        }
Пример #15
0
        public async Task ParseBootstrapStatusEvent2Async()
        {
            string data = "650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=95 TAG=circuit_create SUMMARY=\"Establishing a Tor circuit\"\r\n";

            TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

            StatusEvent statusEvent = StatusEvent.FromReply(rawReply);

            Assert.Equal(StatusType.STATUS_CLIENT, statusEvent.Type);
            Assert.Equal(StatusSeverity.NOTICE, statusEvent.Severity);
            Assert.Equal("BOOTSTRAP", statusEvent.Action);
            Assert.Equal(3, statusEvent.Arguments.Count);
            Assert.Equal("95", statusEvent.Arguments["PROGRESS"]);
            Assert.Equal("circuit_create", statusEvent.Arguments["TAG"]);
            Assert.Equal("Establishing a Tor circuit", statusEvent.Arguments["SUMMARY"]);
        }
Пример #16
0
    /// <exception cref="TorControlReplyParseException"/>
    public static IAsyncEvent Parse(TorControlReply reply)
    {
        if (reply.StatusCode != StatusCode.AsynchronousEventNotify)
        {
            throw new TorControlReplyParseException($"Event: Expected {StatusCode.AsynchronousEventNotify} status code.");
        }

        (string value, _) = Tokenizer.ReadUntilSeparator(reply.ResponseLines[0]);

        return(value switch
        {
            StatusEvent.EventNameStatusClient or StatusEvent.EventNameStatusServer or StatusEvent.EventNameStatusGeneral => StatusEvent.FromReply(reply),
            CircEvent.EventName => CircEvent.FromReply(reply),
            NetworkLivenessEvent.EventName => NetworkLivenessEvent.FromReply(reply),
            OrConnEvent.EventName => OrConnEvent.FromReply(reply),
            _ => throw new NotSupportedException("This should never happen."),
        });
Пример #17
0
    public async Task ParseCircEvent2Async()
    {
        string data = "650 CIRC 16 LAUNCHED BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2021-06-10T05:42:43.808915\r\n";

        TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

        CircEvent circEvent = CircEvent.FromReply(rawReply);

        Assert.NotNull(circEvent.CircuitInfo);

        CircuitInfo info = circEvent.CircuitInfo;

        Assert.Equal("16", info.CircuitID);
        Assert.Equal(CircStatus.LAUNCHED, info.CircStatus);
        Assert.Equal(new List <BuildFlag>()
        {
            BuildFlag.NEED_CAPACITY
        }, info.BuildFlags);
        Assert.Equal(Purpose.GENERAL, info.Purpose);
        Assert.Equal("2021-06-10T05:42:43.808915", info.TimeCreated);
    }
    public async Task ParseReplyAsync()
    {
        StringBuilder sb = new();

        sb.Append("250+circuit-status=\r\n");
        sb.Append("1 BUILT $E9F71AC06F29B2110E3FC09016B0E50407444EE2~libertas,$D0423D3A13C18D2ED0F3D5BFD90E13E77C9AD239~d0xkb,$3A9559477D72F71215850C97FA62A0DA7380964B~PawNetBlue BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2021-05-15T14:04:17.615384\r\n");
        sb.Append("2 BUILT $E9F71AC06F29B2110E3FC09016B0E50407444EE2~libertas,$A0FA50A070CFB4B89737A27F3259F92C118A0AF0~pipiska,$7E77CC94D94C08609D70B517FF938CC61C9F8232~pitfall BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2021-05-15T14:04:18.628885\r\n");
        sb.Append("3 BUILT $E9F71AC06F29B2110E3FC09016B0E50407444EE2~libertas,$706A7674A217BA905FE677E82236B7B968A23DB7~rofltor04,$4D4938B725B89561773A161215D88B7C45C43C35~TheGreenDynamo,$18CA339AD0C33EAB035F1D869518F3D2D88BABC0~FreeAssange BUILD_FLAGS=IS_INTERNAL,NEED_CAPACITY PURPOSE=HS_CLIENT_HSDIR HS_STATE=HSCI_CONNECTING TIME_CREATED=2021-05-15T14:04:19.353271\r\n");
        sb.Append("4 EXTENDED $E9F71AC06F29B2110E3FC09016B0E50407444EE2~libertas BUILD_FLAGS=IS_INTERNAL,NEED_CAPACITY PURPOSE=MEASURE_TIMEOUT TIME_CREATED=2021-05-15T14:04:19.631228\r\n");
        sb.Append("5 BUILT $E9F71AC06F29B2110E3FC09016B0E50407444EE2~libertas,$31D270A38505D4BFBBCABF717E9FB4BCA6DDF2FF~Belgium,$B411027C926A9BFFCF7DA91E3CAF1856A321EFFD~pulsetor BUILD_FLAGS=IS_INTERNAL,NEED_CAPACITY PURPOSE=HS_CLIENT_REND HS_STATE=HSCR_JOINED REND_QUERY=wasabiukrxmkdgve5kynjztuovbg43uxcbcxn6y2okcrsg7gb6jdmbad TIME_CREATED=2021-05-15T14:04:20.634686\r\n");
        sb.Append(".\r\n");
        sb.Append("250 OK\r\n");

        string data = sb.ToString();

        TorControlReply rawReply = await TorControlReplyReaderTest.ParseAsync(data);

        Assert.Equal(8, rawReply.ResponseLines.Count);

        GetInfoCircuitStatusReply reply = GetInfoCircuitStatusReply.FromReply(rawReply);

        Assert.Equal(5, reply.Circuits.Count);

        // Circuit #1.
        {
            CircuitInfo circuitInfo = reply.Circuits[0];

            Assert.Equal("1", circuitInfo.CircuitID);
            Assert.Equal(CircStatus.BUILT, circuitInfo.CircStatus);

            List <CircPath> circPaths = circuitInfo.CircPaths;
            Assert.Equal("$E9F71AC06F29B2110E3FC09016B0E50407444EE2", circPaths[0].FingerPrint);
            Assert.Equal("libertas", circPaths[0].Nickname);

            Assert.Equal("$D0423D3A13C18D2ED0F3D5BFD90E13E77C9AD239", circPaths[1].FingerPrint);
            Assert.Equal("d0xkb", circPaths[1].Nickname);

            Assert.Equal("$3A9559477D72F71215850C97FA62A0DA7380964B", circPaths[2].FingerPrint);
            Assert.Equal("PawNetBlue", circPaths[2].Nickname);

            BuildFlag buildFlag = Assert.Single(circuitInfo.BuildFlags);
            Assert.Equal(BuildFlag.NEED_CAPACITY, buildFlag);

            Assert.Equal(Purpose.GENERAL, circuitInfo.Purpose);
            Assert.Equal("2021-05-15T14:04:17.615384", circuitInfo.TimeCreated);

            Assert.Null(circuitInfo.Reason);
            Assert.Null(circuitInfo.RemoteReason);
            Assert.Null(circuitInfo.HsState);
            Assert.Null(circuitInfo.RendQuery);
            Assert.Null(circuitInfo.UserName);
            Assert.Null(circuitInfo.UserPassword);
        }

        // Circuit #2.
        {
            CircuitInfo circuitInfo = reply.Circuits[1];

            Assert.Equal("2", circuitInfo.CircuitID);
            Assert.Equal(CircStatus.BUILT, circuitInfo.CircStatus);

            List <CircPath> circPaths = circuitInfo.CircPaths;
            Assert.Equal("$E9F71AC06F29B2110E3FC09016B0E50407444EE2", circPaths[0].FingerPrint);
            Assert.Equal("libertas", circPaths[0].Nickname);

            Assert.Equal("$A0FA50A070CFB4B89737A27F3259F92C118A0AF0", circPaths[1].FingerPrint);
            Assert.Equal("pipiska", circPaths[1].Nickname);

            Assert.Equal("$7E77CC94D94C08609D70B517FF938CC61C9F8232", circPaths[2].FingerPrint);
            Assert.Equal("pitfall", circPaths[2].Nickname);

            BuildFlag buildFlag = Assert.Single(circuitInfo.BuildFlags);
            Assert.Equal(BuildFlag.NEED_CAPACITY, buildFlag);

            Assert.Equal(Purpose.GENERAL, circuitInfo.Purpose);
            Assert.Equal("2021-05-15T14:04:18.628885", circuitInfo.TimeCreated);

            Assert.Null(circuitInfo.Reason);
            Assert.Null(circuitInfo.RemoteReason);
            Assert.Null(circuitInfo.HsState);
            Assert.Null(circuitInfo.RendQuery);
            Assert.Null(circuitInfo.UserName);
            Assert.Null(circuitInfo.UserPassword);
        }

        // Circuit #3.
        {
            CircuitInfo circuitInfo = reply.Circuits[2];

            Assert.Equal("3", circuitInfo.CircuitID);
            Assert.Equal(CircStatus.BUILT, circuitInfo.CircStatus);

            List <CircPath> circPaths = circuitInfo.CircPaths;
            Assert.Equal("$E9F71AC06F29B2110E3FC09016B0E50407444EE2", circPaths[0].FingerPrint);
            Assert.Equal("libertas", circPaths[0].Nickname);

            Assert.Equal("$706A7674A217BA905FE677E82236B7B968A23DB7", circPaths[1].FingerPrint);
            Assert.Equal("rofltor04", circPaths[1].Nickname);

            Assert.Equal("$4D4938B725B89561773A161215D88B7C45C43C35", circPaths[2].FingerPrint);
            Assert.Equal("TheGreenDynamo", circPaths[2].Nickname);

            Assert.Equal("$18CA339AD0C33EAB035F1D869518F3D2D88BABC0", circPaths[3].FingerPrint);
            Assert.Equal("FreeAssange", circPaths[3].Nickname);

            Assert.Equal(2, circuitInfo.BuildFlags.Count);
            Assert.Equal(BuildFlag.IS_INTERNAL, circuitInfo.BuildFlags[0]);
            Assert.Equal(BuildFlag.NEED_CAPACITY, circuitInfo.BuildFlags[1]);

            Assert.Equal(Purpose.HS_CLIENT_HSDIR, circuitInfo.Purpose);
            Assert.Equal(HsState.HSCI_CONNECTING, circuitInfo.HsState);
            Assert.Equal("2021-05-15T14:04:19.353271", circuitInfo.TimeCreated);

            Assert.Null(circuitInfo.Reason);
            Assert.Null(circuitInfo.RemoteReason);
            Assert.Null(circuitInfo.RendQuery);
            Assert.Null(circuitInfo.UserName);
            Assert.Null(circuitInfo.UserPassword);
        }

        // Circuit #4.
        {
            CircuitInfo circuitInfo = reply.Circuits[3];

            Assert.Equal("4", circuitInfo.CircuitID);
            Assert.Equal(CircStatus.EXTENDED, circuitInfo.CircStatus);

            List <CircPath> circPaths = circuitInfo.CircPaths;
            Assert.Equal("$E9F71AC06F29B2110E3FC09016B0E50407444EE2", circPaths[0].FingerPrint);
            Assert.Equal("libertas", circPaths[0].Nickname);

            Assert.Equal(2, circuitInfo.BuildFlags.Count);
            Assert.Equal(BuildFlag.IS_INTERNAL, circuitInfo.BuildFlags[0]);
            Assert.Equal(BuildFlag.NEED_CAPACITY, circuitInfo.BuildFlags[1]);
            Assert.Equal(Purpose.MEASURE_TIMEOUT, circuitInfo.Purpose);
            Assert.Equal("2021-05-15T14:04:19.631228", circuitInfo.TimeCreated);

            Assert.Null(circuitInfo.HsState);
            Assert.Null(circuitInfo.Reason);
            Assert.Null(circuitInfo.RemoteReason);
            Assert.Null(circuitInfo.RendQuery);
            Assert.Null(circuitInfo.UserName);
            Assert.Null(circuitInfo.UserPassword);
        }

        // Circuit #5.
        {
            CircuitInfo circuitInfo = reply.Circuits[4];

            Assert.Equal("5", circuitInfo.CircuitID);
            Assert.Equal(CircStatus.BUILT, circuitInfo.CircStatus);

            List <CircPath> circPaths = circuitInfo.CircPaths;
            Assert.Equal("$E9F71AC06F29B2110E3FC09016B0E50407444EE2", circPaths[0].FingerPrint);
            Assert.Equal("libertas", circPaths[0].Nickname);

            Assert.Equal("$31D270A38505D4BFBBCABF717E9FB4BCA6DDF2FF", circPaths[1].FingerPrint);
            Assert.Equal("Belgium", circPaths[1].Nickname);

            Assert.Equal("$B411027C926A9BFFCF7DA91E3CAF1856A321EFFD", circPaths[2].FingerPrint);
            Assert.Equal("pulsetor", circPaths[2].Nickname);

            Assert.Equal(2, circuitInfo.BuildFlags.Count);
            Assert.Equal(BuildFlag.IS_INTERNAL, circuitInfo.BuildFlags[0]);
            Assert.Equal(BuildFlag.NEED_CAPACITY, circuitInfo.BuildFlags[1]);
            Assert.Equal(Purpose.HS_CLIENT_REND, circuitInfo.Purpose);
            Assert.Equal(HsState.HSCR_JOINED, circuitInfo.HsState);
            Assert.Equal("wasabiukrxmkdgve5kynjztuovbg43uxcbcxn6y2okcrsg7gb6jdmbad", circuitInfo.RendQuery);
            Assert.Equal("2021-05-15T14:04:20.634686", circuitInfo.TimeCreated);

            Assert.Null(circuitInfo.Reason);
            Assert.Null(circuitInfo.RemoteReason);
            Assert.Null(circuitInfo.UserName);
            Assert.Null(circuitInfo.UserPassword);
        }
    }
    public async Task ReceivingMixOfSyncAndAsyncMessageFromTorControlAsync()
    {
        using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(120));

        // Test parameters.
        const string AsyncEventContent = "CIRC 1000 EXTENDED moria1,moria2";

        Pipe toServer = new();
        Pipe toClient = new();

        // Set up Tor control client.
        await using TorControlClient client = new(pipeReader : toClient.Reader, pipeWriter : toServer.Writer);

        // Subscribe to Tor events.
        IAsyncEnumerable <TorControlReply> events           = client.ReadEventsAsync(timeoutCts.Token);
        IAsyncEnumerator <TorControlReply> eventsEnumerator = events.GetAsyncEnumerator();
        ValueTask <bool> firstReplyTask = eventsEnumerator.MoveNextAsync();

        Task serverTask = Task.Run(async() =>
        {
            Logger.LogTrace($"Server: Send msg #1 (async) to client: '650 {AsyncEventContent}'.");
            await toClient.Writer.WriteAsciiAndFlushAsync($"650 {AsyncEventContent}\r\n", timeoutCts.Token).ConfigureAwait(false);

            Logger.LogTrace($"Server: Send msg #2 (async) to client: '650 {AsyncEventContent}'.");
            await toClient.Writer.WriteAsciiAndFlushAsync($"650 {AsyncEventContent}\r\n", timeoutCts.Token).ConfigureAwait(false);

            Logger.LogTrace("Server: Wait for TAKEOWNERSHIP command.");
            string command = await toServer.Reader.ReadLineAsync(timeoutCts.Token).ConfigureAwait(false);
            Assert.Equal("TAKEOWNERSHIP", command);

            Logger.LogTrace("Server: Send msg #3 (sync) to client in response to TAKEOWNERSHIP command.");
            await toClient.Writer.WriteAsciiAndFlushAsync($"250 OK\r\n", timeoutCts.Token).ConfigureAwait(false);

            Logger.LogTrace($"Server: Send msg #4 (async) to client: '650 {AsyncEventContent}'.");
            await toClient.Writer.WriteAsciiAndFlushAsync($"650 {AsyncEventContent}\r\n", timeoutCts.Token).ConfigureAwait(false);
        });

        Logger.LogTrace("Client: Receive msg #1 (async).");
        {
            await firstReplyTask.AsTask().WithAwaitCancellationAsync(timeoutCts.Token);

            TorControlReply receivedEvent1 = eventsEnumerator.Current;
            Assert.Equal(StatusCode.AsynchronousEventNotify, receivedEvent1.StatusCode);
            string line = Assert.Single(receivedEvent1.ResponseLines);
            Assert.Equal(AsyncEventContent, line);
        }

        // Msg #2 is sent before expected reply (msg #3).
        TorControlReply takeOwnershipReply = await client.TakeOwnershipAsync(timeoutCts.Token);

        Assert.True(takeOwnershipReply.Success);

        Logger.LogTrace("Client: Receive msg #2 (async).");
        {
            Assert.True(await eventsEnumerator.MoveNextAsync());
            TorControlReply receivedEvent2 = eventsEnumerator.Current;
            Assert.Equal(StatusCode.AsynchronousEventNotify, receivedEvent2.StatusCode);
            string line = Assert.Single(receivedEvent2.ResponseLines);
            Assert.Equal(AsyncEventContent, line);
        }

        Logger.LogTrace("Client: Receive msg #4 (async) - i.e. third async event.");
        {
            Assert.True(await eventsEnumerator.MoveNextAsync());
            TorControlReply receivedEvent3 = eventsEnumerator.Current;
            Assert.Equal(StatusCode.AsynchronousEventNotify, receivedEvent3.StatusCode);
            string line = Assert.Single(receivedEvent3.ResponseLines);
            Assert.Equal(AsyncEventContent, line);
        }

        // Client decided to stop reading Tor async events.
        timeoutCts.Cancel();

        // No more async events.
        _ = Assert.ThrowsAsync <OperationCanceledException>(async() => await eventsEnumerator.MoveNextAsync());
    }