feat: Expose Last-Modified and ETag headers

This commit is contained in:
Joachim Van Herwegen 2021-08-13 17:07:56 +02:00
parent 97c534b2bf
commit 77d695c8b6
8 changed files with 92 additions and 3 deletions

View File

@ -17,6 +17,8 @@
"options_credentials": true,
"options_exposedHeaders": [
"Accept-Patch",
"ETag",
"Last-Modified",
"Link",
"Location",
"MS-Author-Via",

View File

@ -4,6 +4,7 @@
"files-scs:config/ldp/metadata-writer/writers/constant.json",
"files-scs:config/ldp/metadata-writer/writers/link-rel.json",
"files-scs:config/ldp/metadata-writer/writers/mapped.json",
"files-scs:config/ldp/metadata-writer/writers/modified.json",
"files-scs:config/ldp/metadata-writer/writers/wac-allow.json",
"files-scs:config/ldp/metadata-writer/writers/www-auth.json"
],
@ -15,6 +16,7 @@
"handlers": [
{ "@id": "urn:solid-server:default:MetadataWriter_Constant" },
{ "@id": "urn:solid-server:default:MetadataWriter_Mapped" },
{ "@id": "urn:solid-server:default:MetadataWriter_Modified" },
{ "@id": "urn:solid-server:default:MetadataWriter_LinkRel" },
{ "@id": "urn:solid-server:default:MetadataWriter_WacAllow" },
{ "@id": "urn:solid-server:default:MetadataWriter_WwwAuth" }

View File

@ -0,0 +1,10 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Adds the Last-Modified and ETag headers.",
"@id": "urn:solid-server:default:MetadataWriter_Modified",
"@type": "ModifiedMetadataWriter"
}
]
}

View File

@ -91,6 +91,7 @@ export * from './ldp/http/metadata/LinkTypeParser';
export * from './ldp/http/metadata/MappedMetadataWriter';
export * from './ldp/http/metadata/MetadataParser';
export * from './ldp/http/metadata/MetadataWriter';
export * from './ldp/http/metadata/ModifiedMetadataWriter';
export * from './ldp/http/metadata/SlugParser';
export * from './ldp/http/metadata/WacAllowMetadataWriter';
export * from './ldp/http/metadata/WwwAuthMetadataWriter';

View File

@ -0,0 +1,23 @@
import type { HttpResponse } from '../../../server/HttpResponse';
import { getETag } from '../../../storage/Conditions';
import { addHeader } from '../../../util/HeaderUtil';
import { DC } from '../../../util/Vocabularies';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter';
/**
* A {@link MetadataWriter} that generates all the necessary headers related to the modification date of a resource.
*/
export class ModifiedMetadataWriter extends MetadataWriter {
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
const modified = input.metadata.get(DC.terms.modified);
if (modified) {
const date = new Date(modified.value);
addHeader(input.response, 'Last-Modified', date.toUTCString());
}
const etag = getETag(input.metadata);
if (etag) {
addHeader(input.response, 'ETag', etag);
}
}
}

View File

@ -1,4 +1,5 @@
import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { DC } from '../util/Vocabularies';
/**
* The conditions of an HTTP conditional request.
@ -7,11 +8,11 @@ export interface Conditions {
/**
* Valid if matching any of the given ETags.
*/
matchesEtag: string[];
matchesETag?: string[];
/**
* Valid if not matching any of the given ETags.
*/
notMatchesEtag: string[];
notMatchesETag?: string[];
/**
* Valid if modified since the given date.
*/
@ -27,9 +28,23 @@ export interface Conditions {
*/
matchesMetadata: (metadata: RepresentationMetadata) => boolean;
/**
* Checks validity based on the given ETag and/org date.
* Checks validity based on the given ETag and/or date.
* @param eTag - Condition based on ETag.
* @param lastModified - Condition based on last modified date.
*/
matches: (eTag?: string, lastModified?: Date) => boolean;
}
/**
* Generates an ETag based on the last modified date of a resource.
* @param metadata - Metadata of the resource.
*
* @returns the generated ETag. Undefined if no last modified date was found.
*/
export function getETag(metadata: RepresentationMetadata): string | undefined {
const modified = metadata.get(DC.terms.modified);
if (modified) {
const date = new Date(modified.value);
return `"${date.getTime()}"`;
}
}

View File

@ -99,6 +99,13 @@ describe('An http server with middleware', (): void => {
expect(exposed.split(/\s*,\s*/u)).toContain('Accept-Patch');
});
it('exposes the Last-Modified and ETag headers via CORS.', async(): Promise<void> => {
const res = await request(server).get('/').expect(200);
const exposed = res.header['access-control-expose-headers'];
expect(exposed.split(/\s*,\s*/u)).toContain('ETag');
expect(exposed.split(/\s*,\s*/u)).toContain('Last-Modified');
});
it('exposes the Link header via CORS.', async(): Promise<void> => {
const res = await request(server).get('/').expect(200);
const exposed = res.header['access-control-expose-headers'];

View File

@ -0,0 +1,29 @@
import { createResponse } from 'node-mocks-http';
import { ModifiedMetadataWriter } from '../../../../../src/ldp/http/metadata/ModifiedMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { updateModifiedDate } from '../../../../../src/util/ResourceUtil';
import { DC } from '../../../../../src/util/Vocabularies';
describe('A ModifiedMetadataWriter', (): void => {
const writer = new ModifiedMetadataWriter();
it('adds the Last-Modified and ETag header if there is dc:modified metadata.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata();
updateModifiedDate(metadata);
const dateTime = metadata.get(DC.terms.modified)!.value;
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'last-modified': new Date(dateTime).toUTCString(),
etag: `"${new Date(dateTime).getTime()}"`,
});
});
it('does nothing if there is no matching metadata.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata();
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({});
});
});