snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
# @rocket.chat/cas-validate
## 0.0.4
### Patch Changes
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
## 0.0.4-rc.0
### Patch Changes
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
## 0.0.3
### Patch Changes
- ([#35115](https://github.com/RocketChat/Rocket.Chat/pull/35115) by [@tapiarafael](https://github.com/tapiarafael)) Fixes a problem with CAS when URL connection ended with `/`
## 0.0.3-rc.0
### Patch Changes
- ([#35115](https://github.com/RocketChat/Rocket.Chat/pull/35115)) Fixes a problem with CAS when URL connection ended with `/`
## 0.0.2
### Patch Changes
- ([#31138](https://github.com/RocketChat/Rocket.Chat/pull/31138)) feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo
## 0.0.2-rc.0
### Patch Changes
- b223cbde14: feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo

View File

@@ -0,0 +1,12 @@
import server from '@rocket.chat/jest-presets/server';
import type { Config } from 'jest';
export default {
projects: [
{
displayName: 'server',
preset: server.preset,
testMatch: ['<rootDir>/src/**/*.spec.[jt]s?(x)'],
},
],
} satisfies Config;

View File

@@ -0,0 +1,29 @@
{
"name": "@rocket.chat/cas-validate",
"version": "0.0.4",
"private": true,
"description": "Fork of https://github.com/kcbanner/node-cas",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"testunit": "jest"
},
"dependencies": {
"cheerio": "1.0.0"
},
"devDependencies": {
"eslint": "~9.39.5",
"jest": "~30.2.0",
"typescript": "~5.9.3"
},
"volta": {
"extends": "../../package.json"
}
}

View File

@@ -0,0 +1 @@
export * from './validate';

View File

@@ -0,0 +1,53 @@
import { getQueryPath } from './validate';
describe('getQueryPath', () => {
it('should generate the correct query path without trailing slash', () => {
const partialPathname = '/cas';
const validatePath = 'validate';
const query = { ticket: 'ST-12345', service: 'https://myapp.com' };
const result = getQueryPath(partialPathname, validatePath, query);
expect(result).toBe('/cas/validate?ticket=ST-12345&service=https%3A%2F%2Fmyapp.com');
});
it('should generate the correct query path with trailing slash', () => {
const partialPathname = '/cas/';
const validatePath = 'validate';
const query = { ticket: 'ST-12345', service: 'https://myapp.com' };
const result = getQueryPath(partialPathname, validatePath, query);
expect(result).toBe('/cas/validate?ticket=ST-12345&service=https%3A%2F%2Fmyapp.com');
});
it('should generate the correct query path with `/` partialPathname', () => {
const partialPathname = '/';
const validatePath = 'validate';
const query = { ticket: 'ST-12345', service: 'https://myapp.com' };
const result = getQueryPath(partialPathname, validatePath, query);
expect(result).toBe('/validate?ticket=ST-12345&service=https%3A%2F%2Fmyapp.com');
});
it('should generate the correct query path with empty partialPathname', () => {
const partialPathname = '';
const validatePath = 'validate';
const query = { ticket: 'ST-12345', service: 'https://myapp.com' };
const result = getQueryPath(partialPathname, validatePath, query);
expect(result).toBe('/validate?ticket=ST-12345&service=https%3A%2F%2Fmyapp.com');
});
it('should generate the correct query path with empty query', () => {
const partialPathname = '/cas';
const validatePath = 'validate';
const query = {};
const result = getQueryPath(partialPathname, validatePath, query);
expect(result).toBe('/cas/validate');
});
});

View File

@@ -0,0 +1,299 @@
import type { IncomingMessage } from 'node:http';
import https from 'node:https';
import type { ParsedUrlQueryInput } from 'node:querystring';
import url from 'node:url';
import type { Cheerio, CheerioAPI } from 'cheerio';
import { load } from 'cheerio';
export type CasOptions = {
base_url: string;
service?: string;
version: 1.0 | 2.0;
};
export type CasCallbackExtendedData = {
username?: string;
attributes?: Record<string, string[]>;
PGTIOU?: string;
ticket?: string;
proxies?: string[];
};
export type CasCallback = (err: any, status?: unknown, username?: string, extended?: CasCallbackExtendedData) => void;
function parseJasigAttributes(elemAttribute: Cheerio<any>, cheerio: CheerioAPI): Record<string, string[]> {
// "Jasig Style" Attributes:
//
// <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
// <cas:authenticationSuccess>
// <cas:user>jsmith</cas:user>
// <cas:attributes>
// <cas:attraStyle>RubyCAS</cas:attraStyle>
// <cas:surname>Smith</cas:surname>
// <cas:givenName>John</cas:givenName>
// <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
// <cas:memberOf>CN=Spanish Department,OU=Departments,...</cas:memberOf>
// </cas:attributes>
// <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2...</cas:proxyGrantingTicket>
// </cas:authenticationSuccess>
// </cas:serviceResponse>
const attributes: Record<string, string[]> = {};
for (let i = 0; i < elemAttribute.children().length; i++) {
const node = elemAttribute.children()[i];
const attrName = node.name.toLowerCase().replace(/cas:/, '');
if (attrName !== '#text') {
const attrValue = cheerio(node).text();
if (!attributes[attrName]) {
attributes[attrName] = [attrValue];
} else {
attributes[attrName].push(attrValue);
}
}
}
return attributes;
}
function parseRubyCasAttributes(elemSuccess: Cheerio<any>, cheerio: CheerioAPI): Record<string, string[]> {
// "RubyCAS Style" attributes
//
// <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
// <cas:authenticationSuccess>
// <cas:user>jsmith</cas:user>
//
// <cas:attraStyle>RubyCAS</cas:attraStyle>
// <cas:surname>Smith</cas:surname>
// <cas:givenName>John</cas:givenName>
// <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
// <cas:memberOf>CN=Spanish Department,OU=Departments,...</cas:memberOf>
//
// <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2...</cas:proxyGrantingTicket>
// </cas:authenticationSuccess>
// </cas:serviceResponse>
const attributes: Record<string, string[]> = {};
for (let i = 0; i < elemSuccess.children().length; i++) {
const node = elemSuccess.children()[i];
const tagName = node.name.toLowerCase().replace(/cas:/, '');
switch (tagName) {
case 'user':
case 'proxies':
case 'proxygrantingticket':
case '#text':
// these are not CAS attributes
break;
default:
const attrName = tagName;
const attrValue = cheerio(node).text();
if (attrValue !== '') {
if (!attributes[attrName]) {
attributes[attrName] = [attrValue];
} else {
attributes[attrName].push(attrValue);
}
}
break;
}
}
return attributes;
}
function parseAttributes(elemSuccess: Cheerio<any>, cheerio: CheerioAPI): Record<string, string[]> {
const elemAttribute = elemSuccess.find('cas\\:attributes').first();
const isJasig = elemAttribute?.children().length > 0;
const attributes = isJasig ? parseJasigAttributes(elemAttribute, cheerio) : parseRubyCasAttributes(elemSuccess, cheerio);
if (Object.keys(attributes).length > 0) {
return attributes;
}
// "Name-Value" attributes.
//
// Attribute format from this mailing list thread:
// http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html
// Note: This is a less widely used format, but in use by at least two institutions.
//
// <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
// <cas:authenticationSuccess>
// <cas:user>jsmith</cas:user>
//
// <cas:attribute name='attraStyle' value='Name-Value' />
// <cas:attribute name='surname' value='Smith' />
// <cas:attribute name='givenName' value='John' />
// <cas:attribute name='memberOf' value='CN=Staff,OU=Groups,DC=example,DC=edu' />
// <cas:attribute name='memberOf' value='CN=Spanish Department,OU=Departments,...' />
//
// <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
// </cas:authenticationSuccess>
// </cas:serviceResponse>
//
const nodes = elemSuccess.find('cas\\:attribute');
if (nodes?.length) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const attrName = node.attribs.name;
const attrValue = node.attribs.value;
if (!attributes[attrName]) {
attributes[attrName] = [attrValue];
} else {
attributes[attrName].push(attrValue);
}
}
}
return attributes;
}
export function getQueryPath(
partialPathname: string,
validatePath: string,
query: string | ParsedUrlQueryInput | null | undefined,
): string {
const pathname = partialPathname?.endsWith('/') ? partialPathname + validatePath : `${partialPathname}/${validatePath}`;
return url.format({
pathname,
query,
});
}
export function validate(options: CasOptions, ticket: string, callback: CasCallback, renew = false): void {
if (!options.base_url) {
throw new Error('Required CAS option `base_url` missing.');
}
const casUrl = url.parse(options.base_url);
if (casUrl.protocol !== 'https:') {
throw new Error('Only https CAS servers are supported.');
}
if (!casUrl.hostname) {
throw new Error('Option `base_url` must be a valid url like: https://example.com/cas');
}
const { service, version = 1.0 } = options;
if (!service) {
throw new Error('Required CAS option `service` missing.');
}
const { hostname, port = '443', pathname = '' } = casUrl;
const validatePath = version < 2.0 ? 'validate' : 'proxyValidate';
const query = {
ticket,
service,
...(renew ? { renew: 1 } : {}),
};
const queryPath = getQueryPath(pathname ?? '/', validatePath, query);
const req = https.get(
{
host: hostname,
port,
path: queryPath,
rejectUnauthorized: true,
},
function (res: IncomingMessage) {
// Handle server errors
res.on('error', function (e) {
callback(e);
});
// Read result
res.setEncoding('utf8');
let response = '';
res.on('data', function (chunk) {
response += chunk;
if (response.length > 1e6) {
req.connection?.destroy();
}
});
res.on('end', function () {
if (version < 2.0) {
const sections = response.split('\n');
if (sections.length >= 1) {
switch (sections[0]) {
case 'no':
return callback(undefined, false);
case 'yes':
if (sections.length >= 2) {
return callback(undefined, true, sections[1]);
}
}
}
return callback(new Error('Bad response format.'));
}
// Use cheerio to parse the XML repsonse.
const cheerio = load(response);
// Check for auth success
const elemSuccess = cheerio('cas\\:authenticationSuccess').first();
if (elemSuccess && elemSuccess.length > 0) {
const elemUser = elemSuccess.find('cas\\:user').first();
if (!elemUser || elemUser.length < 1) {
// This should never happen
callback(new Error('No username?'), false);
return;
}
// Got username
const username = elemUser.text();
// Look for optional proxy granting ticket
let pgtIOU;
const elemPGT = elemSuccess.find('cas\\:proxyGrantingTicket').first();
if (elemPGT) {
pgtIOU = elemPGT.text();
}
// Look for optional proxies
const proxies = [];
const elemProxies = elemSuccess.find('cas\\:proxies');
for (let i = 0; i < elemProxies.length; i++) {
proxies.push(cheerio(elemProxies[i]).text().trim());
}
// Look for optional attributes
const attributes = parseAttributes(elemSuccess, cheerio);
callback(undefined, true, username, {
username,
attributes,
PGTIOU: pgtIOU,
ticket,
proxies,
});
return;
} // end if auth success
// Check for correctly formatted auth failure message
const elemFailure = cheerio('cas\\:authenticationFailure').first();
if (elemFailure && elemFailure.length > 0) {
const code = elemFailure.attr('code');
const message = `Validation failed [${code}]: ${elemFailure.text()}`;
callback(new Error(message), false);
return;
}
// The response was not in any expected format, error
callback(new Error('Bad response format.'));
console.error(response);
});
},
);
// Connection error with the CAS server
req.on('error', function (err) {
callback(err);
req.abort();
});
}

View File

@@ -0,0 +1,10 @@
{
"extends": "@rocket.chat/tsconfig/server.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src/**/*"]
}