12 ]; #[Route( '/user', name: 'user_index', methods: [ 'GET' ] )] public function index( ?Request $request ): Response { return $this->redirectToRoute( 'user_login' ); } #[Route( '/user/list', name: 'user_list', methods: [ 'GET' ] )] public function list( ?Request $request ): Response { $list = []; // Check client connection if ($client = $this->_client()) { // Check users database accessible if ($namespace = $this->_namespace($client)) { // Collect usernames foreach ((array) $client->kevaFilter($namespace) as $user) { // Check record valid if (empty($user['key']) || empty($user['height'])) { continue; } // Skip values with meta keys if (str_starts_with($user['key'], '_')) { continue; } // Validate username regex if (!preg_match($this->getParameter('app.add.user.name.regex'), $user['key'])) { continue; } // Get room stats $total = 0; $rooms = []; foreach ($this->_rooms($client) as $room => $name) { $posts = 0; foreach ((array) $client->kevaFilter($room, sprintf('^[\d]+@%s$', $user['key'])) as $post) { $total++; $posts++; $rooms[$room] = $posts; } } $list[] = [ 'name' => $user['key'], 'balance' => $client->getBalance( $user['key'] ), 'address' => $client->getAccountAddress( $user['key'] ), 'total' => $total, 'rooms' => $rooms, ]; } } } // Sort by height array_multisort( array_column( $list, 'total' ), SORT_DESC, $list ); // RSS if ('rss' === $request->get('feed')) { $response = new Response(); $response->headers->set( 'Content-Type', 'text/xml' ); return $this->render( 'default/user/list.rss.twig', [ 'list' => $list, 'request' => $request ], $response ); } // HTML return $this->render( 'default/user/list.html.twig', [ 'list' => $list, 'request' => $request ] ); } #[Route( '/join', name: 'user_join', methods: [ 'GET' ] )] public function join( ?Request $request ): Response { // Connect memcached $memcached = new \Memcached(); $memcached->addServer( $this->getParameter('app.memcached.host'), $this->getParameter('app.memcached.port') ); // Create token $token = crc32( microtime(true) + rand() ); $memcached->add( $token, time() ); // Check user session does not exist to continue if (!empty($request->cookies->get('KEVACHAT_SESSION'))) { // Redirect to logout return $this->redirectToRoute( 'user_logout' ); } return $this->render( 'default/user/join.html.twig', [ 'request' => $request, 'token' => $token, 'cost' => $this->getParameter('app.add.user.cost.kva') ] ); } #[Route( '/login', name: 'user_login', methods: [ 'GET' ] )] public function login( ?Request $request ): Response { // Connect memcached $memcached = new \Memcached(); $memcached->addServer( $this->getParameter('app.memcached.host'), $this->getParameter('app.memcached.port') ); // Create token $token = crc32( microtime(true) + rand() ); $memcached->add( $token, time() ); // Check user session does not exist to continue if (!empty($request->cookies->get('KEVACHAT_SESSION'))) { // Redirect to logout return $this->redirectToRoute( 'user_logout' ); } return $this->render( 'default/user/login.html.twig', [ 'request' => $request, 'token' => $token ] ); } #[Route( '/logout', name: 'user_logout', methods: [ 'GET' ] )] public function logout( ?Request $request ): Response { // Connect memcached $memcached = new \Memcached(); $memcached->addServer( $this->getParameter('app.memcached.host'), $this->getParameter('app.memcached.port') ); // Make sure cookies exist if (!empty($request->cookies->get('KEVACHAT_SESSION')) && preg_match('/[A-z0-9]{32}/', $request->cookies->get('KEVACHAT_SESSION'))) { // Delete from memory $memcached->delete($session); // Delete cookies setcookie('KEVACHAT_SESSION', '', -1, '/'); setcookie('KEVACHAT_SIGN', '', -1, '/'); } // Redirect to main page return $this->redirectToRoute( 'user_login' ); } #[Route( '/join', name: 'user_add', methods: [ 'POST' ] )] public function add( Request $request, TranslatorInterface $translator, EntityManagerInterface $entity ): Response { // Check maintenance mode disabled if ($this->getParameter('app.maintenance')) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Maintenance, please try again later!') ] ); } // Check user session does not exist to continue if (!empty($request->cookies->get('KEVACHAT_SESSION'))) { // Redirect to logout return $this->redirectToRoute( 'user_logout' ); } // Trim extra spaces from username $username = trim( $request->get('username') ); // Connect memcached $memcached = new \Memcached(); $memcached->addServer( $this->getParameter('app.memcached.host'), $this->getParameter('app.memcached.port') ); // Create IP delay record $memory = md5( sprintf( '%s.UserController::add:add.user.remote.ip.delay:%s', __DIR__, $request->getClientIp(), ), ); // Validate form token if ($memcached->get($request->get('token'))) { $memcached->delete( $request->get('token') ); } else { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Session token expired') ] ); } // Validate remote IP limits if ($delay = (int) $memcached->get($memory)) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => sprintf( $translator->trans('Please wait %s seconds before register new username!'), (int) $this->getParameter('app.add.user.remote.ip.delay') - (time() - $delay) ) ] ); } // Check client connection if (!$client = $this->_client()) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not connect wallet database!') ] ); } // Check users database accessible if (!$namespace = $this->_namespace($client)) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not access user database locked by transaction, try again later!') ] ); } // Validate kevacoin key requirements if (mb_strlen($username) < 1 || mb_strlen($username) > 520) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username length out of KevaCoin protocol limits!') ] ); } // Validate system username values if (in_array(mb_strtolower($username), ['anon','anonymous'])) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username reserved for anon messages!') ] ); } // Validate meta NS if (str_starts_with($username, '_')) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username contain meta format!') ] ); } // Validate username regex if (!preg_match($this->getParameter('app.add.user.name.regex'), $username)) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => sprintf( $translator->trans('Username does not match node requirements: %s!'), $this->getParameter('app.add.user.name.regex') ) ] ); } // Validate username blacklist (reserved) if (in_array(mb_strtolower($username), array_map('mb_strtolower', (array) explode('|', $this->getParameter('app.add.user.name.blacklist'))))) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username reserved by node!') ] ); } // Validate username exist if ($this->_hash($client, $namespace, $username)) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username already taken!') ] ); } // Validate password length if (mb_strlen($request->get('password')) <= 6) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Please, provide stronger password!') ] ); } // Validate passwords match if ($request->get('password') !== $request->get('repeat')) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Password repeat incorrect!') ] ); } // Generate password hash if (!$hash = password_hash($request->get('password'), $this->_algorithm, $this->_options)) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not generate password hash!') ] ); } // User registration has commission cost, send message to pending payment pool if ($this->getParameter('app.add.user.cost.kva')) { if ($address = $client->getNewAddress($this->getParameter('app.kevacoin.pool.account'))) { $time = time(); $pool = new Pool(); $pool->setTime( $time ); $pool->setSent( 0 ); $pool->setExpired( 0 ); $pool->setCost( $this->getParameter('app.add.user.cost.kva') ); $pool->setAddress( $address ); $pool->setNamespace( $namespace ); $pool->setKey( $username ); $pool->setValue( $hash ); $entity->persist( $pool ); $entity->flush(); // Redirect back to room return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'warning' => sprintf( $translator->trans('To complete registration, send %s KVA to %s'), $this->getParameter('app.add.user.cost.kva'), $address ) ] ); } else { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not init registration address!') ] ); } } // Auth success, add user to DB if (!$client->kevaPut($namespace, $username, $hash)) { return $this->redirectToRoute( 'user_add', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not create user in blockchain!') ] ); } // Register event time $memcached->set( $memory, time(), (int) $this->getParameter('app.add.user.remote.ip.delay') // auto remove on cache expire ); // Auth success, create user session $session = md5( sprintf( '%s.%s.%s', $request->get('username'), $request->getClientIp(), rand() ) ); // Save session to memory if (!$memcached->set($session, $request->get('username'), (int) time() + $this->getParameter('app.session.default.timeout'))) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not save user session!') ] ); } // Save session to user cookies if (!setcookie('KEVACHAT_SESSION', $session, time() + $this->getParameter('app.session.default.timeout'), '/')) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not create session cookie!') ] ); } // Redirect to main page return $this->redirectToRoute( 'room_index' ); // Redirect to login page return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username') ] ); } #[Route( '/login', name: 'user_auth', methods: [ 'POST' ] )] public function auth( Request $request, TranslatorInterface $translator ): Response { // Check maintenance mode disabled if ($this->getParameter('app.maintenance')) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Maintenance, please try again later!') ] ); } // Check user session does not exist to continue if (!empty($request->cookies->get('KEVACHAT_SESSION'))) { // Redirect to logout return $this->redirectToRoute( 'user_logout' ); } // Connect memcached $memcached = new \Memcached(); $memcached->addServer( $this->getParameter('app.memcached.host'), $this->getParameter('app.memcached.port') ); // Validate form token if ($memcached->get($request->get('token'))) { $memcached->delete( $request->get('token') ); } else { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Session token expired') ] ); } // Check client connection if (!$client = $this->_client()) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not connect wallet database!') ] ); } // Check username namespace accessible if (!$namespace = $this->_namespace($client)) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not access user database locked by transaction, try again later!') ] ); } // Trim extra spaces from username $username = trim( $request->get('username') ); // Validate kevacoin key requirements if (mb_strlen($username) < 1 || mb_strlen($username) > 520) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username length out of KevaCoin protocol limits!') ] ); } // Validate meta NS if (str_starts_with($username, '_')) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username contain meta format!') ] ); } // Validate username regex if (!preg_match($this->getParameter('app.add.user.name.regex'), $username)) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => sprintf( $translator->trans('Username does not match node requirements: %s!'), $this->getParameter('app.add.user.name.regex') ) ] ); } // Validate username exist if (!$hash = $this->_hash($client, $namespace, $username)) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Username not found!') ] ); } // Validate password if (!password_verify($request->get('password'), $hash)) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Password invalid!') ] ); } // Validate password algo if (password_needs_rehash($hash, $this->_algorithm, $this->_options)) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Password needs rehash!') ] ); } // Auth success, create user session $session = md5( sprintf( '%s.%s.%s', $request->get('username'), $request->getClientIp(), rand() ) ); // Save session to memory if (!$memcached->set($session, $request->get('username'), (int) time() + $this->getParameter('app.session.default.timeout'))) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not save user session!') ] ); } // Save session to user cookies if (!setcookie('KEVACHAT_SESSION', $session, time() + $this->getParameter('app.session.default.timeout'), '/')) { return $this->redirectToRoute( 'user_login', [ 'username' => $request->get('username'), 'error' => $translator->trans('Could not create session cookie!') ] ); } // Redirect to main page return $this->redirectToRoute( 'room_index' ); } private function _client(): \Kevachat\Kevacoin\Client { $client = new \Kevachat\Kevacoin\Client( $this->getParameter('app.kevacoin.protocol'), $this->getParameter('app.kevacoin.host'), $this->getParameter('app.kevacoin.port'), $this->getParameter('app.kevacoin.username'), $this->getParameter('app.kevacoin.password') ); return $client; } private function _namespace( \Kevachat\Kevacoin\Client $client ): ?string { foreach ((array) $client->kevaListNamespaces() as $value) { if ($value['displayName'] === '_KEVACHAT_USERS_') { return $value['namespaceId']; } } return null; } private function _rooms( \Kevachat\Kevacoin\Client $client ): array { $rooms = []; foreach ((array) $client->kevaListNamespaces() as $value) { if (empty($value['namespaceId'])) { continue; } if (empty($value['displayName'])) { continue; } if (str_starts_with($value['displayName'], '_')) { continue; } $rooms[$value['namespaceId']] = $value['displayName']; } return $rooms; } private function _hash( \Kevachat\Kevacoin\Client $client, string $namespace, string $username ): ?string { if ($user = $client->kevaGet($namespace, $username)) { if (!empty($user['value'])) { return (string) $user['value']; } } return null; } }