mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
Merge 0228e37fc7 into 8ed0bcf5ca
This commit is contained in:
commit
5b43ce5a66
2 changed files with 222 additions and 0 deletions
|
|
@ -5,6 +5,9 @@ const { CacheKeys } = require('librechat-data-provider');
|
|||
const { Client } = require('@microsoft/microsoft-graph-client');
|
||||
const { getOpenIdConfig } = require('~/strategies/openidStrategy');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const nodeFetch = require('node-fetch');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { ProxyAgent } = require('undici');
|
||||
|
||||
/**
|
||||
* @import { TPrincipalSearchResult, TGraphPerson, TGraphUser, TGraphGroup, TGraphPeopleResponse, TGraphUsersResponse, TGraphGroupsResponse } from 'librechat-data-provider'
|
||||
|
|
@ -38,10 +41,16 @@ const createGraphClient = async (accessToken, sub) => {
|
|||
const openidConfig = getOpenIdConfig();
|
||||
const exchangedToken = await exchangeTokenForGraphAccess(openidConfig, accessToken, sub);
|
||||
|
||||
const fetchOptions = {};
|
||||
// Add proxy support if configured
|
||||
if (process.env.PROXY && process.env.PROXY.trim()) {
|
||||
fetchOptions.dispatcher = new ProxyAgent(process.env.PROXY);
|
||||
}
|
||||
const graphClient = Client.init({
|
||||
authProvider: (done) => {
|
||||
done(null, exchangedToken);
|
||||
},
|
||||
fetchOptions,
|
||||
});
|
||||
|
||||
return graphClient;
|
||||
|
|
@ -75,6 +84,14 @@ const exchangeTokenForGraphAccess = async (config, accessToken, sub) => {
|
|||
.map((scope) => `https://graph.microsoft.com/${scope}`)
|
||||
.join(' ');
|
||||
|
||||
const clientOptions = {};
|
||||
if (process.env.PROXY && process.env.PROXY.trim()) {
|
||||
const httpsAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
clientOptions[Symbol.for('openid-client.custom.fetch')] = (url, options = {}) => {
|
||||
return nodeFetch(url, { ...options, agent: httpsAgent });
|
||||
};
|
||||
}
|
||||
|
||||
const grantResponse = await client.genericGrantRequest(
|
||||
config,
|
||||
'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
|
|
@ -83,6 +100,7 @@ const exchangeTokenForGraphAccess = async (config, accessToken, sub) => {
|
|||
assertion: accessToken,
|
||||
requested_token_use: 'on_behalf_of',
|
||||
},
|
||||
clientOptions,
|
||||
);
|
||||
|
||||
await tokensCache.set(
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ describe('GraphApiService', () => {
|
|||
afterEach(() => {
|
||||
// Clean up environment variables
|
||||
delete process.env.OPENID_GRAPH_SCOPES;
|
||||
delete process.env.PROXY;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -153,6 +154,7 @@ describe('GraphApiService', () => {
|
|||
expect(getOpenIdConfig).toHaveBeenCalled();
|
||||
expect(Client.init).toHaveBeenCalledWith({
|
||||
authProvider: expect.any(Function),
|
||||
fetchOptions: {},
|
||||
});
|
||||
expect(result).toBe(mockGraphClient);
|
||||
});
|
||||
|
|
@ -205,6 +207,7 @@ describe('GraphApiService', () => {
|
|||
assertion: 'test-token',
|
||||
requested_token_use: 'on_behalf_of',
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +242,7 @@ describe('GraphApiService', () => {
|
|||
assertion: 'test-token',
|
||||
requested_token_use: 'on_behalf_of',
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -800,4 +804,204 @@ describe('GraphApiService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Proxy Configuration', () => {
|
||||
let originalEnv;
|
||||
const { ProxyAgent } = require('undici');
|
||||
|
||||
beforeAll(() => {
|
||||
originalEnv = { ...process.env };
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('Graph Client Proxy Configuration', () => {
|
||||
it('should configure ProxyAgent dispatcher in Graph Client when PROXY env is set', async () => {
|
||||
process.env.PROXY = 'http://proxy.example.com:8080';
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await GraphApiService.createGraphClient('test-token', 'test-user');
|
||||
|
||||
expect(Client.init).toHaveBeenCalled();
|
||||
const initCall = Client.init.mock.calls[0][0];
|
||||
expect(initCall).toHaveProperty('fetchOptions');
|
||||
expect(initCall.fetchOptions).toHaveProperty('dispatcher');
|
||||
expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||
});
|
||||
|
||||
it('should not configure ProxyAgent dispatcher when PROXY env is not set', async () => {
|
||||
delete process.env.PROXY;
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await GraphApiService.createGraphClient('test-token', 'test-user');
|
||||
|
||||
expect(Client.init).toHaveBeenCalled();
|
||||
const initCall = Client.init.mock.calls[0][0];
|
||||
|
||||
// fetchOptions should either not exist or not have a dispatcher
|
||||
if (initCall.fetchOptions) {
|
||||
expect(initCall.fetchOptions.dispatcher).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should create ProxyAgent dispatcher for any configured proxy URL', async () => {
|
||||
const proxyUrl = 'http://custom-proxy.example.com:3128';
|
||||
process.env.PROXY = proxyUrl;
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await GraphApiService.createGraphClient('test-token', 'test-user');
|
||||
|
||||
const initCall = Client.init.mock.calls[0][0];
|
||||
expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenID Client Proxy Configuration', () => {
|
||||
it('should configure HttpsProxyAgent for openid-client custom fetch when PROXY env is set', async () => {
|
||||
process.env.PROXY = 'http://proxy.example.com:8080';
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await GraphApiService.exchangeTokenForGraphAccess(
|
||||
mockOpenIdConfig,
|
||||
'test-token',
|
||||
'test-user',
|
||||
);
|
||||
|
||||
if (client.genericGrantRequest) {
|
||||
expect(client.genericGrantRequest).toHaveBeenCalled();
|
||||
const clientOptions = client.genericGrantRequest.mock.calls[0][3];
|
||||
|
||||
expect(clientOptions).toBeDefined();
|
||||
const customFetchSymbol = Symbol.for('openid-client.custom.fetch');
|
||||
expect(clientOptions[customFetchSymbol]).toBeDefined();
|
||||
expect(typeof clientOptions[customFetchSymbol]).toBe('function');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not configure custom fetch when PROXY env is not set', async () => {
|
||||
delete process.env.PROXY;
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await GraphApiService.exchangeTokenForGraphAccess(
|
||||
mockOpenIdConfig,
|
||||
'test-token',
|
||||
'test-user',
|
||||
);
|
||||
|
||||
if (client.genericGrantRequest) {
|
||||
expect(client.genericGrantRequest).toHaveBeenCalled();
|
||||
const clientOptions = client.genericGrantRequest.mock.calls[0][3];
|
||||
|
||||
// clientOptions should be empty object or not have custom fetch
|
||||
expect(clientOptions).toEqual({});
|
||||
}
|
||||
});
|
||||
|
||||
it('should use HttpsProxyAgent with correct proxy URL', async () => {
|
||||
const proxyUrl = 'http://custom-proxy.example.com:3128';
|
||||
process.env.PROXY = proxyUrl;
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await GraphApiService.exchangeTokenForGraphAccess(
|
||||
mockOpenIdConfig,
|
||||
'test-token',
|
||||
'test-user',
|
||||
);
|
||||
|
||||
if (client.genericGrantRequest) {
|
||||
const clientOptions = client.genericGrantRequest.mock.calls[0][3];
|
||||
const customFetch = clientOptions[Symbol.for('openid-client.custom.fetch')];
|
||||
|
||||
expect(customFetch).toBeDefined();
|
||||
expect(typeof customFetch).toBe('function');
|
||||
}
|
||||
});
|
||||
|
||||
it('should pass HttpsProxyAgent through custom fetch to nodeFetch', async () => {
|
||||
process.env.PROXY = 'http://proxy.example.com:8080';
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await GraphApiService.exchangeTokenForGraphAccess(
|
||||
mockOpenIdConfig,
|
||||
'test-token',
|
||||
'test-user',
|
||||
);
|
||||
|
||||
if (client.genericGrantRequest) {
|
||||
const clientOptions = client.genericGrantRequest.mock.calls[0][3];
|
||||
const customFetch = clientOptions[Symbol.for('openid-client.custom.fetch')];
|
||||
|
||||
// Test that custom fetch function is created with HttpsProxyAgent
|
||||
expect(customFetch).toBeDefined();
|
||||
expect(typeof customFetch).toBe('function');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Proxy Integration Tests', () => {
|
||||
it('should maintain proxy configuration throughout token exchange and graph client creation', async () => {
|
||||
process.env.PROXY = 'http://proxy.example.com:8080';
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
// First, exchange token (tests openid-client proxy)
|
||||
const graphToken = await GraphApiService.exchangeTokenForGraphAccess(
|
||||
mockOpenIdConfig,
|
||||
'test-token',
|
||||
'test-user',
|
||||
);
|
||||
|
||||
expect(graphToken).toBe('mocked-graph-token');
|
||||
|
||||
if (client.genericGrantRequest) {
|
||||
const clientOptions = client.genericGrantRequest.mock.calls[0][3];
|
||||
expect(clientOptions[Symbol.for('openid-client.custom.fetch')]).toBeDefined();
|
||||
}
|
||||
|
||||
// Then, create graph client (tests Graph API proxy)
|
||||
await GraphApiService.createGraphClient('test-token', 'test-user');
|
||||
|
||||
const initCall = Client.init.mock.calls[0][0];
|
||||
expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||
});
|
||||
|
||||
it('should handle different proxy protocols', async () => {
|
||||
const proxyProtocols = [
|
||||
'http://proxy.example.com:8080',
|
||||
'https://secure-proxy.example.com:8443',
|
||||
];
|
||||
|
||||
for (const proxyUrl of proxyProtocols) {
|
||||
process.env.PROXY = proxyUrl;
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
jest.clearAllMocks();
|
||||
|
||||
await GraphApiService.createGraphClient('test-token', 'test-user');
|
||||
|
||||
const initCall = Client.init.mock.calls[0][0];
|
||||
expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not fail when PROXY is set to empty string', async () => {
|
||||
process.env.PROXY = '';
|
||||
mockTokensCache.get.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
GraphApiService.createGraphClient('test-token', 'test-user'),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const initCall = Client.init.mock.calls[0][0];
|
||||
if (initCall.fetchOptions) {
|
||||
expect(initCall.fetchOptions.dispatcher).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue