Skip to content

Commit

Permalink
change: Do not serve index on */*
Browse files Browse the repository at this point in the history
Closes #844
  • Loading branch information
RubenVerborgh committed Jul 23, 2021
1 parent defdb32 commit c739965
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 24 deletions.
16 changes: 11 additions & 5 deletions src/storage/IndexRepresentationStore.ts
Expand Up @@ -65,12 +65,18 @@ export class IndexRepresentationStore extends PassthroughStore {
}

/**
* Makes sure the stored media range matches the highest weight preference.
* Makes sure the stored media range explicitly matches the highest weight preference.
*/
private matchesPreferences(preferences: RepresentationPreferences): boolean {
const cleaned = cleanPreferences(preferences.type);
const max = Math.max(...Object.values(cleaned));
return Object.entries(cleaned).some(([ range, weight ]): boolean =>
matchesMediaType(range, this.mediaRange) && weight === max);
// Always match */*
if (this.mediaRange === '*/*') {
return true;
}

// Otherwise, determine if an explicit match has the highest weight
const types = cleanPreferences(preferences.type);
const max = Math.max(...Object.values(types));
return Object.entries(types).some(([ range, weight ]): boolean =>
range !== '*/*' && (max - weight) < 0.01 && matchesMediaType(range, this.mediaRange));
}
}
56 changes: 47 additions & 9 deletions test/integration/LdpHandlerWithoutAuth.test.ts
Expand Up @@ -58,12 +58,24 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
await app.stop();
});

it('can read the root container index page.', async(): Promise<void> => {
const response = await getResource(baseUrl, { contentType: 'text/html' });
it('returns the root container listing.', async(): Promise<void> => {
const response = await getResource(baseUrl, {}, { contentType: 'text/turtle' });

await expect(response.text()).resolves.toContain('Welcome to the Community Solid Server');
await expect(response.text()).resolves.toContain('ldp:BasicContainer');
expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`);
});

it('returns the root container listing when asking for */*.', async(): Promise<void> => {
const response = await getResource(baseUrl, { accept: '*/*' }, { contentType: 'text/turtle' });

await expect(response.text()).resolves.toContain('ldp:BasicContainer');
expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`);
});

// This is only here because we're accessing the root container
it('can read the root container index page when asking for HTML.', async(): Promise<void> => {
const response = await getResource(baseUrl, { accept: 'text/html' }, { contentType: 'text/html' });

await expect(response.text()).resolves.toContain('Welcome to the Community Solid Server');
expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`);
});

Expand All @@ -89,7 +101,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE0' });

// GET
const response = await getResource(documentUrl, { contentType: 'text/plain' });
const response = await getResource(documentUrl, {}, { contentType: 'text/plain' });
await expect(response.text()).resolves.toBe('TESTFILE0');

// DELETE
Expand All @@ -102,14 +114,14 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE0' });

// GET
let response = await getResource(documentUrl, { contentType: 'text/plain' });
let response = await getResource(documentUrl, {}, { contentType: 'text/plain' });
await expect(response.text()).resolves.toBe('TESTFILE0');

// PUT
await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE1' });

// GET
response = await getResource(documentUrl, { contentType: 'text/plain' });
response = await getResource(documentUrl, {}, { contentType: 'text/plain' });
await expect(response.text()).resolves.toBe('TESTFILE1');

// DELETE
Expand All @@ -133,6 +145,32 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
expect(await deleteResource(containerUrl)).toBeUndefined();
});

it('can create a container and retrieve it.', async(): Promise<void> => {
// Create container
const containerUrl = `${baseUrl}testcontainer0/`;
await putResource(containerUrl, { contentType: 'text/turtle' });

// GET representation
const response = await getResource(containerUrl, { accept: '*/*' }, { contentType: 'text/turtle' });
await expect(response.text()).resolves.toContain('ldp:BasicContainer');

// DELETE
expect(await deleteResource(containerUrl)).toBeUndefined();
});

it('can create a container and view it as HTML.', async(): Promise<void> => {
// Create container
const containerUrl = `${baseUrl}testcontainer0/`;
await putResource(containerUrl, { contentType: 'text/turtle' });

// GET representation
const response = await getResource(containerUrl, { accept: 'text/html' }, { contentType: 'text/html' });
await expect(response.text()).resolves.toContain('Contents of testcontainer0');

// DELETE
expect(await deleteResource(containerUrl)).toBeUndefined();
});

it('can create a container and put a document in it.', async(): Promise<void> => {
// Create container
const containerUrl = `${baseUrl}testcontainer0/`;
Expand All @@ -143,7 +181,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
await putResource(documentUrl, { contentType: 'text/plain', body: 'TESTFILE0' });

// GET document
const response = await getResource(documentUrl, { contentType: 'text/plain' });
const response = await getResource(documentUrl, {}, { contentType: 'text/plain' });
await expect(response.text()).resolves.toBe('TESTFILE0');

// DELETE
Expand Down Expand Up @@ -227,7 +265,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
await expect(response.text()).resolves.toHaveLength(0);

// GET
await getResource(documentUrl, { contentType: 'image/png' });
await getResource(documentUrl, {}, { contentType: 'image/png' });

// DELETE
expect(await deleteResource(documentUrl)).toBeUndefined();
Expand Down
66 changes: 58 additions & 8 deletions test/unit/storage/IndexRepresentationstore.test.ts
Expand Up @@ -33,30 +33,67 @@ describe('An IndexRepresentationStore', (): void => {
.toThrow('Invalid index name');
});

it('retrieves the index resource if it exists.', async(): Promise<void> => {
const result = await store.getRepresentation({ path: baseUrl }, {});
it('retrieves the index resource if it is explicitly preferred.', async(): Promise<void> => {
const preferences = { type: { 'text/turtle': 0.5, 'text/html': 0.8 }};
const result = await store.getRepresentation({ path: baseUrl }, preferences);
await expect(readableToString(result.data)).resolves.toBe('index data');
expect(source.getRepresentation).toHaveBeenCalledTimes(2);
expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}index.html` }, preferences, undefined);
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, {}, undefined);

// Use correct metadata
expect(result.metadata.identifier.value).toBe(baseUrl);
expect(result.metadata.contentType).toBe('text/html');
});

it('retrieves the index resource if there is a range preference.', async(): Promise<void> => {
const preferences = { type: { 'text/*': 0.8, 'other/other': 0.7 }};
const result = await store.getRepresentation({ path: baseUrl }, preferences);
await expect(readableToString(result.data)).resolves.toBe('index data');
expect(source.getRepresentation).toHaveBeenCalledTimes(2);
expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}index.html` }, {}, undefined);
expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}index.html` }, preferences, undefined);
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, {}, undefined);

// Use correct metadata
expect(result.metadata.identifier.value).toBe(baseUrl);
expect(result.metadata.contentType).toBe('text/html');
});

it('does not retrieve the index resource if there are no type preferences.', async(): Promise<void> => {
const preferences = {};
const result = await store.getRepresentation({ path: baseUrl }, preferences);
await expect(readableToString(result.data)).resolves.toBe('container data');
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, preferences, undefined);

// Use correct metadata
expect(result.metadata.identifier.value).toBe(baseUrl);
expect(result.metadata.contentType).toBe('text/turtle');
});

it('does not retrieve the index resource on */*.', async(): Promise<void> => {
const preferences = { type: { '*/*': 1 }};
const result = await store.getRepresentation({ path: baseUrl }, preferences);
await expect(readableToString(result.data)).resolves.toBe('container data');
});

it('errors if a non-404 error was thrown when accessing the index resource.', async(): Promise<void> => {
const preferences = { type: { 'text/turtle': 0.5, 'text/html': 0.8 }};
source.getRepresentation.mockRejectedValueOnce(new ConflictHttpError('conflict!'));
await expect(store.getRepresentation({ path: baseUrl }, {})).rejects.toThrow('conflict!');
await expect(store.getRepresentation({ path: baseUrl }, preferences)).rejects.toThrow('conflict!');
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
});

it('requests the usual data if there is no index resource.', async(): Promise<void> => {
const result = await store.getRepresentation(emptyContainer, {});
const preferences = { type: { 'text/turtle': 0.5, 'text/html': 0.8 }};
source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
const result = await store.getRepresentation(emptyContainer, preferences);
await expect(readableToString(result.data)).resolves.toBe('container data');
expect(source.getRepresentation).toHaveBeenCalledTimes(2);
expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${emptyContainer.path}index.html` }, {}, undefined);
expect(source.getRepresentation).toHaveBeenLastCalledWith(emptyContainer, {}, undefined);
expect(source.getRepresentation).toHaveBeenCalledWith(
{ path: `${emptyContainer.path}index.html` }, preferences, undefined,
);
expect(source.getRepresentation).toHaveBeenLastCalledWith(emptyContainer, preferences, undefined);
});

it('requests the usual data if the index media range is not the most preferred.', async(): Promise<void> => {
Expand All @@ -67,7 +104,7 @@ describe('An IndexRepresentationStore', (): void => {
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, preferences, undefined);
});

it('always returns the index resource if the media range is set to */*.', async(): Promise<void> => {
it('returns the index resource if the media range is set to */*.', async(): Promise<void> => {
store = new IndexRepresentationStore(source, 'base.html', '*/*');
// Mocking because we also change the index name
source.getRepresentation.mockResolvedValueOnce(new BasicRepresentation('index data', 'text/html'));
Expand All @@ -83,4 +120,17 @@ describe('An IndexRepresentationStore', (): void => {
expect(result.metadata.identifier.value).toBe(baseUrl);
expect(result.metadata.contentType).toBe('text/html');
});

it('returns the index resource if media range and Accept header are */*.', async(): Promise<void> => {
store = new IndexRepresentationStore(source, 'base.html', '*/*');
// Mocking because we also change the index name
source.getRepresentation.mockResolvedValueOnce(new BasicRepresentation('index data', 'text/html'));

const preferences = { type: { '*/*': 1 }};
const result = await store.getRepresentation({ path: baseUrl }, preferences);
await expect(readableToString(result.data)).resolves.toBe('index data');
expect(source.getRepresentation).toHaveBeenCalledTimes(2);
expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${baseUrl}base.html` }, preferences, undefined);
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, {}, undefined);
});
});
6 changes: 4 additions & 2 deletions test/util/FetchUtil.ts
Expand Up @@ -9,9 +9,11 @@ import { LDP } from '../../src/util/Vocabularies';
/**
* This is specifically for GET requests which are expected to succeed.
*/
export async function getResource(url: string, expected?: { contentType?: string }): Promise<Response> {
export async function getResource(url: string,
options?: { accept?: string },
expected?: { contentType?: string }): Promise<Response> {
const isContainer = isContainerPath(url);
const response = await fetch(url);
const response = await fetch(url, { headers: options });
expect(response.status).toBe(200);
expect(response.headers.get('link')).toContain(`<${LDP.Resource}>; rel="type"`);
expect(response.headers.get('link')).toContain(`<${url}.acl>; rel="acl"`);
Expand Down

0 comments on commit c739965

Please sign in to comment.