Files
upstream-mirror 30810eecd7
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 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

862 lines
23 KiB
TypeScript

import type {
ILDAPConnectionOptions,
LDAPEncryptionType,
LDAPSearchScope,
ILDAPEntry,
ILDAPCallback,
ILDAPPageCallback,
} from '@rocket.chat/core-typings';
import { wrapExceptions } from '@rocket.chat/tools';
import ldapjs from 'ldapjs';
import { logger, connLogger, searchLogger, authLogger, bindLogger, mapLogger } from './Logger';
import { getLDAPConditionalSetting } from './getLDAPConditionalSetting';
import { processLdapVariables, type LDAPVariableMap } from './processLdapVariables';
import { ensureArray } from '../../../lib/utils/arrayUtils';
import { settings } from '../../settings';
interface ILDAPEntryCallback<T> {
(entry: ldapjs.SearchEntry): T | undefined;
}
interface ILDAPSearchEndCallback {
(error?: any): void;
}
interface ILDAPSearchPageCallback {
(result: ldapjs.SearchEntry[]): void;
}
interface ILDAPSearchAllCallbacks<T> {
dataCallback?: ILDAPSearchPageCallback;
endCallback?: ILDAPSearchEndCallback;
entryCallback?: ILDAPEntryCallback<T>;
}
type ILDAPExtractedValue = string | Array<ILDAPExtractedValue>;
export class LDAPConnection {
public ldapjs: any;
public connected: boolean;
public options: ILDAPConnectionOptions;
public client: ldapjs.Client;
private _receivedResponse: boolean;
private _connectionTimedOut: boolean;
private _connectionCallback: ILDAPCallback;
private usingAuthentication: boolean;
private _variableMap: LDAPVariableMap;
constructor() {
this.ldapjs = ldapjs;
this.connected = false;
this._receivedResponse = false;
this._connectionTimedOut = false;
this.options = {
host: settings.get<string>('LDAP_Host') ?? '',
port: settings.get<number>('LDAP_Port') ?? 389,
reconnect: settings.get<boolean>('LDAP_Reconnect') ?? false,
timeout: settings.get<number>('LDAP_Timeout') ?? 60000,
connectionTimeout: settings.get<number>('LDAP_Connect_Timeout') ?? 1000,
idleTimeout: settings.get<number>('LDAP_Idle_Timeout') ?? 1000,
encryption: settings.get<LDAPEncryptionType>('LDAP_Encryption') ?? 'plain',
caCert: settings.get<string>('LDAP_CA_Cert'),
rejectUnauthorized: settings.get<boolean>('LDAP_Reject_Unauthorized') || false,
baseDN: settings.get<string>('LDAP_BaseDN') ?? '',
userSearchFilter: settings.get<string>('LDAP_User_Search_Filter') ?? '',
userSearchScope: settings.get<LDAPSearchScope>('LDAP_User_Search_Scope') ?? 'sub',
userSearchField: getLDAPConditionalSetting<string>('LDAP_User_Search_Field') ?? '',
searchPageSize: settings.get<number>('LDAP_Search_Page_Size') ?? 250,
searchSizeLimit: settings.get<number>('LDAP_Search_Size_Limit') ?? 1000,
uniqueIdentifierField: settings.get<string>('LDAP_Unique_Identifier_Field'),
groupFilterEnabled: settings.get<boolean>('LDAP_Group_Filter_Enable') ?? false,
groupFilterObjectClass: settings.get<string>('LDAP_Group_Filter_ObjectClass'),
groupFilterGroupIdAttribute: settings.get<string>('LDAP_Group_Filter_Group_Id_Attribute'),
groupFilterGroupMemberAttribute: settings.get<string>('LDAP_Group_Filter_Group_Member_Attribute'),
groupFilterGroupMemberFormat: settings.get<string>('LDAP_Group_Filter_Group_Member_Format'),
groupFilterGroupName: settings.get<string>('LDAP_Group_Filter_Group_Name'),
authentication: settings.get<boolean>('LDAP_Authentication') ?? false,
authenticationUserDN: settings.get<string>('LDAP_Authentication_UserDN') ?? '',
authenticationPassword: settings.get<string>('LDAP_Authentication_Password') ?? '',
useVariables: settings.get<boolean>('LDAP_DataSync_UseVariables') ?? false,
variableMap: settings.get<string>('LDAP_DataSync_VariableMap') ?? '{}',
attributesToQuery: this.parseAttributeList(settings.get<string>('LDAP_User_Search_AttributesToQuery')),
};
this._variableMap =
(this.options.useVariables &&
wrapExceptions(() => JSON.parse(this.options.variableMap)).suppress(() => {
mapLogger.error({ msg: 'Failed to parse LDAP Variable Map', map: this.options.variableMap });
})) ||
{};
if (!this.options.host) {
logger.warn('LDAP Host is not configured.');
}
if (!this.options.baseDN) {
logger.warn('LDAP Search BaseDN is not configured.');
}
}
public async connect(): Promise<any> {
return new Promise((resolve, reject) => {
this.initializeConnection((error, result) => {
if (error) {
return reject(error);
}
return resolve(result);
});
});
}
public disconnect(): void {
this.usingAuthentication = false;
this.connected = false;
connLogger.info('Disconnecting');
if (this.client) {
this.client.unbind();
}
}
public async testConnection(): Promise<void> {
try {
await this.connect();
await this.maybeBindDN();
} finally {
this.disconnect();
}
}
public async searchByUsername(escapedUsername: string): Promise<ILDAPEntry[]> {
const searchOptions: ldapjs.SearchOptions = {
filter: this.getUserFilter(escapedUsername),
scope: this.options.userSearchScope || 'sub',
sizeLimit: this.options.searchSizeLimit,
attributes: this.options.attributesToQuery,
};
if (this.options.searchPageSize > 0) {
searchOptions.paged = {
pageSize: this.options.searchPageSize,
pagePause: false,
};
}
searchLogger.info({
msg: 'Searching by username',
username: escapedUsername,
baseDN: this.options.baseDN,
searchOptions,
});
return this.search(this.options.baseDN, searchOptions);
}
public async findOneByUsername(username: string): Promise<ILDAPEntry | undefined> {
const results = await this.searchByUsername(username);
if (results.length === 1) {
return results[0];
}
}
public async searchById(id: string, attribute?: string): Promise<ILDAPEntry[]> {
const searchOptions: ldapjs.SearchOptions = {
scope: this.options.userSearchScope || 'sub',
attributes: this.options.attributesToQuery,
};
if (attribute) {
searchOptions.filter = new this.ldapjs.filters.EqualityFilter({
attribute,
value: Buffer.from(id, 'hex'),
});
} else if (this.options.uniqueIdentifierField) {
// If we don't know what attribute the id came from, we have to look for all of them.
const possibleFields = this.options.uniqueIdentifierField.split(',').concat(this.options.userSearchField.split(','));
const filters = [];
for (const field of possibleFields) {
if (!field) {
continue;
}
filters.push(
new this.ldapjs.filters.EqualityFilter({
attribute: field,
value: Buffer.from(id, 'hex'),
}),
);
}
searchOptions.filter = new this.ldapjs.filters.OrFilter({ filters });
} else {
throw new Error('Unique Identifier Field is not configured.');
}
searchLogger.info({ msg: 'Searching by id', id });
searchLogger.debug({ msg: 'search filter', searchOptions, baseDN: this.options.baseDN });
return this.search(this.options.baseDN, searchOptions);
}
public async findOneById(id: string, attribute?: string): Promise<ILDAPEntry | undefined> {
const results = await this.searchById(id, attribute);
if (results.length === 1) {
return results[0];
}
}
public async searchAllUsers<T = ldapjs.SearchEntry>({
dataCallback,
endCallback,
entryCallback,
}: ILDAPSearchAllCallbacks<T>): Promise<void> {
searchLogger.info('Searching all users');
const searchOptions: ldapjs.SearchOptions = {
filter: this.getUserFilter('*'),
scope: this.options.userSearchScope || 'sub',
sizeLimit: this.options.searchSizeLimit,
attributes: this.options.attributesToQuery,
};
if (this.options.searchPageSize > 0) {
let count = 0;
await this.doPagedSearch<T>(
this.options.baseDN,
searchOptions,
this.options.searchPageSize,
(error, entries: ldapjs.SearchEntry[], { end, next } = { end: false, next: undefined }) => {
if (error) {
endCallback?.(error);
return;
}
count += entries.length;
dataCallback?.(entries);
if (end) {
endCallback?.();
}
if (next) {
next(count);
}
},
entryCallback,
);
return;
}
await this.doAsyncSearch(
this.options.baseDN,
searchOptions,
(error, result) => {
dataCallback?.(result);
endCallback?.(error);
},
entryCallback,
);
}
public async authenticate(dn: string, password: string): Promise<boolean> {
authLogger.info({ msg: 'Authenticating', dn });
try {
await this.bindDN(dn, password);
authLogger.info({ msg: 'Authenticated', dn });
return true;
} catch (error) {
authLogger.info({ msg: 'Not authenticated', dn });
authLogger.debug({ msg: 'error', error });
return false;
}
}
public async search(baseDN: string, searchOptions: ldapjs.SearchOptions): Promise<ILDAPEntry[]> {
return this.doCustomSearch<ILDAPEntry>(baseDN, searchOptions, (entry) => this.extractLdapEntryData(entry));
}
public async searchRaw(baseDN: string, searchOptions: ldapjs.SearchOptions): Promise<ldapjs.SearchEntry[]> {
return this.doCustomSearch<ldapjs.SearchEntry>(baseDN, searchOptions, (entry) => entry);
}
public async searchAndCount(baseDN: string, searchOptions: ldapjs.SearchOptions): Promise<number> {
let count = 0;
await this.doCustomSearch(baseDN, searchOptions, async () => {
count++;
});
return count;
}
public extractLdapAttribute(value: Buffer | Buffer[] | string): ILDAPExtractedValue {
if (Array.isArray(value)) {
return value.map((item) => this.extractLdapAttribute(item));
}
if (typeof value === 'string') {
return value;
}
return value.toString();
}
public extractLdapEntryData(entry: ldapjs.SearchEntry): ILDAPEntry {
const values: ILDAPEntry = {
_raw: entry.raw,
};
Object.keys(values._raw).forEach((key) => {
values[key] = this.extractLdapAttribute(values._raw[key]);
const dataType = typeof values[key];
// eslint-disable-next-line no-control-regex
if (dataType === 'string' && values[key].length > 100 && /[\x00-\x1F]/.test(values[key])) {
mapLogger.debug({
msg: 'Extracted Attribute',
key,
type: dataType,
length: values[key].length,
value: `${values[key].substr(0, 100)}...`,
});
return;
}
mapLogger.debug({ msg: 'Extracted Attribute', key, type: dataType, value: values[key] });
});
return processLdapVariables(values, this._variableMap);
}
public async doCustomSearch<T>(baseDN: string, searchOptions: ldapjs.SearchOptions, entryCallback: ILDAPEntryCallback<T>): Promise<T[]> {
await this.runBeforeSearch(searchOptions);
if (!searchOptions.scope) {
searchOptions.scope = this.options.userSearchScope || 'sub';
}
searchLogger.debug({ msg: 'searchOptions', searchOptions, baseDN });
let realEntries = 0;
return new Promise((resolve, reject) => {
this.clientSearch(baseDN, searchOptions, (err, res: ldapjs.SearchCallbackResponse) => {
if (err) {
searchLogger.error({ err });
reject(err);
return;
}
res.on('error', (err) => {
searchLogger.error({ err });
reject(err);
});
const entries: T[] = [];
res.on('searchEntry', (entry) => {
try {
const result = entryCallback(entry);
if (result) {
entries.push(result as T);
}
realEntries++;
} catch (err) {
searchLogger.error({ err });
}
});
res.on('end', () => {
searchLogger.info({
msg: 'LDAP search completed',
foundEntries: realEntries,
loadedEntries: entries.length,
});
resolve(entries);
});
});
});
}
/*
Create an LDAP search filter based on the username
*/
public getUserFilter(username: string): string {
const filter: string[] = [];
this.addUserFilters(filter, username);
const fields = this.options.userSearchField
.split(',')
.map((field) => field.trim())
.filter(Boolean);
if (!fields.length) {
throw new Error('LDAP User Search Field is not configured');
}
const usernameFilter = fields.map((field) => `(${field}=${username})`);
if (usernameFilter.length === 1) {
filter.push(`${usernameFilter[0]}`);
} else {
filter.push(`(|${usernameFilter.join('')})`);
}
return `(&${filter.join('')})`;
}
public async searchMembersOfGroupFilter(): Promise<string[]> {
if (!this.options.groupFilterEnabled) {
return [];
}
if (!this.options.groupFilterGroupMemberAttribute) {
return [];
}
if (!this.options.groupFilterGroupMemberFormat) {
searchLogger.debug('LDAP Group Filter is enabled but no group member format is set.');
return [];
}
const filter = ['(&'];
if (this.options.groupFilterObjectClass) {
filter.push(`(objectclass=${this.options.groupFilterObjectClass})`);
}
if (this.options.groupFilterGroupIdAttribute) {
filter.push(`(${this.options.groupFilterGroupIdAttribute}=${this.options.groupFilterGroupName})`);
}
filter.push(')');
const searchOptions: ldapjs.SearchOptions = {
filter: filter.join(''),
scope: 'sub',
};
searchLogger.debug({ msg: 'Group filter LDAP:', filter: searchOptions.filter });
const result = await this.searchRaw(this.options.baseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
searchLogger.debug({ msg: 'No groups found', result });
return [];
}
const members = this.extractLdapAttribute(result[0].raw[this.options.groupFilterGroupMemberAttribute]) as string | string[];
return ensureArray<string>(members);
}
public async isUserAcceptedByGroupFilter(username: string, userdn: string): Promise<boolean> {
if (!this.options.groupFilterEnabled) {
return true;
}
const filter = ['(&'];
if (this.options.groupFilterObjectClass) {
filter.push(`(objectclass=${this.options.groupFilterObjectClass})`);
}
if (this.options.groupFilterGroupMemberAttribute) {
filter.push(`(${this.options.groupFilterGroupMemberAttribute}=${this.options.groupFilterGroupMemberFormat})`);
}
if (this.options.groupFilterGroupIdAttribute) {
filter.push(`(${this.options.groupFilterGroupIdAttribute}=${this.options.groupFilterGroupName})`);
}
filter.push(')');
const searchOptions: ldapjs.SearchOptions = {
filter: filter
.join('')
.replace(/#{username}/g, username)
.replace(/#{userdn}/g, userdn),
scope: 'sub',
};
searchLogger.debug({ msg: 'Group filter LDAP:', filter: searchOptions.filter });
const result = await this.searchAndCount(this.options.baseDN, searchOptions);
if (result === 0) {
return false;
}
return true;
}
protected addUserFilters(filters: string[], _username: string): void {
const { userSearchFilter } = this.options;
if (userSearchFilter !== '') {
if (userSearchFilter[0] === '(') {
filters.push(`${userSearchFilter}`);
} else {
filters.push(`(${userSearchFilter})`);
}
}
}
public async bindDN(dn: string, password: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
this.client.bind(dn, password, (error) => {
if (error) {
return reject(error);
}
resolve();
});
} catch (error) {
reject(error);
}
});
}
private clientSearch(baseDN: string, searchOptions: ldapjs.SearchOptions, callback: ldapjs.SearchCallBack): void {
try {
this.client.search(baseDN, searchOptions, callback);
} catch (err) {
callback(err as ldapjs.Error, undefined as unknown as ldapjs.SearchCallbackResponse);
}
}
private async doAsyncSearch<T = ldapjs.SearchEntry>(
baseDN: string,
searchOptions: ldapjs.SearchOptions,
callback: ILDAPCallback,
entryCallback?: ILDAPEntryCallback<T>,
): Promise<void> {
await this.runBeforeSearch(searchOptions);
searchLogger.debug({ msg: 'searchOptions', searchOptions, baseDN });
this.clientSearch(baseDN, searchOptions, (err: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse): void => {
if (err) {
searchLogger.error({ err });
callback(err);
return;
}
res.on('error', (err) => {
searchLogger.error({ err });
callback(err);
});
const entries: T[] = [];
res.on('searchEntry', (entry) => {
try {
const result = entryCallback ? entryCallback(entry) : entry;
entries.push(result as T);
} catch (err) {
searchLogger.error({ err });
}
});
res.on('end', () => {
searchLogger.info({ msg: 'Search result count', count: entries.length });
callback(null, entries);
});
});
}
private processSearchPage<T>(
{ entries, title, end, next }: { entries: T[]; title: string; end: boolean; next?: () => void },
callback: ILDAPPageCallback,
): void {
searchLogger.info(title);
// Force LDAP idle to wait the record processing
this._updateIdle(true);
callback(null, entries, {
end,
next: () => {
// Reset idle timer
this._updateIdle();
next?.();
},
});
}
private async doPagedSearch<T = ldapjs.SearchEntry>(
baseDN: string,
searchOptions: ldapjs.SearchOptions,
pageSize: number,
callback: ILDAPPageCallback,
entryCallback?: ILDAPEntryCallback<T>,
): Promise<void> {
searchOptions.paged = {
pageSize,
pagePause: true,
};
await this.runBeforeSearch(searchOptions);
searchLogger.debug({ msg: 'searchOptions', searchOptions, baseDN });
this.clientSearch(baseDN, searchOptions, (err: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse): void => {
if (err) {
searchLogger.error({ err });
callback(err);
return;
}
res.on('error', (err) => {
searchLogger.error({ err });
callback(err);
});
let entries: T[] = [];
const internalPageSize = pageSize * 2;
res.on('searchEntry', (entry) => {
try {
const result = entryCallback ? entryCallback(entry) : entry;
entries.push(result as T);
if (entries.length >= internalPageSize) {
this.processSearchPage<T>(
{
entries,
title: 'Internal Page',
end: false,
},
callback,
);
entries = [];
}
} catch (err) {
searchLogger.error({ err });
}
});
res.on('page', (_result, next) => {
if (!next) {
this._updateIdle(true);
this.processSearchPage<T>(
{
entries,
title: 'Final Page',
end: true,
},
callback,
);
entries = [];
} else if (entries.length) {
this.processSearchPage<T>(
{
entries,
title: 'Page',
end: false,
next,
},
callback,
);
entries = [];
}
});
res.on('end', () => {
if (entries.length) {
this.processSearchPage<T>(
{
entries,
title: 'Final Page',
end: true,
},
callback,
);
entries = [];
}
});
});
}
private _updateIdle(override?: boolean): void {
// @ts-expect-error use a private function to signal to the lib that we're still working
this.client._updateIdle(override);
}
protected async maybeBindDN({ forceBindAuthenticationUser = false } = {}): Promise<void> {
if (!forceBindAuthenticationUser && (this.usingAuthentication || !this.options.authentication)) {
return;
}
if (!this.options.authenticationUserDN) {
logger.error('Invalid UserDN for authentication');
return;
}
bindLogger.info({ msg: 'Binding UserDN', userDN: this.options.authenticationUserDN });
try {
await this.bindDN(this.options.authenticationUserDN, this.options.authenticationPassword);
this.usingAuthentication = true;
} catch (error) {
authLogger.error({
msg: 'Base Authentication Issue',
err: error,
dn: this.options.authenticationUserDN,
});
this.usingAuthentication = false;
}
}
protected async runBeforeSearch(_searchOptions: ldapjs.SearchOptions): Promise<void> {
return this.maybeBindDN();
}
public async bindAuthenticationUser(): Promise<void> {
return this.maybeBindDN({ forceBindAuthenticationUser: true });
}
/*
Get list of options to initialize a new ldapjs Client
*/
private getClientOptions(): {
clientOptions: ldapjs.ClientOptions;
tlsOptions: Record<string, any>;
} {
const clientOptions: ldapjs.ClientOptions = {
url: `${this.options.host}:${this.options.port}`,
timeout: this.options.timeout,
connectTimeout: this.options.connectionTimeout,
idleTimeout: this.options.idleTimeout,
reconnect: this.options.reconnect,
};
const tlsOptions: Record<string, any> = {
rejectUnauthorized: this.options.rejectUnauthorized,
};
if (this.options.caCert) {
// Split CA cert into array of strings
const chainLines = this.options.caCert.split('\n');
let cert: string[] = [];
const ca: string[] = [];
chainLines.forEach((line) => {
cert.push(line);
if (line.match(/-END CERTIFICATE-/)) {
ca.push(cert.join('\n'));
cert = [];
}
});
tlsOptions.ca = ca;
}
if (this.options.encryption === 'ssl') {
clientOptions.url = `ldaps://${clientOptions.url}`;
clientOptions.tlsOptions = tlsOptions;
} else {
clientOptions.url = `ldap://${clientOptions.url}`;
}
return {
clientOptions,
tlsOptions,
};
}
private handleConnectionResponse(err: any, response?: any): void {
if (!this._receivedResponse) {
this._receivedResponse = true;
this._connectionCallback(err, response);
return;
}
if (this._connectionTimedOut && !err) {
connLogger.info('Received a response after the connection timedout.');
} else {
logger.debug('Ignored error/response:');
}
connLogger.debug({ err, response });
}
private initializeConnection(callback: ILDAPCallback): void {
connLogger.info('Init Setup');
this._receivedResponse = false;
this._connectionTimedOut = false;
this._connectionCallback = callback;
const { clientOptions, tlsOptions } = this.getClientOptions();
connLogger.info({ msg: 'Connecting', url: clientOptions.url });
connLogger.debug({ msg: 'clientOptions', clientOptions });
this.client = ldapjs.createClient(clientOptions);
this.client.on('error', (error) => {
connLogger.error({ err: error });
this.handleConnectionResponse(error, null);
});
this.client.on('idle', () => {
searchLogger.info('Idle');
this.disconnect();
});
this.client.on('close', () => {
searchLogger.info('Closed');
});
if (this.options.encryption === 'tls') {
// Set host parameter for tls.connect which is used by ldapjs starttls. This may not be needed anymore
// https://github.com/RocketChat/Rocket.Chat/issues/2035
// https://github.com/mcavage/node-ldapjs/issues/349
tlsOptions.host = this.options.host;
connLogger.info('Starting TLS');
connLogger.debug({ msg: 'tlsOptions', tlsOptions });
this.client.starttls(tlsOptions, null, (err, response) => {
if (err) {
connLogger.error({ msg: 'TLS connection', err });
return this.handleConnectionResponse(err, null);
}
connLogger.info('TLS connected');
this.connected = true;
this.handleConnectionResponse(null, response);
});
} else {
this.client.on('connect', (response) => {
connLogger.info('LDAP connected');
this.connected = true;
this.handleConnectionResponse(null, response);
});
}
setTimeout(() => {
if (!this._receivedResponse) {
connLogger.error({ msg: 'connection time out', timeout: clientOptions.connectTimeout });
this.handleConnectionResponse(new Error('Timeout'));
this._connectionTimedOut = true;
}
}, clientOptions.connectTimeout);
}
private parseAttributeList(csv: string | undefined): Array<string> {
if (!csv) {
return ['*', '+'];
}
const list = csv.split(',').map((item) => item.trim());
if (!list?.length) {
return ['*', '+'];
}
return list;
}
}