mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-20 17:47:47 +01:00
Compare commits
2 Commits
v2026.2.3
...
fix/ntlm-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
626eac3b4c | ||
|
|
1f588d0498 |
@@ -607,6 +607,13 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
};
|
||||
|
||||
let headers_elapsed = duration_to_i32(started_at.elapsed());
|
||||
std::fs::create_dir_all(params.response_dir).map_err(|source| {
|
||||
SendHttpRequestError::CreateResponseDirectory {
|
||||
path: params.response_dir.to_path_buf(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let body_path = params.response_dir.join(&response.id);
|
||||
let connected_response = HttpResponse {
|
||||
state: HttpResponseState::Connected,
|
||||
elapsed_headers: headers_elapsed,
|
||||
@@ -616,6 +623,8 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
remote_addr: http_response.remote_addr.clone(),
|
||||
version: http_response.version.clone(),
|
||||
elapsed_dns: dns_elapsed.load(Ordering::Relaxed),
|
||||
body_path: Some(body_path.to_string_lossy().to_string()),
|
||||
content_length: http_response.content_length.map(u64_to_i32),
|
||||
headers: http_response
|
||||
.headers
|
||||
.iter()
|
||||
@@ -638,14 +647,6 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
response = connected_response;
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(params.response_dir).map_err(|source| {
|
||||
SendHttpRequestError::CreateResponseDirectory {
|
||||
path: params.response_dir.to_path_buf(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
let body_path = params.response_dir.join(&response.id);
|
||||
let mut file =
|
||||
File::options().create(true).truncate(true).write(true).open(&body_path).await.map_err(
|
||||
|source| SendHttpRequestError::WriteResponseBody { path: body_path.clone(), source },
|
||||
@@ -689,6 +690,10 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
file.write_all(&response_body[start_idx..]).await.map_err(|source| {
|
||||
SendHttpRequestError::WriteResponseBody { path: body_path.clone(), source }
|
||||
})?;
|
||||
file.flush().await.map_err(|source| SendHttpRequestError::WriteResponseBody {
|
||||
path: body_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let now = Instant::now();
|
||||
let should_update = now.duration_since(last_progress_update).as_millis()
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
"dev": "yaakcli dev",
|
||||
"test": "vitest --run tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"httpntlm": "^1.8.13"
|
||||
|
||||
@@ -2,6 +2,16 @@ import type { PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
import { ntlm } from 'httpntlm';
|
||||
|
||||
function extractNtlmChallenge(headers: Array<{ name: string; value: string }>): string | null {
|
||||
const authValues = headers
|
||||
.filter((h) => h.name.toLowerCase() === 'www-authenticate')
|
||||
.flatMap((h) => h.value.split(','))
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return authValues.find((v) => /^NTLM\s+\S+/i.test(v)) ?? null;
|
||||
}
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
authentication: {
|
||||
name: 'windows',
|
||||
@@ -68,15 +78,12 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
});
|
||||
|
||||
const wwwAuthenticateHeader = negotiateResponse.headers.find(
|
||||
(h) => h.name.toLowerCase() === 'www-authenticate',
|
||||
);
|
||||
|
||||
if (!wwwAuthenticateHeader?.value) {
|
||||
throw new Error('Unable to find www-authenticate response header for NTLM');
|
||||
const ntlmChallenge = extractNtlmChallenge(negotiateResponse.headers);
|
||||
if (ntlmChallenge == null) {
|
||||
throw new Error('Unable to find NTLM challenge in WWW-Authenticate response headers');
|
||||
}
|
||||
|
||||
const type2 = ntlm.parseType2Message(wwwAuthenticateHeader.value, (err: Error | null) => {
|
||||
const type2 = ntlm.parseType2Message(ntlmChallenge, (err: Error | null) => {
|
||||
if (err != null) throw err;
|
||||
});
|
||||
const type3 = ntlm.createType3Message(type2, options);
|
||||
|
||||
84
plugins/auth-ntlm/tests/index.test.ts
Normal file
84
plugins/auth-ntlm/tests/index.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const ntlmMock = vi.hoisted(() => ({
|
||||
createType1Message: vi.fn(),
|
||||
parseType2Message: vi.fn(),
|
||||
createType3Message: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('httpntlm', () => ({ ntlm: ntlmMock }));
|
||||
|
||||
import { plugin } from '../src';
|
||||
|
||||
describe('auth-ntlm', () => {
|
||||
beforeEach(() => {
|
||||
ntlmMock.createType1Message.mockReset();
|
||||
ntlmMock.parseType2Message.mockReset();
|
||||
ntlmMock.createType3Message.mockReset();
|
||||
ntlmMock.createType1Message.mockReturnValue('NTLM TYPE1');
|
||||
ntlmMock.parseType2Message.mockReturnValue({} as any);
|
||||
ntlmMock.createType3Message.mockReturnValue('NTLM TYPE3');
|
||||
});
|
||||
|
||||
test('uses NTLM challenge when Negotiate and NTLM headers are separate', async () => {
|
||||
const send = vi.fn().mockResolvedValue({
|
||||
headers: [
|
||||
{ name: 'WWW-Authenticate', value: 'Negotiate' },
|
||||
{ name: 'WWW-Authenticate', value: 'NTLM TlRMTVNTUAACAAAAAA==' },
|
||||
],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
|
||||
const result = await plugin.authentication?.onApply(ctx, {
|
||||
values: {},
|
||||
headers: [],
|
||||
url: 'https://example.local/resource',
|
||||
method: 'GET',
|
||||
contextId: 'ctx',
|
||||
});
|
||||
|
||||
expect(ntlmMock.parseType2Message).toHaveBeenCalledWith(
|
||||
'NTLM TlRMTVNTUAACAAAAAA==',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(result).toEqual({ setHeaders: [{ name: 'Authorization', value: 'NTLM TYPE3' }] });
|
||||
});
|
||||
|
||||
test('uses NTLM challenge when auth schemes are comma-separated in one header', async () => {
|
||||
const send = vi.fn().mockResolvedValue({
|
||||
headers: [{ name: 'www-authenticate', value: 'Negotiate, NTLM TlRMTVNTUAACAAAAAA==' }],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
|
||||
await plugin.authentication?.onApply(ctx, {
|
||||
values: {},
|
||||
headers: [],
|
||||
url: 'https://example.local/resource',
|
||||
method: 'GET',
|
||||
contextId: 'ctx',
|
||||
});
|
||||
|
||||
expect(ntlmMock.parseType2Message).toHaveBeenCalledWith(
|
||||
'NTLM TlRMTVNTUAACAAAAAA==',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws a clear error when NTLM challenge is missing', async () => {
|
||||
const send = vi.fn().mockResolvedValue({
|
||||
headers: [{ name: 'WWW-Authenticate', value: 'Negotiate' }],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
|
||||
await expect(
|
||||
plugin.authentication?.onApply(ctx, {
|
||||
values: {},
|
||||
headers: [],
|
||||
url: 'https://example.local/resource',
|
||||
method: 'GET',
|
||||
contextId: 'ctx',
|
||||
}),
|
||||
).rejects.toThrow('Unable to find NTLM challenge in WWW-Authenticate response headers');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user