Jump to content

Need help on programming a real-time chat application using PHP WebSocket


Recommended Posts

Posted (edited)

Hi, I'm creating a social media with a real-time chat application using PHP WebSocket, the basic functionalities are working properly but I need to implement some other features as well. So I need help with the following related to the chat application:

  1. Login to the WebSocket using the credentials users use to log into the social media.
  2. Allow users to chat with their friends only.
  3. Identify users from the database using their ID and fetch data from the database.
  4. Show online/offline status based on the WebSocket.
  5. Show if a message is pending/sent/delivered/seen status.
  6. Show typing notification to the other user.

I'm attaching a screenshot of the chat screen:

image.thumb.png.5201a746609b23040bf424b4983150d5.png

Edited by sagnik
Added another reason
Link to comment
Share on other sites

  • 2 weeks later...

Logins via social media are usually done using OAuth and OpenID Connect, which can be non-trivial to implement. For development purposes, as long as you're just testing on your own computer, I'd recommend building a mock login flow that just takes a username. It would be far too insecure to expose to the public Internet, but it would give you an opportunity to work out your database layer, which will be required for OAuth anyway.

Link to comment
Share on other sites

Thanks for the advice, but the service has its login service, which uses SSO but the problem is handling the sessions within the WebSocket server for multiple clients.

Link to comment
Share on other sites

  • 2 weeks later...

First of all, sorry about the slow response. I was expecting to get a notification e-mail, but it turns out that I needed to adjust my settings for that to happen.

So, it sounds like you've got an external SSO provider of some kind. Is it based on OAuth/OpenID Connect or on some other SSO protocol? For OAuth, if you have access to self-encoded access tokens, those can be relatively straightforward to validate without having to track too much session-related state in your own database. Since you mentioned social media logins, I'm going to guess they probably do use self-encoded access tokens since that's common for services that operate at a very large scale.

For the database layer, is there already a specific database you're looking at using?

Link to comment
Share on other sites

The platform doesn't use any third-party service. The SSO is also its own which I've developed.

Link to comment
Share on other sites

OK, if you've already got SSO sorted out, is your first question about how to handle authentication specifically when you're opening the WebSocket connection?
 

On 5/23/2024 at 7:37 AM, sagnik said:

Login to the WebSocket using the credentials users use to log into the social media.

Edited by Haradion
Link to comment
Share on other sites

Yes, as I cannot start a session within WebSocket because it will replace the previous session whenever a new client is authenticated.

Link to comment
Share on other sites

When you say the session will be "replaced", do you mean that the session data would be overwritten on the backend?

The request to initiate the WebSocket connection should include the same session cookies that other requests would include, so if you're using cookies for sessions, the logic shouldn't be too different from the normal procedure for opening an existing session. It should be possible to use the same session ID that your normal HTTP requests are using rather than having to start a separate session ID dedicated to just the WebSocket.

Link to comment
Share on other sites

I use some keys namely "sgn-login-uid", "sgn-login-sessid", "sgn-login-ip", "sgn-login-timestamp" & "sgn-login-expires" to authenticate a user by setting $_SESSION variables for the aforementioned keys. So every time a new user is authenticated those values have to be changed.

Link to comment
Share on other sites

I didn't understand how exactly the $_SESSION variables are getting overwritten. I thought if a new user joins, they would have their own set of $_SESSION variables which doesn't interfere with the first user's $_SESSION?

Maybe you can try sharing the part of the code that updates the $_SESSION variables so I can see how it's working?

Link to comment
Share on other sites

On 7/1/2024 at 2:29 AM, sagnik said:

I use some keys namely "sgn-login-uid", "sgn-login-sessid", "sgn-login-ip", "sgn-login-timestamp" & "sgn-login-expires" to authenticate a user by setting $_SESSION variables for the aforementioned keys. So every time a new user is authenticated those values have to be changed.

Are those variables changed on every request, or do they only change when the user submits login credentials? If normal requests after login only read those fields, the WebSocket connection can do the same thing, as it should receive the PHPSESSID cookie just like normal requests do.

Link to comment
Share on other sites

13 hours ago, Haradion said:

Are those variables changed on every request, or do they only change when the user submits login credentials? If normal requests after login only read those fields, the WebSocket connection can do the same thing, as it should receive the PHPSESSID cookie just like normal requests do.

Those variables are changed when a user submits their login credentials and are validated. So, I need to pass the PHPSESSID cookie as well to the WS server, right?

Link to comment
Share on other sites

Posted (edited)
On 7/1/2024 at 6:48 PM, badrihippo said:

I didn't understand how exactly the $_SESSION variables are getting overwritten. I thought if a new user joins, they would have their own set of $_SESSION variables which doesn't interfere with the first user's $_SESSION?

Maybe you can try sharing the part of the code that updates the $_SESSION variables so I can see how it's working?

@badrihippo Here is the code of SSO SignOn.php:

<?php
/*
 * Copyright (c) 2022-2023 SGNetworks. All rights reserved.
 *
 * The software is an exclusive copyright of "SGNetworks" and is provided as is exclusively with only "USAGE" access. "Modification",  "Alteration", "Re-distribution" is completely prohibited.
 * VIOLATING THE ABOVE TERMS IS A PUNISHABLE OFFENSE WHICH MAY LEAD TO LEGAL CONSEQUENCES.
 */

session_start();
$SGNSSO = ['accounts.sgnetworks.net', 'accounts.sgnetworks.eu.org'];

function is_base64(string $data): bool {
	$base64 = base64_encode(base64_decode($data, true));
	return ($base64 === $data);
}

function is_base64URL(string $data): bool {
	$base64 = strtr($data, '-_', '+/');
	$base64 = base64_encode(base64_decode($base64));
	$base64 = strtr(rtrim($base64, '='), '+/', '-_');
	return ($base64 === $data);
}

function Base64UrlEncode(string $data, bool $force = false): string {
	if($force) {
		return strtr(rtrim(base64_encode($data), '='), '+/', '-_');
	}
	$base64 = (!is_base64($data)) ? base64_encode($data) : $data;
	return (!is_base64URL($base64)) ? strtr(rtrim($base64, '='), '+/', '-_') : $base64;
}

function Base64UrlDecode(string $base64, bool $strict = false): string|false {
	$data = (is_base64URL($base64)) ? strtr($base64, '-_', '+/') : $base64;
	return (is_base64($data)) ? base64_decode($data, $strict) : base64_decode($data);
}

function server(string $key, string|int|bool|array $default = null): array|bool|int|string|null {
	$server = $_SERVER;
	$null = ($default === null && !is_bool($default) && !is_array($default) && !is_integer($default) && !is_string($default)) ? null : $default;
	return (array_key_exists($key, $server)) ? $server[$key] : $null;
}

function session(string $key, string|int|bool|array $default = null): array|bool|int|string|null {
	$session = $_SESSION;
	$null = ($default === null && !is_bool($default) && !is_array($default) && !is_integer($default) && !is_string($default)) ? null : $default;
	return (array_key_exists($key, $session)) ? $session[$key] : $null;
}

function post(string $key, string|int|bool|array $default = null): array|bool|int|string|null {
	$post = $_POST;
	$null = ($default === null && !is_bool($default) && !is_array($default) && !is_integer($default) && !is_string($default)) ? null : $default;
	return ((array_key_exists($key, $post)) ? $post[$key] : $null);
}

function get(string $key, string|int|bool|array $default = null): array|bool|int|string|null {
	$get = $_GET;
	$null = ($default === null && !is_bool($default) && !is_array($default) && !is_integer($default) && !is_string($default)) ? null : $default;
	return (array_key_exists($key, $get)) ? $get[$key] : $null;
}

function buildURL(string $uri, ?string $params = null, ?string $args = null): string {
	$params = (!empty($params)) ? ltrim($params, '?' . '&') : '';
	$args = (!empty($args)) ? ltrim($args, '?' . '&') : '';

	if(!empty($params) && !empty($args)) {
		$url = (str_contains($uri, '?') || str_contains($params, '?') || str_contains($args, '?')) ? "$uri&$params&$args" : "$uri?$params&$args";
	} elseif(!empty($params) && empty($args)) {
		$url = (str_contains($uri, '?') || str_contains($params, '?')) ? "$uri&$params" : "$uri?$params";
	} elseif(empty($params) && !empty($args)) {
		$url = (str_contains($uri, '?') || str_contains($args, '?')) ? "$uri&$args" : "$uri?$args";
	} else {
		$url = $uri;
	}

	$url_parts = parse_url($url);
	$qs = '';
	if(array_key_exists('query', $url_parts)) {
		$qs = $url_parts['query'];
		parse_str($qs, $qo);
		$qs = (count($qo) > 0) ? http_build_query($qo) : '';
	}
	$constructed_url = $url_parts['scheme'] . '://' . $url_parts['host'] . ($url_parts['path'] ?? '');
	return (!empty($qs)) ? "$constructed_url?$qs" : $constructed_url;
}

function redirect(string $uri, string $vars = ''): void {
	$qm = (str_contains($uri, '?') || str_contains($vars, '?')) ? '&' : '?';
	$url = buildURL($uri);

	if(!headers_sent()) {
		header("Location: $url");
		exit();
	} else {
		echo '<script>';
		echo "window.location.href=('$url');";
		echo '</script>';
		echo "You will be redirected shortly. If you are not redirected automatically, please <a href='$url'>click here</a> to redirect";
	}
}

function get_domain(string $url): string|false {
	$urlobj = parse_url($url);
	$domain = $urlobj['host'];
	if(preg_match('/(?P<domain>[a-z0-9][a-z0-9\-]{1,63}\.[a-z.]{2,6})$/i', $domain, $regs)) {
		return $regs['domain'];
	}
	return false;
}

if(server('REQUEST_METHOD') == 'POST') {
	$redirectTo = (!post('continue')) ? post('redirect') : post('continue');
	$params = post('params', '');
	$args = post('args', '');
	$session = post('session');
	$origin = post('origin');
} else {
	$redirectTo = (!get('continue')) ? get('redirect') : get('continue');
	$params = get('params', '');
	$args = get('args', '');
	$session = get('session');
	$origin = get('origin');
}
$sc = explode('-', $session);
$sessid = Base64UrlDecode($sc[0]);
$uid = Base64UrlDecode($sc[1]);
$uid_hashed = Base64UrlDecode($sc[2]);

$ssoProcessed = false;
$continueHost = $continue = '';
if(in_array($origin, $SGNSSO)) {
	$args = Base64UrlDecode($args);
	$redirectTo = Base64UrlDecode($redirectTo);
	$redirectTo = buildURL($redirectTo, $args);
	if(empty(session('sgn-login-sid'))) {
		if(empty($session)) {
			$continue = (!$redirectTo) ? $origin . server('REQUEST_URI') : $redirectTo;
		} else {
			$_SESSION['sgn-login-sid'] = $sessid;
			$_SESSION['sgn-login-uid'] = $uid;
			$_SESSION['sgn-login-uid_hashed'] = $uid_hashed;
			$_SESSION['sgn-login-expires'] = time() + 3600;
			$_SESSION['sgn-login-timestamp'] = time();
			$_SESSION['sgn-login-ip'] = server('REMOTE_ADDR');
			$ssoProcessed = true;
			$url = parse_url($redirectTo);
			$p = (array_key_exists('path', $url)) ? $url['path'] : '';
			$q = (array_key_exists('query', $url)) ? '?' . $url['query'] : '';
			$s = (!$q) ? "?sessid=$sessid" : "&sessid=$sessid";
			unset($redirectTo);
			unset($_GET['session']);
			$continueUrl = $url['scheme'] . '://' . $url['host'] . $p . $q;
			$continue = "$continueUrl$s";
			$continue = (!$continue) ? $_SERVER['HTTP_REFERER'] : $continue;
			$continueHost = $url['host'];
			$continueLocation = "{$url['scheme']}://$continueHost";
		}
	} elseif(!empty($session)) {
		$_SESSION['sgn-login-sid'] = $sessid;
		$_SESSION['sgn-login-uid'] = $uid;
		$_SESSION['sgn-login-uid_hashed'] = $uid_hashed;
		$_SESSION['sgn-login-expires'] = time() + 3600;
		$_SESSION['sgn-login-timestamp'] = time();
		$_SESSION['sgn-login-ip'] = server('REMOTE_ADDR');
		$ssoProcessed = true;
		if(!empty($redirectTo)) {
			$url = parse_url($redirectTo);
			$p = (array_key_exists('path', $url)) ? $url['path'] : '';
			$q = (array_key_exists('query', $url)) ? '?' . $url['query'] : '';
			$s = (!$q) ? "?sessid=$sessid" : "&sessid=$sessid";
			unset($redirectTo);
			unset($_GET['session']);
			$continueUrl = $url['scheme'] . '://' . $url['host'] . $p . $q;
			$continue = "$continueUrl$s";
			$continue = (!$continue) ? $_SERVER['HTTP_REFERER'] : $continue;
			$continueHost = $url['host'];
			$continueLocation = "{$url['scheme']}://$continueHost";
		} else {
			$continue = $_SERVER['HTTP_REFERER'];
		}
	}
	$params = (!empty($params)) ? Base64UrlDecode($params) : '';
	$continue = buildURL($continue, $params);
} else {
	echo 'The Origin Host is not allowed to make SSO Requests';
}
if($_SERVER['REQUEST_METHOD'] == 'GET'): ?>
	<script>
	function crossDomainLogin() {
		const url = "<?=$continueLocation;?>/SGNSSO/SignOn";
		const xhr = new XMLHttpRequest();
		xhr.onerror = function() {
			if(xhr.status === 0) {
				console.log("Cross-Domain Request Failed");
			} else {
				console.log("Cross-Domain Request Failed with the following Status: ", xhr.status);
			}
		};
		xhr.onreadystatechange = function() {
			if(this.readyState === 4 && this.status === 200) {
				if(xhr.responseText === "done") {
					window.location.replace("<?=$continue;?>");
				} else {
					console.log("SGNSSO is available only for SGNetworks and its Subsidiaries");
				}
			} else {
				//console.log("Cross-Domain Request is not ready or the request has failed with status: ",this.status);
			}
		};
		xhr.open("POST", url);
		xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
		xhr.crossDomain = true;
		xhr.withCredentials = true;
		xhr.send("session=<?=$session;?>&origin=<?=$origin;?>&redirect=<?=Base64UrlEncode($continueUrl);?>");
	}

	const sso = <?=($ssoProcessed) ? 'true' : 'false';?>;
	if(window.location.host !== '<?=$continueHost;?>') {
		crossDomainLogin();
	} else {
		if(sso === true || sso === "true") {
			window.location.replace("<?=$continue;?>");
		}
	}
	</script>
<?php
elseif($ssoProcessed):
	echo 'done';
else:
	echo 'failed';
endif;

 

Edited by sagnik
Link to comment
Share on other sites

On 7/2/2024 at 10:08 PM, sagnik said:

Those variables are changed when a user submits their login credentials and are validated. So, I need to pass the PHPSESSID cookie as well to the WS server, right?

Yes; that's probably the easiest way to pick up the user's session in the WebSocket context. Based on my understanding (I haven't actually tested this), the browser should pass PHPSESSID in the request automatically, so you'd just need to call session_start() in the WebSocket request handler and read the appropriate values out of $_SESSION.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...