mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Use full encoded topic iri in streaming http receiveFrom url template
* fix: use full encoded topic iri in streaming http receiveFrom url template * clean up urls and routing
This commit is contained in:
parent
4599bf413e
commit
3e8365bb26
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
||||||
"import": [
|
"import": [
|
||||||
|
"css:config/http/notifications/base/description.json",
|
||||||
"css:config/http/notifications/base/handler.json",
|
"css:config/http/notifications/base/handler.json",
|
||||||
"css:config/http/notifications/base/http.json",
|
"css:config/http/notifications/base/http.json",
|
||||||
"css:config/http/notifications/base/storage.json",
|
"css:config/http/notifications/base/storage.json",
|
||||||
|
@ -2,16 +2,16 @@
|
|||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"comment": "Path prefix used by streaming HTTP receiveFrom endpoints",
|
"@id": "urn:solid-server:default:StreamingHTTP2023Route",
|
||||||
"@id": "urn:solid-server:default:variable:streamingHTTPReceiveFromPrefix",
|
"@type": "RelativePathInteractionRoute",
|
||||||
"valueRaw": ".notifications/StreamingHTTPChannel2023/"
|
"base": { "@id": "urn:solid-server:default:NotificationRoute" },
|
||||||
|
"relativePath": "/StreamingHTTPChannel2023/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Creates updatesViaStreamingHttp2023 Link relations",
|
"comment": "Creates updatesViaStreamingHttp2023 Link relations",
|
||||||
"@id": "urn:solid-server:default:StreamingHttpMetadataWriter",
|
"@id": "urn:solid-server:default:StreamingHttpMetadataWriter",
|
||||||
"@type": "StreamingHttpMetadataWriter",
|
"@type": "StreamingHttpMetadataWriter",
|
||||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"route": { "@id": "urn:solid-server:default:StreamingHTTP2023Route" }
|
||||||
"pathPrefix": { "@id": "urn:solid-server:default:variable:streamingHTTPReceiveFromPrefix" }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Allows discovery of the corresponding streaming HTTP channel",
|
"comment": "Allows discovery of the corresponding streaming HTTP channel",
|
||||||
@ -32,7 +32,7 @@
|
|||||||
"@id": "urn:solid-server:default:StreamingHttp2023RequestHandler",
|
"@id": "urn:solid-server:default:StreamingHttp2023RequestHandler",
|
||||||
"@type": "StreamingHttpRequestHandler",
|
"@type": "StreamingHttpRequestHandler",
|
||||||
"streamMap": { "@id": "urn:solid-server:default:StreamingHttpMap" },
|
"streamMap": { "@id": "urn:solid-server:default:StreamingHttpMap" },
|
||||||
"pathPrefix": { "@id": "urn:solid-server:default:variable:streamingHTTPReceiveFromPrefix" },
|
"route": { "@id": "urn:solid-server:default:StreamingHTTP2023Route" },
|
||||||
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
||||||
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
||||||
"credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
"credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||||
import type { HttpResponse } from '../../HttpResponse';
|
import type { HttpResponse } from '../../HttpResponse';
|
||||||
import { addHeader } from '../../../util/HeaderUtil';
|
import { addHeader } from '../../../util/HeaderUtil';
|
||||||
|
import { joinUrl } from '../../../util/PathUtil';
|
||||||
|
import type { InteractionRoute } from '../../../identity/interaction/routing/InteractionRoute';
|
||||||
import type { RepresentationMetadata } from '../../../http/representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../../../http/representation/RepresentationMetadata';
|
||||||
import { MetadataWriter } from '../../../http/output/metadata/MetadataWriter';
|
import { MetadataWriter } from '../../../http/output/metadata/MetadataWriter';
|
||||||
|
|
||||||
@ -12,15 +14,14 @@ export class StreamingHttpMetadataWriter extends MetadataWriter {
|
|||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly baseUrl: string,
|
private readonly route: InteractionRoute,
|
||||||
private readonly pathPrefix: string,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||||
const resourcePath = input.metadata.identifier.value.replace(this.baseUrl, '');
|
const encodedUrl = encodeURIComponent(input.metadata.identifier.value);
|
||||||
const receiveFrom = `${this.baseUrl}${this.pathPrefix}${resourcePath}`;
|
const receiveFrom = joinUrl(this.route.getPath(), encodedUrl);
|
||||||
const link = `<${receiveFrom}>; rel="http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023"`;
|
const link = `<${receiveFrom}>; rel="http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023"`;
|
||||||
this.logger.debug('Adding updatesViaStreamingHttp2023 to the Link header');
|
this.logger.debug('Adding updatesViaStreamingHttp2023 to the Link header');
|
||||||
addHeader(input.response, 'Link', link);
|
addHeader(input.response, 'Link', link);
|
||||||
|
@ -7,6 +7,7 @@ import { AccessMode } from '../../../authorization/permissions/Permissions';
|
|||||||
import { OkResponseDescription } from '../../../http/output/response/OkResponseDescription';
|
import { OkResponseDescription } from '../../../http/output/response/OkResponseDescription';
|
||||||
import type { ResponseDescription } from '../../../http/output/response/ResponseDescription';
|
import type { ResponseDescription } from '../../../http/output/response/ResponseDescription';
|
||||||
import { BasicRepresentation } from '../../../http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../http/representation/BasicRepresentation';
|
||||||
|
import type { InteractionRoute } from '../../../identity/interaction/routing/InteractionRoute';
|
||||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||||
import type { OperationHttpHandlerInput } from '../../OperationHttpHandler';
|
import type { OperationHttpHandlerInput } from '../../OperationHttpHandler';
|
||||||
import { OperationHttpHandler } from '../../OperationHttpHandler';
|
import { OperationHttpHandler } from '../../OperationHttpHandler';
|
||||||
@ -28,7 +29,7 @@ export class StreamingHttpRequestHandler extends OperationHttpHandler {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly streamMap: StreamingHttpMap,
|
private readonly streamMap: StreamingHttpMap,
|
||||||
private readonly pathPrefix: string,
|
private readonly route: InteractionRoute,
|
||||||
private readonly generator: NotificationGenerator,
|
private readonly generator: NotificationGenerator,
|
||||||
private readonly serializer: NotificationSerializer,
|
private readonly serializer: NotificationSerializer,
|
||||||
private readonly credentialsExtractor: CredentialsExtractor,
|
private readonly credentialsExtractor: CredentialsExtractor,
|
||||||
@ -39,7 +40,8 @@ export class StreamingHttpRequestHandler extends OperationHttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ operation, request }: OperationHttpHandlerInput): Promise<ResponseDescription> {
|
public async handle({ operation, request }: OperationHttpHandlerInput): Promise<ResponseDescription> {
|
||||||
const topic = operation.target.path.replace(this.pathPrefix, '');
|
const encodedUrl = operation.target.path.replace(this.route.getPath(), '');
|
||||||
|
const topic = decodeURIComponent(encodedUrl);
|
||||||
|
|
||||||
// Verify if the client is allowed to connect
|
// Verify if the client is allowed to connect
|
||||||
const credentials = await this.credentialsExtractor.handleSafe(request);
|
const credentials = await this.credentialsExtractor.handleSafe(request);
|
||||||
|
@ -17,6 +17,7 @@ import namedNode = DataFactory.namedNode;
|
|||||||
|
|
||||||
const port = getPort('StreamingHTTPChannel2023');
|
const port = getPort('StreamingHTTPChannel2023');
|
||||||
const baseUrl = `http://localhost:${port}/`;
|
const baseUrl = `http://localhost:${port}/`;
|
||||||
|
const pathPrefix = '.notifications/StreamingHTTPChannel2023';
|
||||||
|
|
||||||
const rootFilePath = getTestFolder('StreamingHTTPChannel2023');
|
const rootFilePath = getTestFolder('StreamingHTTPChannel2023');
|
||||||
const stores: [string, any][] = [
|
const stores: [string, any][] = [
|
||||||
@ -38,13 +39,16 @@ async function readChunk(reader: ReadableStreamDefaultReader): Promise<Store> {
|
|||||||
return new Store(parser.parse(notification));
|
return new Store(parser.parse(notification));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function endpoint(topic: string): string {
|
||||||
|
return joinUrl(baseUrl, pathPrefix, encodeURIComponent(topic));
|
||||||
|
}
|
||||||
|
|
||||||
describe.each(stores)('A server supporting StreamingHTTPChannel2023 using %s', (name, { configs, teardown }): void => {
|
describe.each(stores)('A server supporting StreamingHTTPChannel2023 using %s', (name, { configs, teardown }): void => {
|
||||||
let app: App;
|
let app: App;
|
||||||
let store: ResourceStore;
|
let store: ResourceStore;
|
||||||
const webId = 'http://example.com/card/#me';
|
const webId = 'http://example.com/card/#me';
|
||||||
const topic = joinUrl(baseUrl, '/foo');
|
const topic = joinUrl(baseUrl, '/foo');
|
||||||
const pathPrefix = '.notifications/StreamingHTTPChannel2023';
|
const receiveFrom = endpoint(topic);
|
||||||
const receiveFrom = joinUrl(baseUrl, pathPrefix, '/foo');
|
|
||||||
|
|
||||||
beforeAll(async(): Promise<void> => {
|
beforeAll(async(): Promise<void> => {
|
||||||
const variables = {
|
const variables = {
|
||||||
@ -246,7 +250,7 @@ describe.each(stores)('A server supporting StreamingHTTPChannel2023 using %s', (
|
|||||||
|
|
||||||
it('prevents connecting to channels of restricted topics.', async(): Promise<void> => {
|
it('prevents connecting to channels of restricted topics.', async(): Promise<void> => {
|
||||||
const restricted = joinUrl(baseUrl, '/restricted');
|
const restricted = joinUrl(baseUrl, '/restricted');
|
||||||
const restrictedReceiveFrom = joinUrl(baseUrl, pathPrefix, '/restricted');
|
const restrictedReceiveFrom = endpoint(restricted);
|
||||||
await store.setRepresentation({ path: restricted }, new BasicRepresentation('new', 'text/plain'));
|
await store.setRepresentation({ path: restricted }, new BasicRepresentation('new', 'text/plain'));
|
||||||
|
|
||||||
// Only allow our WebID to read
|
// Only allow our WebID to read
|
||||||
@ -285,7 +289,7 @@ describe.each(stores)('A server supporting StreamingHTTPChannel2023 using %s', (
|
|||||||
|
|
||||||
it('emits container notifications if contents get added or removed.', async(): Promise<void> => {
|
it('emits container notifications if contents get added or removed.', async(): Promise<void> => {
|
||||||
const resource = joinUrl(baseUrl, '/resource');
|
const resource = joinUrl(baseUrl, '/resource');
|
||||||
const baseReceiveFrom = joinUrl(baseUrl, pathPrefix, '/');
|
const baseReceiveFrom = endpoint(joinUrl(baseUrl, '/'));
|
||||||
|
|
||||||
// Connecting to the base URL, which is the parent container
|
// Connecting to the base URL, which is the parent container
|
||||||
const streamingResponse = await fetch(baseReceiveFrom);
|
const streamingResponse = await fetch(baseReceiveFrom);
|
||||||
|
@ -2,19 +2,24 @@ import { createResponse } from 'node-mocks-http';
|
|||||||
import {
|
import {
|
||||||
StreamingHttpMetadataWriter,
|
StreamingHttpMetadataWriter,
|
||||||
} from '../../../../../src/server/notifications/StreamingHttpChannel2023/StreamingHttpMetadataWriter';
|
} from '../../../../../src/server/notifications/StreamingHttpChannel2023/StreamingHttpMetadataWriter';
|
||||||
|
import {
|
||||||
|
AbsolutePathInteractionRoute,
|
||||||
|
} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';
|
||||||
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
|
||||||
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
||||||
|
import type { ResourceIdentifier } from '../../../../../src/http/representation/ResourceIdentifier';
|
||||||
|
|
||||||
describe('A StreamingHttpMetadataWriter', (): void => {
|
describe('A StreamingHttpMetadataWriter', (): void => {
|
||||||
const baseUrl = 'http://example.org/';
|
const path = 'http://example.org/.notifications/StreamingHTTPChannel2023/';
|
||||||
const pathPrefix = '.notifications/StreamingHTTPChannel2023/';
|
const route = new AbsolutePathInteractionRoute(path);
|
||||||
const writer = new StreamingHttpMetadataWriter(baseUrl, pathPrefix);
|
const writer = new StreamingHttpMetadataWriter(route);
|
||||||
const rel = 'http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023';
|
const rel = 'http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023';
|
||||||
|
|
||||||
it('adds the correct link header.', async(): Promise<void> => {
|
it('adds the correct link header.', async(): Promise<void> => {
|
||||||
|
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
||||||
const response = createResponse() as HttpResponse;
|
const response = createResponse() as HttpResponse;
|
||||||
const metadata = new RepresentationMetadata({ path: 'http://example.org/foo/bar/baz' });
|
const metadata = new RepresentationMetadata(topic);
|
||||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||||
expect(response.getHeaders()).toEqual({ link: `<http://example.org/.notifications/StreamingHTTPChannel2023/foo/bar/baz>; rel="${rel}"` });
|
expect(response.getHeaders()).toEqual({ link: `<http://example.org/.notifications/StreamingHTTPChannel2023/${encodeURIComponent(topic.path)}>; rel="${rel}"` });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,8 +16,8 @@ import { getLoggerFor } from '../../../../../src/logging/LogUtil';
|
|||||||
import {
|
import {
|
||||||
StreamingHttpRequestHandler,
|
StreamingHttpRequestHandler,
|
||||||
} from '../../../../../src/server/notifications/StreamingHttpChannel2023/StreamingHttpRequestHandler';
|
} from '../../../../../src/server/notifications/StreamingHttpChannel2023/StreamingHttpRequestHandler';
|
||||||
|
import { AbsolutePathInteractionRoute, StreamingHttpMap } from '../../../../../src';
|
||||||
import type { NotificationGenerator, NotificationSerializer } from '../../../../../src';
|
import type { NotificationGenerator, NotificationSerializer } from '../../../../../src';
|
||||||
import { StreamingHttpMap } from '../../../../../src';
|
|
||||||
import type { Notification } from '../../../../../src/server/notifications/Notification';
|
import type { Notification } from '../../../../../src/server/notifications/Notification';
|
||||||
import { flushPromises } from '../../../../util/Util';
|
import { flushPromises } from '../../../../util/Util';
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ jest.mock('../../../../../src/logging/LogUtil', (): any => {
|
|||||||
describe('A StreamingHttpRequestHandler', (): void => {
|
describe('A StreamingHttpRequestHandler', (): void => {
|
||||||
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
|
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
|
||||||
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
||||||
const pathPrefix = '.notifications/StreamingHTTPChannel2023/';
|
const path = 'http://example.com/.notifications/StreamingHTTPChannel2023/';
|
||||||
const channel: NotificationChannel = {
|
const channel: NotificationChannel = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
topic: topic.path,
|
topic: topic.path,
|
||||||
@ -52,6 +52,7 @@ describe('A StreamingHttpRequestHandler', (): void => {
|
|||||||
const request: HttpRequest = {} as any;
|
const request: HttpRequest = {} as any;
|
||||||
const response: HttpResponse = {} as any;
|
const response: HttpResponse = {} as any;
|
||||||
let representation: BasicRepresentation;
|
let representation: BasicRepresentation;
|
||||||
|
let route: AbsolutePathInteractionRoute;
|
||||||
let streamMap: StreamingHttpMap;
|
let streamMap: StreamingHttpMap;
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
let generator: jest.Mocked<NotificationGenerator>;
|
let generator: jest.Mocked<NotificationGenerator>;
|
||||||
@ -64,12 +65,14 @@ describe('A StreamingHttpRequestHandler', (): void => {
|
|||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
operation = {
|
operation = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
target: { path: 'http://example.com/.notifications/StreamingHTTPChannel2023/foo' },
|
target: { path: `${path}${encodeURIComponent(topic.path)}` },
|
||||||
body: new BasicRepresentation(),
|
body: new BasicRepresentation(),
|
||||||
preferences: {},
|
preferences: {},
|
||||||
};
|
};
|
||||||
representation = new BasicRepresentation(chunk, 'text/plain');
|
representation = new BasicRepresentation(chunk, 'text/plain');
|
||||||
|
|
||||||
|
route = new AbsolutePathInteractionRoute(path);
|
||||||
|
|
||||||
streamMap = new StreamingHttpMap();
|
streamMap = new StreamingHttpMap();
|
||||||
|
|
||||||
generator = {
|
generator = {
|
||||||
@ -95,7 +98,7 @@ describe('A StreamingHttpRequestHandler', (): void => {
|
|||||||
|
|
||||||
handler = new StreamingHttpRequestHandler(
|
handler = new StreamingHttpRequestHandler(
|
||||||
streamMap,
|
streamMap,
|
||||||
pathPrefix,
|
route,
|
||||||
generator,
|
generator,
|
||||||
serializer,
|
serializer,
|
||||||
credentialsExtractor,
|
credentialsExtractor,
|
||||||
@ -151,7 +154,7 @@ describe('A StreamingHttpRequestHandler', (): void => {
|
|||||||
} as any;
|
} as any;
|
||||||
handler = new StreamingHttpRequestHandler(
|
handler = new StreamingHttpRequestHandler(
|
||||||
streamMap,
|
streamMap,
|
||||||
pathPrefix,
|
route,
|
||||||
generator,
|
generator,
|
||||||
serializer,
|
serializer,
|
||||||
credentialsExtractor,
|
credentialsExtractor,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user