mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Make sure all URI characters are correctly encoded
This commit is contained in:
parent
e8e96b903d
commit
e85ca622da
@ -1,7 +1,7 @@
|
|||||||
import type { TLSSocket } from 'tls';
|
import type { TLSSocket } from 'tls';
|
||||||
import { format } from 'url';
|
import { format } from 'url';
|
||||||
import type { HttpRequest } from '../../server/HttpRequest';
|
import type { HttpRequest } from '../../server/HttpRequest';
|
||||||
import { toCanonicalUrl } from '../../util/Util';
|
import { toCanonicalUriPath } from '../../util/Util';
|
||||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||||
import { TargetExtractor } from './TargetExtractor';
|
import { TargetExtractor } from './TargetExtractor';
|
||||||
|
|
||||||
@ -23,12 +23,12 @@ export class BasicTargetExtractor extends TargetExtractor {
|
|||||||
throw new Error('Missing host.');
|
throw new Error('Missing host.');
|
||||||
}
|
}
|
||||||
const isHttps = input.connection && (input.connection as TLSSocket).encrypted;
|
const isHttps = input.connection && (input.connection as TLSSocket).encrypted;
|
||||||
const url = format({
|
const path = format({
|
||||||
protocol: `http${isHttps ? 's' : ''}`,
|
protocol: `http${isHttps ? 's' : ''}`,
|
||||||
host: input.headers.host,
|
host: toCanonicalUriPath(input.headers.host),
|
||||||
pathname: input.url,
|
pathname: toCanonicalUriPath(input.url),
|
||||||
});
|
});
|
||||||
|
|
||||||
return { path: toCanonicalUrl(url) };
|
return { path };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { APPLICATION_OCTET_STREAM, TEXT_TURTLE } from '../util/ContentTypes';
|
|||||||
import { ConflictHttpError } from '../util/errors/ConflictHttpError';
|
import { ConflictHttpError } from '../util/errors/ConflictHttpError';
|
||||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||||
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
|
||||||
import { trimTrailingSlashes } from '../util/Util';
|
import { decodeUriPathComponents, encodeUriPathComponents, trimTrailingSlashes } from '../util/Util';
|
||||||
import type { FileIdentifierMapper, ResourceLink } from './FileIdentifierMapper';
|
import type { FileIdentifierMapper, ResourceLink } from './FileIdentifierMapper';
|
||||||
|
|
||||||
const { join: joinPath, normalize: normalizePath } = posix;
|
const { join: joinPath, normalize: normalizePath } = posix;
|
||||||
@ -137,7 +137,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
|||||||
let relative = filePath.slice(this.rootFilepath.length);
|
let relative = filePath.slice(this.rootFilepath.length);
|
||||||
if (isContainer) {
|
if (isContainer) {
|
||||||
return {
|
return {
|
||||||
identifier: { path: encodeURI(this.baseRequestURI + relative) },
|
identifier: { path: this.baseRequestURI + encodeUriPathComponents(relative) },
|
||||||
filePath,
|
filePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -150,7 +150,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identifier: { path: encodeURI(this.baseRequestURI + relative) },
|
identifier: { path: this.baseRequestURI + encodeUriPathComponents(relative) },
|
||||||
filePath,
|
filePath,
|
||||||
contentType,
|
contentType,
|
||||||
};
|
};
|
||||||
@ -201,7 +201,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
|
|||||||
if (!identifier.path.startsWith(this.baseRequestURI)) {
|
if (!identifier.path.startsWith(this.baseRequestURI)) {
|
||||||
throw new NotFoundHttpError();
|
throw new NotFoundHttpError();
|
||||||
}
|
}
|
||||||
return decodeURI(identifier.path).slice(this.baseRequestURI.length);
|
return decodeUriPathComponents(identifier.path.slice(this.baseRequestURI.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,14 +68,19 @@ export const pipeStreamsAndErrors = <T extends Writable>(readable: Readable, des
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a URL string to the "canonical" version that should be used internally for consistency.
|
* Converts a URI path to the canonical version by splitting on slashes,
|
||||||
* Decodes all percent encodings and then makes sure only the necessary characters are encoded again.
|
* decoding any percent-based encodings,
|
||||||
|
* and then encoding any special characters.
|
||||||
*/
|
*/
|
||||||
export const toCanonicalUrl = (url: string): string => {
|
export const toCanonicalUriPath = (path: string): string => path.split('/').map((part): string =>
|
||||||
const match = /(\w+:\/\/[^/]+\/)(.*)/u.exec(url);
|
encodeURIComponent(decodeURIComponent(part))).join('/');
|
||||||
if (!match) {
|
|
||||||
throw new UnsupportedHttpError(`Invalid URL ${url}`);
|
/**
|
||||||
}
|
* Decodes all components of a URI path.
|
||||||
const [ , domain, path ] = match;
|
*/
|
||||||
return encodeURI(domain + path.split('/').map(decodeURIComponent).join('/'));
|
export const decodeUriPathComponents = (path: string): string => path.split('/').map(decodeURIComponent).join('/');
|
||||||
};
|
|
||||||
|
/**
|
||||||
|
* Encodes all (non-slash) special characters in a URI path.
|
||||||
|
*/
|
||||||
|
export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
|
||||||
|
@ -26,8 +26,14 @@ describe('A BasicTargetExtractor', (): void => {
|
|||||||
)).resolves.toEqual({ path: 'https://test.com/url' });
|
)).resolves.toEqual({ path: 'https://test.com/url' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decodes relevant percent encodings.', async(): Promise<void> => {
|
it('encodes relevant characters.', async(): Promise<void> => {
|
||||||
await expect(extractor.handle({ url: '/a%20path%26/name', headers: { host: 'test.com' }} as any))
|
await expect(extractor.handle({ url: '/a%20path%26/name', headers: { host: 'test.com' }} as any))
|
||||||
.resolves.toEqual({ path: 'http://test.com/a%20path&/name' });
|
.resolves.toEqual({ path: 'http://test.com/a%20path%26/name' });
|
||||||
|
|
||||||
|
await expect(extractor.handle({ url: '/a path%26/name', headers: { host: 'test.com' }} as any))
|
||||||
|
.resolves.toEqual({ path: 'http://test.com/a%20path%26/name' });
|
||||||
|
|
||||||
|
await expect(extractor.handle({ url: '/path&%26/name', headers: { host: 'test.com' }} as any))
|
||||||
|
.resolves.toEqual({ path: 'http://test.com/path%26%26/name' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import streamifyArray from 'streamify-array';
|
import streamifyArray from 'streamify-array';
|
||||||
import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError';
|
import {
|
||||||
import { ensureTrailingSlash, matchingMediaType, readableToString, toCanonicalUrl } from '../../../src/util/Util';
|
decodeUriPathComponents,
|
||||||
|
encodeUriPathComponents,
|
||||||
|
ensureTrailingSlash,
|
||||||
|
matchingMediaType,
|
||||||
|
readableToString,
|
||||||
|
toCanonicalUriPath,
|
||||||
|
} from '../../../src/util/Util';
|
||||||
|
|
||||||
describe('Util function', (): void => {
|
describe('Util function', (): void => {
|
||||||
describe('ensureTrailingSlash', (): void => {
|
describe('ensureTrailingSlash', (): void => {
|
||||||
@ -33,14 +39,17 @@ describe('Util function', (): void => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toCanonicalUrl', (): void => {
|
describe('UriPath functions', (): void => {
|
||||||
it('makes sure only the necessary parts are encoded.', async(): Promise<void> => {
|
it('makes sure only the necessary parts are encoded with toCanonicalUriPath.', async(): Promise<void> => {
|
||||||
expect(toCanonicalUrl('http://test.com/a%20path%26/name'))
|
expect(toCanonicalUriPath('/a%20path&/name')).toEqual('/a%20path%26/name');
|
||||||
.toEqual('http://test.com/a%20path&/name');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors on invalid URLs.', async(): Promise<void> => {
|
it('decodes all parts of a path with decodeUriPathComponents.', async(): Promise<void> => {
|
||||||
expect((): any => toCanonicalUrl('notAnUrl')).toThrow(new UnsupportedHttpError('Invalid URL notAnUrl'));
|
expect(decodeUriPathComponents('/a%20path&/name')).toEqual('/a path&/name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodes all parts of a path with encodeUriPathComponents.', async(): Promise<void> => {
|
||||||
|
expect(encodeUriPathComponents('/a%20path&/name')).toEqual('/a%2520path%26/name');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user