Skip to content

Commit

Permalink
Merge pull request #370 from perry-mitchell/feat/auto_auth_mode
Browse files Browse the repository at this point in the history
Automatic auth mode
  • Loading branch information
perry-mitchell authored Mar 18, 2024
2 parents 4f0e3a0 + 3c1155e commit e58fb70
Show file tree
Hide file tree
Showing 21 changed files with 118 additions and 45 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ The WebDAV client automatically detects which authentication to use, between `Au

Setting the `authType` will automatically manage the `Authorization` header when connecting.

You can set the `authType` to `AuthType.Auto` if you're unsure whether the remote server requires **digest** or **password** based authentication.

#### Basic/no authentication

You can use the client without authentication if the server doesn't require it - simply avoid passing any values to `username`, `password` in the config.
Expand Down
10 changes: 8 additions & 2 deletions source/auth/digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,13 @@ function makeNonce(): string {
}

export function parseDigestAuth(response: Response, _digest: DigestContext): boolean {
const authHeader = (response.headers && response.headers.get("www-authenticate")) || "";
if (authHeader.split(/\s/)[0].toLowerCase() !== "digest") {
const isDigest = responseIndicatesDigestAuth(response);
if (!isDigest) {
return false;
}
const re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi;
for (;;) {
const authHeader = (response.headers && response.headers.get("www-authenticate")) || "";
const match = re.exec(authHeader);
if (!match) {
break;
Expand All @@ -86,3 +87,8 @@ export function parseDigestAuth(response: Response, _digest: DigestContext): boo
_digest.cnonce = makeNonce();
return true;
}

export function responseIndicatesDigestAuth(response: Response): boolean {
const authHeader = (response.headers && response.headers.get("www-authenticate")) || "";
return authHeader.split(/\s/)[0].toLowerCase() === "digest";
}
5 changes: 5 additions & 0 deletions source/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export function setupAuth(
ha1: string
): void {
switch (context.authType) {
case AuthType.Auto:
if (username && password) {
context.headers.Authorization = generateBasicAuthHeader(username, password);
}
break;
case AuthType.Digest:
context.digest = createDigestContext(username, password, ha1);
break;
Expand Down
2 changes: 1 addition & 1 deletion source/operations/copyFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export async function copyFile(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
}
2 changes: 1 addition & 1 deletion source/operations/createDirectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function createDirectory(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
}

Expand Down
4 changes: 2 additions & 2 deletions source/operations/createStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function createWriteStream(
context,
options
);
request(requestOptions)
request(requestOptions, context)
.then(response => handleResponseCode(context, response))
.then(response => {
// Fire callback asynchronously to avoid errors
Expand Down Expand Up @@ -90,7 +90,7 @@ async function getFileStream(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
if (headers.Range && response.status !== 206) {
const responseError: WebDAVClientError = new Error(
Expand Down
2 changes: 1 addition & 1 deletion source/operations/customRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function customRequest(
requestOptions.url = joinURL(context.remoteURL, encodePath(remotePath));
}
const finalOptions = prepareRequestOptions(requestOptions, context, {});
const response = await request(finalOptions);
const response = await request(finalOptions, context);
handleResponseCode(context, response);
return response;
}
2 changes: 1 addition & 1 deletion source/operations/deleteFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export async function deleteFile(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
}
2 changes: 1 addition & 1 deletion source/operations/directoryContents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function getDirectoryContents(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
const responseData = await response.text();
if (!responseData) {
Expand Down
2 changes: 1 addition & 1 deletion source/operations/getDAVCompliance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function getDAVCompliance(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
try {
handleResponseCode(context, response);
} catch (err) {
Expand Down
4 changes: 2 additions & 2 deletions source/operations/getFileContents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function getFileContentsBuffer(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
let body: BufferLike;
if (isWeb() || isReactNative()) {
Expand Down Expand Up @@ -78,7 +78,7 @@ async function getFileContentsString(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
const body = await response.text();
return processResponsePayload(response, body, options.details);
Expand Down
2 changes: 1 addition & 1 deletion source/operations/getQuota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function getQuota(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
const responseData = await response.text();
const result = await parseXML(responseData);
Expand Down
4 changes: 2 additions & 2 deletions source/operations/lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function lock(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
const responseData = await response.text();
const lockPayload = parseGenericResponse(responseData);
Expand Down Expand Up @@ -70,7 +70,7 @@ export async function unlock(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
if (response.status !== 204 && response.status !== 200) {
const err = createErrorFromResponse(response);
Expand Down
2 changes: 1 addition & 1 deletion source/operations/moveFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ export async function moveFile(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
}
4 changes: 2 additions & 2 deletions source/operations/partialUpdateFileContents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async function partialUpdateFileContentsSabredav(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
}

Expand Down Expand Up @@ -121,6 +121,6 @@ async function partialUpdateFileContentsApache(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
}
2 changes: 1 addition & 1 deletion source/operations/putFileContents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function putFileContents(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
try {
handleResponseCode(context, response);
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion source/operations/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function getSearch(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
const responseText = await response.text();
const responseData = await parseXML(responseText);
Expand Down
2 changes: 1 addition & 1 deletion source/operations/stat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function getStat(
context,
options
);
const response = await request(requestOptions);
const response = await request(requestOptions, context);
handleResponseCode(context, response);
const responseData = await response.text();
const result = await parseXML(responseData);
Expand Down
79 changes: 56 additions & 23 deletions source/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { fetch } from "@buttercup/fetch";
import type { RequestInit as RequestInitNF } from "node-fetch";
import { getPatcher } from "./compat/patcher.js";
import { isReactNative, isWeb } from "./compat/env.js";
import { generateDigestAuthHeader, parseDigestAuth } from "./auth/digest.js";
import {
generateDigestAuthHeader,
parseDigestAuth,
responseIndicatesDigestAuth
} from "./auth/digest.js";
import { cloneShallow, merge } from "./tools/merge.js";
import { mergeHeaders } from "./tools/headers.js";
import { requestDataToFetchBody } from "./tools/body.js";
Expand All @@ -15,23 +19,10 @@ import {
RequestOptions,
Response,
WebDAVClientContext,
WebDAVMethodOptions
WebDAVMethodOptions,
AuthType
} from "./types.js";

function _request(requestOptions: RequestOptions): Promise<Response> {
const patcher = getPatcher();
return patcher.patchInline(
"request",
(options: RequestOptions) =>
patcher.patchInline(
"fetch",
fetch,
options.url,
getFetchOptions(options) as RequestInit
),
requestOptions
);
}
import { setupAuth } from "./auth/index.js";

function getFetchOptions(requestOptions: RequestOptions): RequestInit | RequestInitNF {
let headers: Headers = {};
Expand Down Expand Up @@ -101,11 +92,38 @@ export function prepareRequestOptions(
return finalOptions;
}

export async function request(requestOptions: RequestOptionsWithState): Promise<Response> {
// Client not configured for digest authentication
if (!requestOptions._digest) {
return _request(requestOptions);
export async function request(
requestOptions: RequestOptionsWithState,
context: WebDAVClientContext
): Promise<Response> {
if (context.authType === AuthType.Auto) {
return requestAuto(requestOptions, context);
}
if (requestOptions._digest) {
return requestDigest(requestOptions);
}
return requestStandard(requestOptions);
}

async function requestAuto(
requestOptions: RequestOptionsWithState,
context: WebDAVClientContext
): Promise<Response> {
const response = await requestStandard(requestOptions);
if (response.ok) {
context.authType = AuthType.Password;
return response;
}
if (response.status == 401 && responseIndicatesDigestAuth(response)) {
context.authType = AuthType.Digest;
setupAuth(context, context.username, context.password, undefined, undefined);
requestOptions._digest = context.digest;
return requestDigest(requestOptions);
}
return response;
}

async function requestDigest(requestOptions: RequestOptionsWithState): Promise<Response> {
// Remove client's digest authentication object from request options
const _digest = requestOptions._digest;
delete requestOptions._digest;
Expand All @@ -118,7 +136,7 @@ export async function request(requestOptions: RequestOptionsWithState): Promise<
});
}
// Perform digest request + check
const response = await _request(requestOptions);
const response = await requestStandard(requestOptions);
if (response.status == 401) {
_digest.hasDigestAuth = parseDigestAuth(response, _digest);
if (_digest.hasDigestAuth) {
Expand All @@ -127,7 +145,7 @@ export async function request(requestOptions: RequestOptionsWithState): Promise<
Authorization: generateDigestAuthHeader(requestOptions, _digest)
}
});
const response2 = await _request(requestOptions);
const response2 = await requestStandard(requestOptions);
if (response2.status == 401) {
_digest.hasDigestAuth = false;
} else {
Expand All @@ -140,3 +158,18 @@ export async function request(requestOptions: RequestOptionsWithState): Promise<
}
return response;
}

function requestStandard(requestOptions: RequestOptions): Promise<Response> {
const patcher = getPatcher();
return patcher.patchInline(
"request",
(options: RequestOptions) =>
patcher.patchInline(
"fetch",
fetch,
options.url,
getFetchOptions(options) as RequestInit
),
requestOptions
);
}
1 change: 1 addition & 0 deletions source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { Request, Response } from "@buttercup/fetch";
export type AuthHeader = string;

export enum AuthType {
Auto = "auto",
Digest = "digest",
None = "none",
Password = "password",
Expand Down
28 changes: 27 additions & 1 deletion test/node/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,27 @@ describe("auth", function () {
return webdav.getFileContents("/file");
});

it("should support auto-detection of password/digest auth", function () {
nock(DUMMYSERVER)
.get("/file")
.reply(200, function () {
expect(this.req.headers.authorization).to.equal("Basic dXNlcjpwYXNz");
return "";
});
const webdav = createWebDAVClient(DUMMYSERVER, {
authType: AuthType.Auto,
username: "user",
password: "pass"
});
return webdav.getFileContents("/file");
});

describe("using Digest-enabled server", function () {
beforeEach(function () {
this.client = createWebDAVClient(`http://localhost:${SERVER_PORT}/webdav/server`, {
username: SERVER_USERNAME,
password: SERVER_PASSWORD,
authType: AuthType.Digest
authType: AuthType.Auto
});
clean();
this.server = createWebDAVServer("digest");
Expand All @@ -80,5 +95,16 @@ describe("auth", function () {
expect(exists).to.be.true;
});
});

it("should support auto-detection of password/digest auth", function () {
this.client = createWebDAVClient(`http://localhost:${SERVER_PORT}/webdav/server`, {
username: SERVER_USERNAME,
password: SERVER_PASSWORD,
authType: AuthType.Auto
});
return this.client.exists("/alrighty.jpg").then(function (exists) {
expect(exists).to.be.true;
});
});
});
});

0 comments on commit e58fb70

Please sign in to comment.