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:
elf Pavlik 2024-08-19 00:58:53 -06:00 committed by GitHub
parent 4599bf413e
commit 3e8365bb26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 42 additions and 26 deletions

View File

@ -1,6 +1,7 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
"import": [
"css:config/http/notifications/base/description.json",
"css:config/http/notifications/base/handler.json",
"css:config/http/notifications/base/http.json",
"css:config/http/notifications/base/storage.json",

View File

@ -2,16 +2,16 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Path prefix used by streaming HTTP receiveFrom endpoints",
"@id": "urn:solid-server:default:variable:streamingHTTPReceiveFromPrefix",
"valueRaw": ".notifications/StreamingHTTPChannel2023/"
"@id": "urn:solid-server:default:StreamingHTTP2023Route",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:default:NotificationRoute" },
"relativePath": "/StreamingHTTPChannel2023/"
},
{
"comment": "Creates updatesViaStreamingHttp2023 Link relations",
"@id": "urn:solid-server:default:StreamingHttpMetadataWriter",
"@type": "StreamingHttpMetadataWriter",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"pathPrefix": { "@id": "urn:solid-server:default:variable:streamingHTTPReceiveFromPrefix" }
"route": { "@id": "urn:solid-server:default:StreamingHTTP2023Route" }
},
{
"comment": "Allows discovery of the corresponding streaming HTTP channel",
@ -32,7 +32,7 @@
"@id": "urn:solid-server:default:StreamingHttp2023RequestHandler",
"@type": "StreamingHttpRequestHandler",
"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" },
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
"credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },

View File

@ -1,6 +1,8 @@
import { getLoggerFor } from '../../../logging/LogUtil';
import type { HttpResponse } from '../../HttpResponse';
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 { MetadataWriter } from '../../../http/output/metadata/MetadataWriter';
@ -12,15 +14,14 @@ export class StreamingHttpMetadataWriter extends MetadataWriter {
protected readonly logger = getLoggerFor(this);
public constructor(
private readonly baseUrl: string,
private readonly pathPrefix: string,
private readonly route: InteractionRoute,
) {
super();
}
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
const resourcePath = input.metadata.identifier.value.replace(this.baseUrl, '');
const receiveFrom = `${this.baseUrl}${this.pathPrefix}${resourcePath}`;
const encodedUrl = encodeURIComponent(input.metadata.identifier.value);
const receiveFrom = joinUrl(this.route.getPath(), encodedUrl);
const link = `<${receiveFrom}>; rel="http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023"`;
this.logger.debug('Adding updatesViaStreamingHttp2023 to the Link header');
addHeader(input.response, 'Link', link);

View File

@ -7,6 +7,7 @@ import { AccessMode } from '../../../authorization/permissions/Permissions';
import { OkResponseDescription } from '../../../http/output/response/OkResponseDescription';
import type { ResponseDescription } from '../../../http/output/response/ResponseDescription';
import { BasicRepresentation } from '../../../http/representation/BasicRepresentation';
import type { InteractionRoute } from '../../../identity/interaction/routing/InteractionRoute';
import { getLoggerFor } from '../../../logging/LogUtil';
import type { OperationHttpHandlerInput } from '../../OperationHttpHandler';
import { OperationHttpHandler } from '../../OperationHttpHandler';
@ -28,7 +29,7 @@ export class StreamingHttpRequestHandler extends OperationHttpHandler {
public constructor(
private readonly streamMap: StreamingHttpMap,
private readonly pathPrefix: string,
private readonly route: InteractionRoute,
private readonly generator: NotificationGenerator,
private readonly serializer: NotificationSerializer,
private readonly credentialsExtractor: CredentialsExtractor,
@ -39,7 +40,8 @@ export class StreamingHttpRequestHandler extends OperationHttpHandler {
}
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
const credentials = await this.credentialsExtractor.handleSafe(request);

View File

@ -17,6 +17,7 @@ import namedNode = DataFactory.namedNode;
const port = getPort('StreamingHTTPChannel2023');
const baseUrl = `http://localhost:${port}/`;
const pathPrefix = '.notifications/StreamingHTTPChannel2023';
const rootFilePath = getTestFolder('StreamingHTTPChannel2023');
const stores: [string, any][] = [
@ -38,13 +39,16 @@ async function readChunk(reader: ReadableStreamDefaultReader): Promise<Store> {
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 => {
let app: App;
let store: ResourceStore;
const webId = 'http://example.com/card/#me';
const topic = joinUrl(baseUrl, '/foo');
const pathPrefix = '.notifications/StreamingHTTPChannel2023';
const receiveFrom = joinUrl(baseUrl, pathPrefix, '/foo');
const receiveFrom = endpoint(topic);
beforeAll(async(): Promise<void> => {
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> => {
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'));
// 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> => {
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
const streamingResponse = await fetch(baseReceiveFrom);

View File

@ -2,19 +2,24 @@ import { createResponse } from 'node-mocks-http';
import {
StreamingHttpMetadataWriter,
} from '../../../../../src/server/notifications/StreamingHttpChannel2023/StreamingHttpMetadataWriter';
import {
AbsolutePathInteractionRoute,
} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import type { ResourceIdentifier } from '../../../../../src/http/representation/ResourceIdentifier';
describe('A StreamingHttpMetadataWriter', (): void => {
const baseUrl = 'http://example.org/';
const pathPrefix = '.notifications/StreamingHTTPChannel2023/';
const writer = new StreamingHttpMetadataWriter(baseUrl, pathPrefix);
const path = 'http://example.org/.notifications/StreamingHTTPChannel2023/';
const route = new AbsolutePathInteractionRoute(path);
const writer = new StreamingHttpMetadataWriter(route);
const rel = 'http://www.w3.org/ns/solid/terms#updatesViaStreamingHttp2023';
it('adds the correct link header.', async(): Promise<void> => {
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
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();
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}"` });
});
});

View File

@ -16,8 +16,8 @@ import { getLoggerFor } from '../../../../../src/logging/LogUtil';
import {
StreamingHttpRequestHandler,
} from '../../../../../src/server/notifications/StreamingHttpChannel2023/StreamingHttpRequestHandler';
import { AbsolutePathInteractionRoute, StreamingHttpMap } from '../../../../../src';
import type { NotificationGenerator, NotificationSerializer } from '../../../../../src';
import { StreamingHttpMap } from '../../../../../src';
import type { Notification } from '../../../../../src/server/notifications/Notification';
import { flushPromises } from '../../../../util/Util';
@ -31,7 +31,7 @@ jest.mock('../../../../../src/logging/LogUtil', (): any => {
describe('A StreamingHttpRequestHandler', (): void => {
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
const pathPrefix = '.notifications/StreamingHTTPChannel2023/';
const path = 'http://example.com/.notifications/StreamingHTTPChannel2023/';
const channel: NotificationChannel = {
id: 'id',
topic: topic.path,
@ -52,6 +52,7 @@ describe('A StreamingHttpRequestHandler', (): void => {
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;
let representation: BasicRepresentation;
let route: AbsolutePathInteractionRoute;
let streamMap: StreamingHttpMap;
let operation: Operation;
let generator: jest.Mocked<NotificationGenerator>;
@ -64,12 +65,14 @@ describe('A StreamingHttpRequestHandler', (): void => {
beforeEach(async(): Promise<void> => {
operation = {
method: 'GET',
target: { path: 'http://example.com/.notifications/StreamingHTTPChannel2023/foo' },
target: { path: `${path}${encodeURIComponent(topic.path)}` },
body: new BasicRepresentation(),
preferences: {},
};
representation = new BasicRepresentation(chunk, 'text/plain');
route = new AbsolutePathInteractionRoute(path);
streamMap = new StreamingHttpMap();
generator = {
@ -95,7 +98,7 @@ describe('A StreamingHttpRequestHandler', (): void => {
handler = new StreamingHttpRequestHandler(
streamMap,
pathPrefix,
route,
generator,
serializer,
credentialsExtractor,
@ -151,7 +154,7 @@ describe('A StreamingHttpRequestHandler', (): void => {
} as any;
handler = new StreamingHttpRequestHandler(
streamMap,
pathPrefix,
route,
generator,
serializer,
credentialsExtractor,