/// <summary>
        /// Handles a request from a Web Client who is requesting a proxied web socket connection to a specific Host Service.
        /// </summary>
        /// <param name="p">The HttpProcessor handling the request.</param>
        public static void HandleWebSocketClientProxyRequest(HttpProcessor p)
        {
            #region Perform Validation
            // Path format: "/WebSocketClientProxy/1/SESSION". More path segments, if given, are ignored (the last path segment is typically used as a display name in browser developer tools).
            string[] parts = p.request_url.LocalPath.Split('/').Skip(1).ToArray();
            int      computerId;        // The computer ID the client wishes to connect to.
            if (parts.Length < 3 || parts[0] != "WebSocketClientProxy" || !int.TryParse(parts[1], out computerId))
            {
                p.writeFailure("400 Bad Request");
                return;
            }
            string sid = parts[2];             // The session ID of the client's session.
            #endregion
            #region Verify Permission
            ServerSession session = SessionManager.GetSession(sid);
            if (session == null || session.Expired)
            {
                p.writeFailure("403 Forbidden");
                return;
            }
            Computer computer = ServiceWrapper.db.GetComputer(computerId);
            if (computer == null)
            {
                p.writeFailure("404 Not Found");
                return;
            }

            User user = session.GetUser();
            if (user == null)
            {
                p.writeFailure("403 Forbidden");
                return;
            }

            // If we get here, we have an active authenticated session.
            if (!user.IsAdmin)
            {
                // Admin users can access all computers.
                // This user is not an adminn, so we must check group membership.
                ComputerGroupMembership[] cgm = computer.GetGroupMemberships();
                if (cgm.Length == 0)
                {
                    Logger.Info("Non-admin user " + user.ID + " (" + user.Name + ") attempted to access computer " + computer.ID + " (" + computer.Name + ") but computer has no group memberships.");
                    p.writeFailure("403 Forbidden");
                    return;
                }
                UserGroupMembership[] ugm = user.GetGroupMemberships();
                if (ugm.Length == 0)
                {
                    Logger.Info("Non-admin user " + user.ID + " (" + user.Name + ") attempted to access computer " + computer.ID + " (" + computer.Name + ") but user has no group memberships.");
                    p.writeFailure("403 Forbidden");
                    return;
                }

                // The computer is accessible to this user if the computer and the user share at least one group membership.
                bool accessible = 0 < cgm.Select(m => m.GroupID).Intersect(ugm.Select(m => m.GroupID)).Count();
                if (!accessible)
                {
                    Logger.Info("Non-admin user " + user.ID + " (" + user.Name + ") attempted to access computer " + computer.ID + " (" + computer.Name + ") without permission.");
                    p.writeFailure("403 Forbidden");
                    return;
                }
            }
            #endregion

            // Now that permission has been verified, find out of the specified computer is online.
            HostConnectHandle host = HostConnect.GetOnlineComputer(computer.ID);
            if (host == null)
            {
                p.writeFailure("504 Gateway Timeout", "Computer " + computer.ID + " is not online.");
                return;
            }

            // The computer is online.  Send a request to have the Host Service connect to this Master Server's web socket proxy service.
            string        proxyKey      = StringUtil.GetRandomAlphaNumericString(64);
            WaitingClient waitingClient = null;
            try
            {
                waitingClient = new WaitingClient(p);
                pendingProxyConnections[proxyKey] = waitingClient;
                host.RequestWebSocketProxy(p.RemoteIPAddress, proxyKey);

                // Wait for the connection from the Host Service.
                if (!waitingClient.clientWaitHandle.WaitOne(10000))
                {
                    p.writeFailure("504 Gateway Timeout", "Computer " + computer.ID + " did not respond in a timely manner.");
                    return;
                }
                Stream hostStream = waitingClient.hostProcessor?.tcpStream;
                if (hostStream == null)
                {
                    p.writeFailure("500 Internal Server Error");
                    Logger.Debug("hostStream was null in WebSocketProxy handler");
                    return;
                }

                // The Host Service has connected.  Remove the pending connection and clean up before starting to proxy data between the sockets.
                pendingProxyConnections.TryRemove(proxyKey, out WaitingClient ignored);
                proxyKey = null;
                waitingClient.Dispose();

                // Copy data from Host Service to Web Client
                p.responseWritten   = true;
                p.tcpClient.NoDelay = true;
                Console.WriteLine("Client Proxy Initialized");
                CopyStreamUntilClosed(hostStream, p.tcpStream);
            }
            finally
            {
                // If anything went wrong initializing the proxy connection, we might not have cleaned up yet.
                if (proxyKey != null)
                {
                    pendingProxyConnections.TryRemove(proxyKey, out WaitingClient ignored);
                }

                waitingClient?.Dispose();
            }
        }
Exemplo n.º 2
0
        /// <summary>
        /// Handles a request from a Web Client who is requesting a proxied web socket connection to a specific Host Service.
        /// </summary>
        /// <param name="p">The HttpProcessor handling the request.</param>
        private static void HandleWebSocketProxyRequest(HttpProcessor p)
        {
            string[] parts = p.request_url.Segments;
            #region Validate Input
            if (parts.Length != 3)
            {
                p.writeFailure("400 Bad Request");
                return;
            }
            if (!int.TryParse(parts[1], out int computerId))
            {
                p.writeFailure("400 Bad Request");
                return;
            }
            #endregion
            #region Verify Permission
            string        sid     = parts[2];
            ServerSession session = SessionManager.GetSession(sid);
            if (session == null || session.Expired)
            {
                p.writeFailure("403 Forbidden");
                return;
            }
            Computer computer = ServiceWrapper.db.GetComputer(computerId);
            if (computer == null)
            {
                p.writeFailure("404 Not Found");
                return;
            }

            User user = session.GetUser();
            if (user == null)
            {
                p.writeFailure("403 Forbidden");
                return;
            }

            // If we get here, we have an active authenticated session.
            if (!user.IsAdmin)
            {
                // Admin users can access all computers.
                // This user is not an adminn, so we must check group membership.
                ComputerGroupMembership[] cgm = computer.GetGroupMemberships();
                if (cgm.Length == 0)
                {
                    Logger.Info("Non-admin user " + user.ID + " (" + user.Name + ") attempted to access computer " + computer.ID + " (" + computer.Name + ") but computer has no group memberships.");
                    p.writeFailure("403 Forbidden");
                    return;
                }
                UserGroupMembership[] ugm = user.GetGroupMemberships();
                if (ugm.Length == 0)
                {
                    Logger.Info("Non-admin user " + user.ID + " (" + user.Name + ") attempted to access computer " + computer.ID + " (" + computer.Name + ") but user has no group memberships.");
                    p.writeFailure("403 Forbidden");
                    return;
                }

                // The computer is accessible to this user if the computer and the user share at least one group membership.
                bool accessible = 0 < cgm.Select(m => m.GroupID).Intersect(ugm.Select(m => m.GroupID)).Count();
                if (!accessible)
                {
                    Logger.Info("Non-admin user " + user.ID + " (" + user.Name + ") attempted to access computer " + computer.ID + " (" + computer.Name + ") without permission.");
                    p.writeFailure("403 Forbidden");
                    return;
                }
            }
            #endregion

            // Now that permission has been verified, find out of the specified computer is online.
            HostConnectHandle host = HostConnect.GetOnlineComputer(computer.ID);
            if (host == null)
            {
                p.writeFailure("504 Gateway Timeout", "Computer " + computer.ID + " is not online.");
                return;
            }

            // The computer is online.  Send a request to have the Host Service connect to this Master Server's web socket proxy service.
            string        proxyKey      = Util.GetRandomAlphaNumericString(64);
            WaitingClient waitingClient = null;
            try
            {
                waitingClient = new WaitingClient(p);
                pendingProxyConnections[proxyKey] = waitingClient;
                host.RequestWebSocketProxy(p.RemoteIPAddress, proxyKey);

                // Wait for the connection from the Host Service.
                if (!waitingClient.clientWaitHandle.WaitOne(10000))
                {
                    p.writeFailure("504 Gateway Timeout", "Computer " + computer.ID + " did not respond in a timely manner.");
                    return;
                }
                Stream hostStream = waitingClient.hostProcessor?.tcpStream;
                if (hostStream == null)
                {
                    p.writeFailure("500 Internal Server Error");
                    Logger.Debug("hostStream was null in WebSocketProxy handler");
                    return;
                }

                // The Host Service has connected.  Remove the pending connection and clean up before starting to proxy data between the sockets.
                pendingProxyConnections.TryRemove(proxyKey, out WaitingClient ignored);
                proxyKey = null;
                waitingClient.Dispose();

                // Copy data from Host Service to Web Client
                p.responseWritten = true;
                CopyStreamUntilClosed(hostStream, p.tcpStream);
            }
            finally
            {
                if (proxyKey != null)
                {
                    pendingProxyConnections.TryRemove(proxyKey, out WaitingClient ignored);
                }

                waitingClient?.Dispose();
            }
        }
Exemplo n.º 3
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());
        }