mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/6.0.0
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { TLSSocket } from 'tls';
|
||||
import type { WebSocket } from 'ws';
|
||||
import type { SingleThreaded } from '../init/cluster/SingleThreaded';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpRequest } from '../server/HttpRequest';
|
||||
import { WebSocketHandler } from '../server/WebSocketHandler';
|
||||
@@ -119,7 +120,7 @@ class WebSocketListener extends EventEmitter {
|
||||
* Provides live update functionality following
|
||||
* the Solid WebSockets API Spec solid-0.1
|
||||
*/
|
||||
export class UnsecureWebSocketsProtocol extends WebSocketHandler {
|
||||
export class UnsecureWebSocketsProtocol extends WebSocketHandler implements SingleThreaded {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
private readonly listeners = new Set<WebSocketListener>();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { Operation } from '../../http/Operation';
|
||||
import type { ErrorHandler } from '../../http/output/error/ErrorHandler';
|
||||
import type { ResponseWriter } from '../../http/output/ResponseWriter';
|
||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
|
||||
@@ -77,6 +78,8 @@ const COOKIES_KEY = 'cookie-secret';
|
||||
* Routes will be updated based on the `baseUrl` and `oidcPath`.
|
||||
*/
|
||||
export class IdentityProviderFactory implements ProviderFactory {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly config: Configuration;
|
||||
private readonly adapterFactory: AdapterFactory;
|
||||
private readonly baseUrl: string;
|
||||
@@ -136,9 +139,44 @@ export class IdentityProviderFactory implements ProviderFactory {
|
||||
const provider = new Provider(this.baseUrl, config);
|
||||
provider.proxy = true;
|
||||
|
||||
this.captureErrorResponses(provider);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the `configureErrors` function below, we configure the `renderError` function of the provider configuration.
|
||||
* This function is called by the OIDC provider library to render errors,
|
||||
* but only does this if the accept header is HTML.
|
||||
* Otherwise, it just returns the error object iself as a JSON object.
|
||||
* See https://github.com/panva/node-oidc-provider/blob/0fcc112e0a95b3b2dae4eba6da812253277567c9/lib/shared/error_handler.js#L48-L52.
|
||||
*
|
||||
* In this function we override the `ctx.accepts` function
|
||||
* to make the above code think HTML is always requested there.
|
||||
* This way we have full control over error representation as configured in `configureErrors`.
|
||||
* We still check the accept headers ourselves so there still is content negotiation on the output,
|
||||
* the client will not simply always receive HTML.
|
||||
*
|
||||
* Should this part of the OIDC library code ever change, our function will break,
|
||||
* at which point behaviour will simply revert to what it was before.
|
||||
*/
|
||||
private captureErrorResponses(provider: Provider): void {
|
||||
provider.use(async(ctx, next): Promise<void> => {
|
||||
const accepts = ctx.accepts.bind(ctx);
|
||||
|
||||
// Using `any` typings to make sure we support all different versions of `ctx.accepts`
|
||||
ctx.accepts = (...types: any[]): any => {
|
||||
// Make sure we only override our specific case
|
||||
if (types.length === 2 && types[0] === 'json' && types[1] === 'html') {
|
||||
return 'html';
|
||||
}
|
||||
return accepts(...types);
|
||||
};
|
||||
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration by copying the internal configuration
|
||||
* and adding the adapter, default audience and jwks/cookie keys.
|
||||
@@ -340,14 +378,19 @@ export class IdentityProviderFactory implements ProviderFactory {
|
||||
// Doesn't really matter which type it is since all relevant fields are optional
|
||||
const oidcError = error as errors.OIDCProviderError;
|
||||
|
||||
let detailedError = error.message;
|
||||
if (oidcError.error_description) {
|
||||
detailedError += ` - ${oidcError.error_description}`;
|
||||
}
|
||||
if (oidcError.error_detail) {
|
||||
detailedError += ` - ${oidcError.error_detail}`;
|
||||
}
|
||||
|
||||
this.logger.warn(`OIDC request failed: ${detailedError}`);
|
||||
|
||||
// OIDC library hides extra details in these fields
|
||||
if (this.showStackTrace) {
|
||||
if (oidcError.error_description) {
|
||||
error.message += ` - ${oidcError.error_description}`;
|
||||
}
|
||||
if (oidcError.error_detail) {
|
||||
oidcError.message += ` - ${oidcError.error_detail}`;
|
||||
}
|
||||
error.message = detailedError;
|
||||
|
||||
// Also change the error message in the stack trace
|
||||
if (error.stack) {
|
||||
|
||||
@@ -21,7 +21,7 @@ const DEFAULT_CLI_RESOLVER = 'urn:solid-server-app-setup:default:CliResolver';
|
||||
const DEFAULT_APP = 'urn:solid-server:default:App';
|
||||
|
||||
const CORE_CLI_PARAMETERS = {
|
||||
config: { type: 'string', alias: 'c', default: DEFAULT_CONFIG, requiresArg: true },
|
||||
config: { type: 'array', alias: 'c', default: [ DEFAULT_CONFIG ], requiresArg: true },
|
||||
loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS },
|
||||
mainModulePath: { type: 'string', alias: 'm', requiresArg: true },
|
||||
} as const;
|
||||
@@ -48,13 +48,13 @@ export class AppRunner {
|
||||
* The values in `variableBindings` take priority over those in `shorthand`.
|
||||
*
|
||||
* @param loaderProperties - Components.js loader properties.
|
||||
* @param configFile - Path to the server config file.
|
||||
* @param configFile - Path to the server config file(s).
|
||||
* @param variableBindings - Bindings of Components.js variables.
|
||||
* @param shorthand - Shorthand values that need to be resolved.
|
||||
*/
|
||||
public async run(
|
||||
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
||||
configFile: string,
|
||||
configFile: string | string[],
|
||||
variableBindings?: VariableBindings,
|
||||
shorthand?: Shorthand,
|
||||
): Promise<void> {
|
||||
@@ -75,13 +75,13 @@ export class AppRunner {
|
||||
* The values in `variableBindings` take priority over those in `shorthand`.
|
||||
*
|
||||
* @param loaderProperties - Components.js loader properties.
|
||||
* @param configFile - Path to the server config file.
|
||||
* @param configFile - Path to the server config file(s).
|
||||
* @param variableBindings - Bindings of Components.js variables.
|
||||
* @param shorthand - Shorthand values that need to be resolved.
|
||||
*/
|
||||
public async create(
|
||||
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
||||
configFile: string,
|
||||
configFile: string | string[],
|
||||
variableBindings?: VariableBindings,
|
||||
shorthand?: Shorthand,
|
||||
): Promise<App> {
|
||||
@@ -152,16 +152,16 @@ export class AppRunner {
|
||||
typeChecking: false,
|
||||
};
|
||||
|
||||
const config = resolveAssetPath(params.config);
|
||||
const configs = params.config.map(resolveAssetPath);
|
||||
|
||||
// Create the Components.js manager used to build components from the provided config
|
||||
let componentsManager: ComponentsManager<any>;
|
||||
try {
|
||||
componentsManager = await this.createComponentsManager(loaderProperties, config);
|
||||
componentsManager = await this.createComponentsManager(loaderProperties, configs);
|
||||
} catch (error: unknown) {
|
||||
// Print help of the expected core CLI parameters
|
||||
const help = await yargv.getHelp();
|
||||
this.resolveError(`${help}\n\nCould not build the config files from ${config}`, error);
|
||||
this.resolveError(`${help}\n\nCould not build the config files from ${configs}`, error);
|
||||
}
|
||||
|
||||
// Build the CLI components and use them to generate values for the Components.js variables
|
||||
@@ -176,10 +176,12 @@ export class AppRunner {
|
||||
*/
|
||||
public async createComponentsManager<T>(
|
||||
loaderProperties: IComponentsManagerBuilderOptions<T>,
|
||||
configFile: string,
|
||||
configFile: string | string[],
|
||||
): Promise<ComponentsManager<T>> {
|
||||
const componentsManager = await ComponentsManager.build(loaderProperties);
|
||||
await componentsManager.configRegistry.register(configFile);
|
||||
for (const config of Array.isArray(configFile) ? configFile : [ configFile ]) {
|
||||
await componentsManager.configRegistry.register(config);
|
||||
}
|
||||
return componentsManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,61 @@
|
||||
import { PassThrough } from 'stream';
|
||||
import { KeysRdfParseJsonLd } from '@comunica/context-entries';
|
||||
import type { NamedNode } from '@rdfjs/types';
|
||||
import fetch from 'cross-fetch';
|
||||
import { readJsonSync } from 'fs-extra';
|
||||
import { FetchDocumentLoader } from 'jsonld-context-parser';
|
||||
import type { IJsonLdContext } from 'jsonld-context-parser';
|
||||
import rdfParser from 'rdf-parse';
|
||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../http/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import { resolveAssetPath } from '../../util/PathUtil';
|
||||
import { pipeSafely } from '../../util/StreamUtil';
|
||||
import { PREFERRED_PREFIX_TERM, SOLID_META } from '../../util/Vocabularies';
|
||||
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
|
||||
/**
|
||||
* First checks if a context is stored locally before letting the super class do a fetch.
|
||||
*/
|
||||
class ContextDocumentLoader extends FetchDocumentLoader {
|
||||
private readonly contexts: Record<string, IJsonLdContext>;
|
||||
|
||||
public constructor(contexts: Record<string, string>) {
|
||||
super(fetch);
|
||||
this.contexts = {};
|
||||
for (const [ key, path ] of Object.entries(contexts)) {
|
||||
this.contexts[key] = readJsonSync(resolveAssetPath(path));
|
||||
}
|
||||
}
|
||||
|
||||
public async load(url: string): Promise<IJsonLdContext> {
|
||||
if (url in this.contexts) {
|
||||
return this.contexts[url];
|
||||
}
|
||||
return super.load(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts most major RDF serializations to `internal/quads`.
|
||||
*
|
||||
* Custom contexts can be defined to be used when parsing JSON-LD.
|
||||
* The keys of the object should be the URL of the context,
|
||||
* and the values the file path of the contexts to use when the JSON-LD parser would fetch the given context.
|
||||
* We use filepaths because embedding them directly into the configurations breaks Components.js.
|
||||
*/
|
||||
export class RdfToQuadConverter extends BaseTypedRepresentationConverter {
|
||||
public constructor() {
|
||||
private readonly documentLoader: ContextDocumentLoader;
|
||||
|
||||
public constructor(contexts: Record<string, string> = {}) {
|
||||
const inputTypes = rdfParser.getContentTypes()
|
||||
// ContentType application/json MAY NOT be converted to Quad.
|
||||
.then((types): string[] => types.filter((type): boolean => type !== 'application/json'));
|
||||
super(inputTypes, INTERNAL_QUADS);
|
||||
this.documentLoader = new ContextDocumentLoader(contexts);
|
||||
}
|
||||
|
||||
public async handle({ representation, identifier }: RepresentationConverterArgs): Promise<Representation> {
|
||||
@@ -27,7 +63,12 @@ export class RdfToQuadConverter extends BaseTypedRepresentationConverter {
|
||||
const rawQuads = rdfParser.parse(representation.data, {
|
||||
contentType: representation.metadata.contentType!,
|
||||
baseIRI: identifier.path,
|
||||
})
|
||||
// All extra keys get passed in the Comunica ActionContext
|
||||
// and this is the key that is used to define the document loader.
|
||||
// See https://github.com/rubensworks/rdf-parse.js/blob/master/lib/RdfParser.ts
|
||||
// and https://github.com/comunica/comunica/blob/master/packages/actor-rdf-parse-jsonld/lib/ActorRdfParseJsonLd.ts
|
||||
[KeysRdfParseJsonLd.documentLoader.name]: this.documentLoader,
|
||||
} as any)
|
||||
// This works only for those cases where the data stream has been completely read before accessing the metadata.
|
||||
// Eg. the PATCH operation, which is the main case why we store the prefixes in metadata here if there are any.
|
||||
// See also https://github.com/CommunitySolidServer/CommunitySolidServer/issues/126
|
||||
|
||||
Reference in New Issue
Block a user