diff --git a/README.md b/README.md index 18dc3b5d..9de8c046 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/source/auth/digest.ts b/source/auth/digest.ts index c29c14a4..bd43e99f 100644 --- a/source/auth/digest.ts +++ b/source/auth/digest.ts @@ -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; @@ -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"; +} diff --git a/source/auth/index.ts b/source/auth/index.ts index 8ac56997..03f2d01c 100644 --- a/source/auth/index.ts +++ b/source/auth/index.ts @@ -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; diff --git a/source/operations/copyFile.ts b/source/operations/copyFile.ts index 283b228a..22f2664d 100644 --- a/source/operations/copyFile.ts +++ b/source/operations/copyFile.ts @@ -34,6 +34,6 @@ export async function copyFile( context, options ); - const response = await request(requestOptions); + const response = await request(requestOptions, context); handleResponseCode(context, response); } diff --git a/source/operations/createDirectory.ts b/source/operations/createDirectory.ts index c8429432..ea1d5d39 100644 --- a/source/operations/createDirectory.ts +++ b/source/operations/createDirectory.ts @@ -24,7 +24,7 @@ export async function createDirectory( context, options ); - const response = await request(requestOptions); + const response = await request(requestOptions, context); handleResponseCode(context, response); } diff --git a/source/operations/createStream.ts b/source/operations/createStream.ts index c29f56d8..53ae8ded 100644 --- a/source/operations/createStream.ts +++ b/source/operations/createStream.ts @@ -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 @@ -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( diff --git a/source/operations/customRequest.ts b/source/operations/customRequest.ts index 8f9916b8..f181f585 100644 --- a/source/operations/customRequest.ts +++ b/source/operations/customRequest.ts @@ -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; } diff --git a/source/operations/deleteFile.ts b/source/operations/deleteFile.ts index 4436d83f..e2ead9cf 100644 --- a/source/operations/deleteFile.ts +++ b/source/operations/deleteFile.ts @@ -17,6 +17,6 @@ export async function deleteFile( context, options ); - const response = await request(requestOptions); + const response = await request(requestOptions, context); handleResponseCode(context, response); } diff --git a/source/operations/directoryContents.ts b/source/operations/directoryContents.ts index 7181b523..0b2450a4 100644 --- a/source/operations/directoryContents.ts +++ b/source/operations/directoryContents.ts @@ -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) { diff --git a/source/operations/getDAVCompliance.ts b/source/operations/getDAVCompliance.ts index a3b45438..b9df91c7 100644 --- a/source/operations/getDAVCompliance.ts +++ b/source/operations/getDAVCompliance.ts @@ -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) { diff --git a/source/operations/getFileContents.ts b/source/operations/getFileContents.ts index c0301337..e0e1f87a 100644 --- a/source/operations/getFileContents.ts +++ b/source/operations/getFileContents.ts @@ -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()) { @@ -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); diff --git a/source/operations/getQuota.ts b/source/operations/getQuota.ts index 7275b05f..bdd94f31 100644 --- a/source/operations/getQuota.ts +++ b/source/operations/getQuota.ts @@ -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); diff --git a/source/operations/lock.ts b/source/operations/lock.ts index 16e6b282..92837660 100644 --- a/source/operations/lock.ts +++ b/source/operations/lock.ts @@ -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); @@ -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); diff --git a/source/operations/moveFile.ts b/source/operations/moveFile.ts index aac96547..ae459303 100644 --- a/source/operations/moveFile.ts +++ b/source/operations/moveFile.ts @@ -28,6 +28,6 @@ export async function moveFile( context, options ); - const response = await request(requestOptions); + const response = await request(requestOptions, context); handleResponseCode(context, response); } diff --git a/source/operations/partialUpdateFileContents.ts b/source/operations/partialUpdateFileContents.ts index 5ab283e3..2df29c22 100644 --- a/source/operations/partialUpdateFileContents.ts +++ b/source/operations/partialUpdateFileContents.ts @@ -84,7 +84,7 @@ async function partialUpdateFileContentsSabredav( context, options ); - const response = await request(requestOptions); + const response = await request(requestOptions, context); handleResponseCode(context, response); } @@ -121,6 +121,6 @@ async function partialUpdateFileContentsApache( context, options ); - const response = await request(requestOptions); + const response = await request(requestOptions, context); handleResponseCode(context, response); } diff --git a/source/operations/putFileContents.ts b/source/operations/putFileContents.ts index b9efbdd8..33b705c3 100644 --- a/source/operations/putFileContents.ts +++ b/source/operations/putFileContents.ts @@ -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) { diff --git a/source/operations/search.ts b/source/operations/search.ts index 061484d4..edeac254 100644 --- a/source/operations/search.ts +++ b/source/operations/search.ts @@ -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); diff --git a/source/operations/stat.ts b/source/operations/stat.ts index 1f9bea88..ce3a0596 100644 --- a/source/operations/stat.ts +++ b/source/operations/stat.ts @@ -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); diff --git a/source/request.ts b/source/request.ts index bd972fa2..673c56b8 100644 --- a/source/request.ts +++ b/source/request.ts @@ -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"; @@ -15,23 +19,10 @@ import { RequestOptions, Response, WebDAVClientContext, - WebDAVMethodOptions + WebDAVMethodOptions, + AuthType } from "./types.js"; - -function _request(requestOptions: RequestOptions): Promise { - 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 = {}; @@ -101,11 +92,38 @@ export function prepareRequestOptions( return finalOptions; } -export async function request(requestOptions: RequestOptionsWithState): Promise { - // Client not configured for digest authentication - if (!requestOptions._digest) { - return _request(requestOptions); +export async function request( + requestOptions: RequestOptionsWithState, + context: WebDAVClientContext +): Promise { + 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 { + 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 { // Remove client's digest authentication object from request options const _digest = requestOptions._digest; delete requestOptions._digest; @@ -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) { @@ -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 { @@ -140,3 +158,18 @@ export async function request(requestOptions: RequestOptionsWithState): Promise< } return response; } + +function requestStandard(requestOptions: RequestOptions): Promise { + const patcher = getPatcher(); + return patcher.patchInline( + "request", + (options: RequestOptions) => + patcher.patchInline( + "fetch", + fetch, + options.url, + getFetchOptions(options) as RequestInit + ), + requestOptions + ); +} diff --git a/source/types.ts b/source/types.ts index bea4621b..f6f328f6 100644 --- a/source/types.ts +++ b/source/types.ts @@ -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", diff --git a/test/node/auth.spec.ts b/test/node/auth.spec.ts index 60d65f3f..fa10490c 100644 --- a/test/node/auth.spec.ts +++ b/test/node/auth.spec.ts @@ -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"); @@ -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; + }); + }); }); });