예제 #1
0
        private HostConnectResult FollowHostConnectProtocol()
        {
            State = HostConnectClientState.Connecting;
            StateChanged(this, new StateChangedEventArgs(null, State));

            Uri masterServerUri = ServiceWrapper.settings.GetMasterServerUri();

            // Ensure the certificate exists before we connect, because it can take a moment to create and we don't want the connection to time out.
            IdentityVerification.EnsureClientCertificateExists();

            tcpClient = new TcpClient();
            KeepAliveSender keepAlive = null;

            try
            {
                #region Get Connected
                tcpClient.NoDelay        = true;
                tcpClient.ReceiveTimeout = 30000;
                tcpClient.SendTimeout    = 30000;
                tcpClient.Connect(masterServerUri.DnsSafeHost, masterServerUri.Port);
                stream = tcpClient.GetStream();
                if (masterServerUri.Scheme == "https")
                {
                    try
                    {
                        RemoteCertificateValidationCallback certCallback = null;
                        if (!ServiceWrapper.settings.ValidateServerCertificate)
                        {
                            certCallback = (sender, certificate, chain, sslPolicyErrors) => true;
                        }
                        stream = new SslStream(stream, false, certCallback, null);
                        ((SslStream)stream).AuthenticateAsClient(masterServerUri.DnsSafeHost, null, System.Security.Authentication.SslProtocols.Tls12, ServiceWrapper.settings.ValidateServerCertificate);
                    }
                    catch (ThreadAbortException) { throw; }
                    catch (SocketException) { throw; }
                    catch (Exception ex)
                    {
                        Logger.Debug(ex);
                        return(new HostConnectResult(ProtocolErrors.HttpsNegotiationFailed));
                    }
                }
                State = HostConnectClientState.Authenticating;
                StateChanged(this, new StateChangedEventArgs(null, State));
                {
                    // Create HTTP request.
                    StringBuilder sb = new StringBuilder();
                    sb.Append("POST ").Append(masterServerUri.PathAndQuery).Append("hostconnect HTTP/1.1\r\n");
                    sb.Append("Host: ").Append(masterServerUri.Host).Append("\r\n");
                    sb.Append("Content-Length: 0\r\n");
                    sb.Append("\r\n");
                    byte[] buf = ByteUtil.Utf8NoBOM.GetBytes(sb.ToString());
                    stream.Write(buf, 0, buf.Length);
                }
                #endregion
                #region Authentication Protocol
                // Auth 0) Receive ClientAuthentication command code.
                Command command = (Command)ByteUtil.ReadNBytes(stream, 1)[0];
                if (command != Command.ClientAuthentication)
                {
                    return(new HostConnectResult(ProtocolErrors.AuthResponseCommand));
                }

                // Auth 1) Receive authentication challenge.  This is an array of 32 random bytes which the Host Service must sign with its private key.
                byte[] authChallenge = ByteUtil.ReadNBytes(stream, 32);

                // Auth 2) Build authentication reply.
                // Auth 2.1) Authentication type
                HostAuthenticationType authType = HostAuthenticationType.PermanentHost;

                // Auth 2.2) Encode security key
                byte[] securityKey = ByteUtil.Utf8NoBOM.GetBytes("NOT REAL");
                // TODO: Get the real security key from this exe's embedded settings.
                if (securityKey.Length > byte.MaxValue)
                {
                    return(new HostConnectResult(ProtocolErrors.SecurityKeyLength));
                }

                byte[] signature, publicKey;
                if (authType == HostAuthenticationType.PermanentHost)
                {
                    // Auth 2.3) Create signature
                    signature = IdentityVerification.SignAuthenticationChallenge(authChallenge);
                    // Auth 2.4) Encode public key
                    publicKey = ByteUtil.Utf8NoBOM.GetBytes(IdentityVerification.GetPublicKeyXML());
                }
                else
                {
                    signature = new byte[0];
                    publicKey = new byte[0];
                }
                // Auth 2.5) Encode computer name
                byte[] computerName = ByteUtil.Utf8NoBOM.GetBytes(Environment.MachineName);
                // Auth 2.6) Encode host version string
                byte[] appVersion = ByteUtil.Utf8NoBOM.GetBytes(AppVersion.VersionNumber);
                // Auth 2.7) Encode OS version string
                byte[] osVersion = ByteUtil.Utf8NoBOM.GetBytes(OSInfo.GetOSVersionInfo());

                // Build single buffer (not strictly necessary, but it helps us ensure we got the length calculated right).
                int calculatedLength = 1
                                       + 1 + securityKey.Length
                                       + 2 + signature.Length
                                       + 2 + publicKey.Length
                                       + 1 + computerName.Length
                                       + 1 + appVersion.Length
                                       + 1 + osVersion.Length;
                if (calculatedLength > ushort.MaxValue)
                {
                    return(new HostConnectResult(ProtocolErrors.AuthResponseTooLarge));
                }

                using (MemoryDataStream mds = new MemoryDataStream(calculatedLength))
                {
                    mds.WriteByte((byte)authType);
                    mds.WriteByte((byte)securityKey.Length);
                    mds.Write(securityKey);
                    mds.WriteUInt16((ushort)signature.Length);
                    mds.Write(signature);
                    mds.WriteUInt16((ushort)publicKey.Length);
                    mds.Write(publicKey);
                    mds.WriteByte((byte)computerName.Length);
                    mds.Write(computerName);
                    mds.WriteByte((byte)appVersion.Length);
                    mds.Write(appVersion);
                    mds.WriteByte((byte)osVersion.Length);
                    mds.Write(osVersion);
                    if (mds.Position != calculatedLength)
                    {
                        return(new HostConnectResult(ProtocolErrors.AuthResponseSizeError));
                    }
                    mds.Seek(0, SeekOrigin.Begin);

                    // Send authentication reply
                    stream.WriteByte((byte)Command.ClientAuthentication);
                    ByteUtil.WriteUInt16((ushort)calculatedLength, stream);
                    mds.CopyTo(stream);
                }
                #endregion
                // This is the Host Service, which is responsible for sending a KeepAlive packet after 60 seconds of sending inactivity.  The Master Server will do the same on a 120 second interval.
                tcpClient.ReceiveTimeout = 135000;                 // 120 seconds + 15 seconds for bad network conditions.
                tcpClient.SendTimeout    = 75000;

                State = HostConnectClientState.Connected;
                StateChanged(this, new StateChangedEventArgs(null, State));

                // Send KeepAlive packets every 60 seconds if no other packets have been sent.
                keepAlive = new KeepAliveSender("KeepAlive", 60000, SendKeepalive, (ignoredArg) => Disconnect());
                CommandLoop(tcpClient, stream);
            }
            finally
            {
                // Make local copies of these references so they can't become null after the null check.
                KeepAliveSender k = keepAlive;
                if (k != null)
                {
                    Try.Catch_RethrowThreadAbort(keepAlive.Stop);
                }
                TcpClient c = tcpClient;
                if (c != null)
                {
                    Try.Catch_RethrowThreadAbort(c.Close);
                }
            }
            return(new HostConnectResult());
        }
예제 #2
0
        /// <summary>
        /// Implements the server-side part of Self Hosted Remote Desktop's HostConnect protocol.  Called by the web server when a remote Host Service POSTs to url /hostconnect
        /// </summary>
        /// <param name="p">The HttpProcessor instance.</param>
        public static HostConnectResult HandleHostService(HttpProcessor p)
        {
            p.tcpClient.ReceiveTimeout = 30000;
            p.tcpClient.SendTimeout    = 30000;
            Logger.Info("Host connected");
            Computer computer = null;

            try
            {
                #region Authentication Protocol
                {
                    // Auth 0) Send ClientAuthentication command code.
                    p.tcpStream.WriteByte((byte)Command.ClientAuthentication);

                    // Auth 1) Send authentication challenge.  This is an array of 32 random bytes which the Host Service must sign with its private key.
                    byte[] authChallenge = ByteUtil.GenerateRandomBytes(32);
                    p.tcpStream.Write(authChallenge, 0, authChallenge.Length);

                    // Auth 2) Receive authentication reply.  This comes in as one big block so that data can be added to the end of the block in the future without breaking existing implementations.
                    Command receivedCommand = (Command)ByteUtil.ReadNBytes(p.tcpStream, 1)[0];
                    if (receivedCommand != Command.ClientAuthentication)
                    {
                        return(new HostConnectResult(ProtocolErrors.AuthResponseCommand));
                    }

                    ushort authResponseLength = ByteUtil.ReadUInt16(p.tcpStream);
                    if (authResponseLength == 0)
                    {
                        return(new HostConnectResult(ProtocolErrors.AuthResponseLength));
                    }
                    using (MemoryDataStream authResponse = new MemoryDataStream(p.tcpStream, authResponseLength))
                    {
                        // Auth 2.1) Read authentication type.
                        HostAuthenticationType authType = (HostAuthenticationType)authResponse.ReadByte();

                        // Auth 2.2) Read the security key that was created when the host download was provisioned.
                        int securityKeyLength = authResponse.ReadByte();
                        if (securityKeyLength == 0)
                        {
                            return(new HostConnectResult(ProtocolErrors.SecurityKeyLength));
                        }
                        string securityKey = authResponse.ReadUtf8(securityKeyLength);


                        // Auth 2.3) Read the signature.
                        ushort signatureLength = authResponse.ReadUInt16();
                        if (signatureLength == 0 && authType == HostAuthenticationType.PermanentHost)
                        {
                            return(new HostConnectResult(ProtocolErrors.SignatureLength));
                        }
                        byte[] signature = authResponse.ReadNBytes(signatureLength);

                        // Auth 2.4) Read the public key. This is used to identify and authenticate the computer.
                        ushort publicKeyLength = authResponse.ReadUInt16();
                        if (publicKeyLength == 0 && authType == HostAuthenticationType.PermanentHost)
                        {
                            return(new HostConnectResult(ProtocolErrors.PublicKeyLength));
                        }
                        string publicKey = authResponse.ReadUtf8(publicKeyLength);

                        // Auth 2.5) Read the computer name.
                        int nameLength = authResponse.ReadByte();
                        if (nameLength == 0)
                        {
                            return(new HostConnectResult(ProtocolErrors.NameLength));
                        }
                        string name = authResponse.ReadUtf8(nameLength);

                        // Get computer from database
                        computer = ServiceWrapper.db.GetComputerByPublicKey(publicKey);
                        bool computerIsNew = computer == null;
                        if (computerIsNew)
                        {
                            Logger.Info("computerIsNew: " + name);
                            computer                = new Computer();
                            computer.PublicKey      = publicKey;
                            computer.Name           = name;                   // Only set the name if this is the first time we've seen the computer.  Future name changes will only happen in the SHRD administration interface.
                            computer.LastDisconnect = TimeUtil.GetTimeInMsSinceEpoch();
                        }
                        else
                        {
                            Logger.Info("Existing computer is reconnecting: " + computer.ID + " (" + computer.Name + ")");
                        }

                        // Auth 2.6) Read the Host version string.
                        int hostVersionLength = authResponse.ReadByte();
                        computer.AppVersion = authResponse.ReadUtf8(hostVersionLength);

                        // Auth 2.7) Read the OS version string.
                        int osVersionLength = authResponse.ReadByte();
                        computer.OS = authResponse.ReadUtf8(osVersionLength);

                        // Signature Verification
                        if (authType == HostAuthenticationType.PermanentHost &&
                            !IdentityVerification.VerifySignature(authChallenge, computer.PublicKey, signature))
                        {
                            return(new HostConnectResult(ProtocolErrors.SignatureVerificationFailed));
                        }

                        // Add or update this Computer in the database.
                        try
                        {
                            if (computerIsNew)
                            {
                                ServiceWrapper.db.AddComputer(computer);
                                // TODO: Remove this TEMPORARY LOGIC to add this computer to Group 1
                                ServiceWrapper.db.AddComputerGroupMembership(computer.ID, 2);
                            }
                            else
                            {
                                ServiceWrapper.db.UpdateComputer(computer);
                                ServiceWrapper.db.AddComputerGroupMembership(computer.ID, 2);
                            }
                        }
                        catch (ThreadAbortException) { throw; }
                        catch (Exception ex)
                        {
                            Logger.Debug(ex);
                            return(new HostConnectResult(ProtocolErrors.FailedToAddComputer));
                        }
                    }
                }
                #endregion

                // This is the Master Server, which is responsible for sending a KeepAlive packet after 120 seconds of sending inactivity.  The Host Service will do the same on a 60 second interval.
                // A 75 second timeout means that disconnections should always be detected within 75 seconds of connection loss.  I don't know how long the underlying TCP stacks will wait, so this provides some measure of a guarantee that we don't wait excessively long.
                p.tcpClient.ReceiveTimeout = 75000;                 // 60 seconds + 15 seconds for bad network conditions.
                p.tcpClient.SendTimeout    = 75000;

                Logger.Info("Host authenticated: (" + computer.ID + ") " + computer.Name);
                // Create a HostConnectHandle for this computer to take over responsibility for the connection.
                HostConnectHandle handle = new HostConnectHandle(computer.ID, p);
                hosts.AddOrUpdate(computer.ID, handle, (id, existing) =>
                {
                    // We have a handle for this host already, so just disconnect the old one.  It probably just hasn't timed out yet.
                    existing.Disconnect();
                    return(handle);
                });

                handle.ListenLoop();
            }
            catch (ThreadAbortException) { }
            catch (SocketException)
            {
                Logger.Info(computer == null ? "Host disconnected before authentication was complete" : ("Host disconnected: (" + computer.ID + ") " + computer.Name));
            }
            catch (EndOfStreamException)             // ordinary socket disconnect
            {
                Logger.Info(computer == null ? "Host disconnected before authentication was complete" : ("Host disconnected: (" + computer.ID + ") " + computer.Name));
            }
            catch (Exception ex)
            {
                string additionalInfo = computer == null ? "Host was not authenticated" : ("Host (" + computer.ID + ") " + computer.Name);
                Logger.Debug(ex, "Host (" + computer.ID + ") " + computer.Name);
            }
            finally
            {
                if (computer != null && hosts.TryRemove(computer.ID, out HostConnectHandle existing))
                {
                    existing.Disconnect();
                }
            }

            return(new HostConnectResult());
        }