diff --git a/5.0/404.html b/5.0/404.html new file mode 100644 index 000000000..80482b8b2 --- /dev/null +++ b/5.0/404.html @@ -0,0 +1,945 @@ + + + +
+ + + + + + + + + + + +There are several core building blocks used in many places of the server. +These are described here.
+A very important building block that gets reused in many places is the AsyncHandler
.
+The idea is that a handler has 2 important functions.
+canHandle
determines if this class is capable of correctly handling the request,
+and throws an error if it can not.
+For example, a class that converts JSON-LD to turtle can handle all requests containing JSON-LD data,
+but does not know what to do with a request that contains a JPEG.
+The second function is handle
where the class executes on the input data and returns the result.
+If an error gets thrown here it means there is an issue with the input.
+For example, if the input data claims to be JSON-LD but is actually not.
The power of using this interface really shines when using certain utility classes.
+The one we use the most is the WaterfallHandler
,
+which takes as input a list of handlers of the same type.
+The input and output of a WaterfallHandler
is the same as those of its inputs,
+meaning it can be used in the same places.
+When doing a canHandle
call, it will iterate over all its input handlers
+to find the first one where the canHandle
call succeeds,
+and when calling handle
it will return the result of that specific handler.
+This allows us to chain together many handlers that each have their specific niche,
+such as handler that each support a specific HTTP method (GET/PUT/POST/etc.),
+or handlers that only take requests targeting a specific subset of URLs.
+To the parent class it will look like it has a handler that supports all methods,
+while in practice it will be a WaterfallHandler
containing all these separate handlers.
Some other utility classes are the ParallelHandler
that runs all handlers simultaneously,
+and the SequenceHandler
that runs all of them one after the other.
+Since multiple handlers are executed here, these only work for handlers that have no output.
Almost all data is handled in a streaming fashion.
+This allows us to work with very large resources without having to fully load them in memory,
+a client could be reading data that is being returned by the server while the server is still reading the file.
+Internally this means we are mostly handling data as Readable
objects.
+We actually use Guarded<Readable>
which is an internal format we created to help us with error handling.
+Such streams can be created using utility functions such as guardStream
and guardedStreamFrom
.
+Similarly, we have a pipeSafely
to pipe streams in such a way that also helps with errors.
The community server uses the dependency injection framework +Components.js +to link all class instances together, +and uses Components-Generator.js +to automatically generate the necessary description configurations of all classes. +This framework allows us to configure our components in a JSON file. +The advantage of this is that changing the configuration of components does not require any changes to the code, +as one can just change the default configuration file, or provide a custom configuration file.
+More information can be found in the Components.js documentation, +but a summarized overview can be found below.
+Components.js requires a component file for every class you might want to instantiate.
+Fortunately those get generated automatically by Components-Generator.js.
+Calling npm run build
will call the generator and generate those JSON-LD files in the dist
folder.
+The generator uses the index.ts
, so new classes always have to be added there
+or they will not get a component file.
Configuration files are how we tell Components.js which classes to instantiate and link together.
+All the community server configurations can be found in
+the config
folder.
+That folder also contains information about how different pre-defined configurations can be used.
A single component in such a configuration file might look as follows: +
{
+ "comment": "Storage used for account management.",
+ "@id": "urn:solid-server:default:AccountStorage",
+ "@type": "JsonResourceStorage",
+ "source": { "@id": "urn:solid-server:default:ResourceStore" },
+ "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
+ "container": "/.internal/accounts/"
+}
+
With the corresponding constructor of the JsonResourceStorage
class:
+
public constructor(source: ResourceStore, baseUrl: string, container: string)
+
The important elements here are the following:
+* "comment"
: (optional) A description of this component.
+* "@id"
: (optional) A unique identifier of this component, which allows it to be used as parameter values in different places.
+* "@type"
: The class name of the component. This must be a TypeScript class name that is exported via index.ts
.
As you can see from the constructor, the other fields are direct mappings from the constructor parameters.
+source
references another object, which we refer to using its identifier urn:solid-server:default:ResourceStore
.
+baseUrl
is just a string, but here we use a variable that was set before calling Components.js
+which is why it also references an @id
.
+These variables are set when starting up the server, based on the command line parameters.
When starting the server, the application actually uses Components.js twice to instantiate components. +The first instantiation is used to parse the command line arguments. +These then get converted into Components.js variables and are used to instantiate the actual server.
+flowchart TD
+ CliResolver("<strong>CliResolver</strong><br>CliResolver")
+ CliResolver --> CliResolverArgs
+
+ subgraph CliResolverArgs[" "]
+ CliExtractor("<strong>CliExtractor</strong><br>YargsCliExtractor")
+ ShorthandResolver("<strong>ShorthandResolver</strong><br>CombinedShorthandResolver")
+ end
+
+ ShorthandResolver --> ShorthandResolverArgs
+ subgraph ShorthandResolverArgs[" "]
+ BaseUrlExtractor("<br>BaseUrlExtractor")
+ KeyExtractor("<br>KeyExtractor")
+ AssetPathExtractor("<br>AssetPathExtractor")
+ end
+The CliResolver
(urn:solid-server-app-setup:default:CliResolver
) is simply a way
+to combine both the CliExtractor
(urn:solid-server-app-setup:default:CliExtractor
)
+and ShorthandResolver
(urn:solid-server-app-setup:default:ShorthandResolver
)
+into a single object and has no other function.
Which arguments are supported and which Components.js variables are generated +can depend on the configuration that is being used. +For example, for an HTTPS server additional arguments will be needed to specify the necessary key/cert files.
+The CliResolver
converts the incoming string of arguments into a key/value object.
+By default, a YargsCliExtractor
is used, which makes use of the yargs
library and is configured similarly.
The ShorthandResolver
uses the key/value object that was generated above to generate Components.js variable bindings.
+A CombinedShorthandResolver
combines the results of multiple ShorthandExtractor
+by mapping their values to specific variables.
+For example, a BaseUrlExtractor
will be used to extract the value for baseUrl
,
+or port
if no baseUrl
value is provided,
+and use it to generate the value for the variable urn:solid-server:default:variable:baseUrl
.
These extractors are also where the default values for the server are defined.
+For example, BaseUrlExtractor will be instantiated with a default port of 3000
+which will be used if no port is provided.
The variables generated here will be used to initialize the server.
+ + +The direction of the arrows was changed slightly here to make the graph readable. +
flowchart LR
+ HttpHandler("<strong>HttpHandler</strong><br>SequenceHandler")
+ HttpHandler --> HttpHandlerArgs
+
+ subgraph HttpHandlerArgs[" "]
+ direction LR
+ Middleware("<strong>Middleware</strong><br><i>HttpHandler</i>")
+ WaterfallHandler("<br>WaterfallHandler")
+ end
+
+ Middleware --> WaterfallHandler
+ WaterfallHandler --> WaterfallHandlerArgs
+
+ subgraph WaterfallHandlerArgs[" "]
+ direction TB
+ StaticAssetHandler("<strong>StaticAssetHandler</strong><br>StaticAssetHandler")
+ SetupHandler("<strong>SetupHandler</strong><br><i>HttpHandler</i>")
+ OidcHandler("<strong>OidcHandler</strong><br><i>HttpHandler</i>")
+ AuthResourceHttpHandler("<strong>AuthResourceHttpHandler</strong><br><i>HttpHandler</i>")
+ IdentityProviderHttpHandler("<strong>IdentityProviderHttpHandler</strong><br><i>HttpHandler</i>")
+ LdpHandler("<strong>LdpHandler</strong><br><i>HttpHandler</i>")
+ end
+
+ StaticAssetHandler --> SetupHandler
+ SetupHandler --> OidcHandler
+ OidcHandler --> AuthResourceHttpHandler
+ AuthResourceHttpHandler --> IdentityProviderHttpHandler
+ IdentityProviderHttpHandler --> LdpHandler
+The HttpHandler
is responsible for handling an incoming HTTP request.
+The request will always first go through the Middleware
,
+where certain required headers will be added such as CORS headers.
After that it will go through the list in the WaterfallHandler
+to find the first handler that understands the request,
+with the LdpHandler
at the bottom being the catch-all default.
The urn:solid-server:default:StaticAssetHandler
matches exact URLs to static assets which require no further logic.
+An example of this is the favicon, where the /favicon.ico
URL
+is directed to the favicon file at /templates/images/favicon.ico
.
+It can also map entire folders to a specific path, such as /.well-known/css/styles/
which contains all stylesheets.
The urn:solid-server:default:SetupHandler
is responsible
+for redirecting all requests to /setup
until setup is finished,
+thereby ensuring that setup needs to be finished before anything else can be done on the server,
+and handling the actual setup request that is sent to /setup
.
+Once setup is finished, this handler will reject all requests and thus no longer be relevant.
If the server is configured to not have setup enabled, +the corresponding identifier will point to a handler that always rejects all requests.
+The urn:solid-server:default:OidcHandler
handles all requests related
+to the Solid-OIDC specification.
+The OIDC component is configured to work on the /.oidc/
subpath,
+so this handler catches all those requests and sends them to the internal OIDC library that is used.
The urn:solid-server:default:AuthResourceHttpHandler
is identical
+to the urn:solid-server:default:LdpHandler
which will be discussed below,
+but only handles resources relevant for authorization.
In practice this means that is your server is configured
+to use Web Access Control for authorization,
+this handler will catch all requests targeting .acl
resources.
The reason these already need to be handled here is so these can also be used +to allow authorization on the following handler(s). +More on this can be found in the identity provider documentation
+The urn:solid-server:default:IdentityProviderHttpHandler
handles everything
+related to our custom identity provider API, such as registering, logging in, returning the relevant HTML pages, etc.
+All these requests are identified by being on the /idp/
subpath.
+More information on the API can be found in the identity provider documentation
Once a request reaches the urn:solid-server:default:LdpHandler
,
+the server assumes this is a standard Solid request according to the Solid protocol.
+A detailed description of what happens then can be found here
When starting the server, multiple Initializers trigger to set up everything correctly, +the last one of which starts listening to the specified port. +Similarly, when stopping the server several Finalizers trigger to clean up where necessary, +although the latter only happens when starting the server through code.
+flowchart TD
+ App("<strong>App</strong><br>App")
+ App --> AppArgs
+
+ subgraph AppArgs[" "]
+ Initializer("<strong>Initializer</strong><br><i>Initializer</i>")
+ AppFinalizer("<strong>Finalizer</strong><br><i>Finalizer</i>")
+ end
+App
(urn:solid-server:default:App
) is the main component that gets instantiated by Components.js.
+Every other component should be able to trace an instantiation path back to it if it also wants to be instantiated.
It's only function is to contain an Initializer
and Finalizer
+which get called by calling start
/stop
respectively.
flowchart TD
+ Initializer("<strong>Initializer</strong><br>SequenceHandler")
+ Initializer --> InitializerArgs
+
+ subgraph InitializerArgs[" "]
+ direction LR
+ LoggerInitializer("<strong>LoggerInitializer</strong><br>LoggerInitializer")
+ PrimaryInitializer("<strong>PrimaryInitializer</strong><br>ProcessHandler")
+ WorkerInitializer("<strong>WorkerInitializer</strong><br>ProcessHandler")
+ end
+
+ LoggerInitializer --> PrimaryInitializer
+ PrimaryInitializer --> WorkerInitializer
+The very first thing that needs to happen is initializing the logger. +Before this other classes will be unable to use logging.
+The PrimaryInitializer
will only trigger once, in the primary worker thread,
+while the WorkerInitializer
will trigger for every worker thread.
+Although if your server setup is single-threaded, which is the default,
+there is no relevant difference between those two.
flowchart TD
+ PrimaryInitializer("<strong>PrimaryInitializer</strong><br>ProcessHandler")
+ PrimaryInitializer --> PrimarySequenceInitializer("<strong>PrimarySequenceInitializer</strong><br>SequenceHandler")
+ PrimarySequenceInitializer --> PrimarySequenceInitializerArgs
+
+ subgraph PrimarySequenceInitializerArgs[" "]
+ direction LR
+ CleanupInitializer("<strong>CleanupInitializer</strong><br>SequenceHandler")
+ PrimaryParallelInitializer("<strong>PrimaryParallelInitializer</strong><br>ParallelHandler")
+ WorkerManager("<strong>WorkerManager</strong><br>WorkerManager")
+ end
+
+ CleanupInitializer --> PrimaryParallelInitializer
+ PrimaryParallelInitializer --> WorkerManager
+The above is a simplification of all the initializers that are present in the PrimaryInitializer
+as there are several smaller initializers that also trigger but are less relevant here.
+The CleanupInitializer
is an initializer that cleans up anything
+that might have remained from a previous server start
+and could impact behaviour.
+Relevant components in other parts of the configuration are responsible for adding themselves to this array if needed.
+An example of this is file-based locking components which might need to remove any dangling locking files.
The PrimaryParallelInitializer
can be used to add any initializers to that have to happen in the primary process.
+This makes it easier for users to add initializers by being able to append to its handlers.
The WorkerManager
is responsible for setting up the worker threads, if any.
flowchart TD
+ WorkerInitializer("<strong>WorkerInitializer</strong><br>ProcessHandler")
+ WorkerInitializer --> WorkerSequenceInitializer("<strong>WorkerSequenceInitializer</strong><br>SequenceHandler")
+ WorkerSequenceInitializer --> WorkerSequenceInitializerArgs
+
+ subgraph WorkerSequenceInitializerArgs[" "]
+ direction LR
+ WorkerParallelInitializer("<strong>WorkerParallelInitializer</strong><br>ParallelHandler")
+ ServerInitializer("<strong>ServerInitializer</strong><br>ServerInitializer")
+ end
+
+ WorkerParallelInitializer --> ServerInitializer
+The WorkerInitializer
is quite similar to the PrimaryInitializer
but triggers once per worker thread.
+Like the PrimaryParallelInitializer
, the WorkerParallelInitializer
can be used
+to add any custom initializers that need to run.
+The ServerInitializer
is the initializer that finally starts up the server by listening to the relevant port,
+once all the initialization described above is finished.
+This is an example of a component that differs based on some of the choices made during configuration.
+
flowchart TD
+ ServerInitializer("<strong>ServerInitializer</strong><br>ServerInitializer")
+ ServerInitializer --> WebSocketServerFactory("<strong>ServerFactory</strong><br>WebSocketServerFactory")
+ WebSocketServerFactory --> BaseHttpServerFactory("<br>BaseHttpServerFactory")
+ BaseHttpServerFactory --> HttpHandler("<strong>HttpHandler</strong><br><i>HttpHandler</i>")
+
+ ServerInitializer2("<strong>ServerInitializer</strong><br>ServerInitializer")
+ ServerInitializer2 ---> BaseHttpServerFactory2("<strong>ServerFactory</strong><br>BaseHttpServerFactory")
+ BaseHttpServerFactory2 --> HttpHandler2("<strong>HttpHandler</strong><br><i>HttpHandler</i>")
+Depending on if the configurations necessary for websockets are imported or not,
+the urn:solid-server:default:ServerFactory
identifier will point to a different resource.
+There will always be a BaseHttpServerFactory
that starts the HTTP(S) server,
+but there might also be a WebSocketServerFactory
wrapped around it to handle websocket support.
+Although not indicated here, the parameters for initializing the BaseHttpServerFactory
+might also differ in case an HTTPS configuration is imported.
The HttpHandler
it takes as input is responsible for how HTTP requests get resolved.
flowchart TD
+ AuthorizingHttpHandler("<br>AuthorizingHttpHandler")
+ AuthorizingHttpHandler --> AuthorizingHttpHandlerArgs
+
+ subgraph AuthorizingHttpHandlerArgs[" "]
+ CredentialsExtractor("<strong>CredentialsExtractor</strong><br><i>CredentialsExtractor</i>")
+ ModesExtractor("<strong>ModesExtractor</strong><br><i>ModesExtractor</i>")
+ PermissionReader("<strong>PermissionReader</strong><br><i>PermissionReader</i>")
+ Authorizer("<strong>Authorizer</strong><br>PermissionBasedAuthorizer")
+ OperationHttpHandler("<br><i>OperationHttpHandler</i>")
+ end
+Authorization is usually handled by the AuthorizingHttpHandler
,
+which receives a parsed HTTP request in the form of an Operation
.
+It goes through the following steps:
CredentialsExtractor
identifies the credentials of the agent making the call.ModesExtractor
finds which access modes are needed for which resources.PermissionReader
determines the permissions the agent has on the targeted resources.Authorizer
.OperationHttpHandler
, otherwise throw an error.There are multiple CredentialsExtractor
s that each determine identity in a different way.
+Potentially multiple extractors can apply,
+making a requesting agent have multiple credentials.
The diagram below shows the default configuration if authentication is enabled.
+flowchart TD
+ CredentialsExtractor("<strong>CredentialsExtractor</strong><br>UnionCredentialsExtractor")
+ CredentialsExtractor --> CredentialsExtractorArgs
+
+ subgraph CredentialsExtractorArgs[" "]
+ WaterfallHandler("<br>WaterfallHandler")
+ PublicCredentialsExtractor("<br>PublicCredentialsExtractor")
+ end
+
+ WaterfallHandler --> WaterfallHandlerArgs
+ subgraph WaterfallHandlerArgs[" "]
+ direction LR
+ DPoPWebIdExtractor("<br>DPoPWebIdExtractor") --> BearerWebIdExtractor("<br>BearerWebIdExtractor")
+ end
+Both of the WebID extractors make use of
+the (access-token-verifier
)[https://github.com/CommunitySolidServer/access-token-verifier] library
+to parse incoming tokens based on the Solid-OIDC specification.
+Besides those there are always the public credentials, which everyone has.
+All these credentials then get combined into a single union object.
If successful, a CredentialsExtractor
will return a key/value map
+linking the type of credentials to their specific values.
There are also debug configuration options available that can be used to simulate credentials.
+These can be enabled as different options through the config/ldp/authentication
imports.
Access modes are a predefined list of read
, write
, append
, create
and delete
.
+The ModesExtractor
determine which modes will be necessary and for which resources,
+based on the request contents.
flowchart TD
+ ModesExtractor("<strong>ModesExtractor</strong><br>IntermediateCreateExtractor")
+ ModesExtractor --> HttpModesExtractor("<strong>HttpModesExtractor</strong><br>WaterfallHandler")
+
+ HttpModesExtractor --> HttpModesExtractorArgs
+
+ subgraph HttpModesExtractorArgs[" "]
+ direction LR
+ PatchModesExtractor("<strong>PatchModesExtractor</strong><br><i>ModesExtractor</i>") --> MethodModesExtractor("<br>MethodModesExtractor")
+ end
+The IntermediateCreateExtractor
is responsible if requests try to create intermediate containers with a single request.
+E.g., a PUT request to /foo/bar/baz
should create both the /foo/
and /foo/bar/
containers in case they do not exist yet.
+This extractor makes sure that create
permissions are also checked on those containers.
Modes can usually be determined based on just the HTTP methods,
+which is what the MethodModesExtractor
does.
+A GET request will always need the read
mode for example.
The only exception are PATCH requests, +where the necessary modes depend on the body and the PATCH type.
+flowchart TD
+ PatchModesExtractor("<strong>PatchModesExtractor</strong><br>WaterfallHandler") --> PatchModesExtractorArgs
+ subgraph PatchModesExtractorArgs[" "]
+ N3PatchModesExtractor("<br>N3PatchModesExtractor")
+ SparqlUpdateModesExtractor("<br>SparqlUpdateModesExtractor")
+ end
+The server supports both N3 Patch and SPARQL Update PATCH requests. +In both cases it will parse the bodies to determine what the impact would be of the request and what modes it requires.
+PermissionReaders
take the input of the above to determine which permissions are available for which credentials.
+The modes from the previous step are not yet needed,
+but can be used as optimization as we only need to know if we have permission on those modes.
+Each reader returns all the information it can find based on the resources and modes it receives.
+In the default configuration the following readers are combined when WebACL is enabled as authorization method.
+In case authorization is disabled by changing the authorization import to config/ldp/authorization/allow-all.json
,
+this diagram is just a class that always returns all permissions.
flowchart TD
+ PermissionReader("<strong>PermissionReader</strong><br>AuxiliaryReader")
+ PermissionReader --> UnionPermissionReader("<br>UnionPermissionReader")
+ UnionPermissionReader --> UnionPermissionReaderArgs
+
+ subgraph UnionPermissionReaderArgs[" "]
+ PathBasedReader("<strong>PathBasedReader</strong><br>PathBasedReader")
+ OwnerPermissionReader("<strong>OwnerPermissionReader</strong><br>OwnerPermissionReader")
+ WrappedWebAclReader("<strong>WrappedWebAclReader</strong><br>ParentContainerReader")
+ end
+
+ WrappedWebAclReader --> WebAclAuxiliaryReader("<strong>WebAclAuxiliaryReader</strong><br>WebAclAuxiliaryReader")
+ WebAclAuxiliaryReader --> WebAclReader("<strong>WebAclReader</strong><br>WebAclReader")
+The first thing that happens is that if the target is an auxiliary resource that uses the authorization of its subject resource,
+the AuxiliaryReader
inserts that identifier instead.
+An example of this is if the requests targets the metadata of a resource.
The UnionPermissionReader
then combines the results of its readers into a single permission object.
+If one reader rejects a specific mode and another allows it, the rejection takes priority.
The PathBasedReader
rejects all permissions for certain paths.
+This is used to prevent access to the internal data of the server.
The OwnerPermissionReader
makes sure owners always have control access
+to the pods they created on the server.
+Users will always be able to modify the ACL resources in their pod,
+even if they accidentally removed their own access.
The final readers are specifically relevant for the WebACL algorithm.
+The ParentContainerReader
checks the permissions on a parent resource if required:
+creating a resource requires append
permissions on the parent container,
+while deleting a resource requires write
permissions there.
In case the target is an ACL resource, control
permissions need to be checked,
+no matter what mode was generated by the ModesExtractor
.
+The WebAclAuxiliaryReader
makes sure this conversion happens.
Finally, the WebAclReader
implements
+the efffective ACL resource algorithm
+and returns the permissions it finds in that resource.
+In case no ACL resource is found this indicates a configuration error and no permissions will be granted.
All the results of the previous steps then get combined in the PermissionBasedAuthorizer
to either allow or reject a request.
+If no permissions are found for a requested mode,
+or they are explicitly forbidden,
+a 401/403 will be returned,
+depending on if the agent was logged in or not.
The LdpHandler
, named as a reference to the Linked Data Platform specification,
+chains several handlers together, each with their own specific purpose, to fully resolve the HTTP request.
+It specifically handles Solid requests as described
+in the protocol specification,
+e.g. a POST request to create a new resource.
Below is a simplified view of how these handlers are linked.
+flowchart LR
+ LdpHandler("<strong>LdpHandler</strong><br>ParsingHttphandler")
+ LdpHandler --> AuthorizingHttpHandler("<br>AuthorizingHttpHandler")
+ AuthorizingHttpHandler --> OperationHandler("<strong>OperationHandler</strong><br><i>OperationHandler</i>")
+ OperationHandler --> ResourceStore("<strong>ResourceStore</strong><br><i>ResourceStore</i>")
+A standard request would go through the following steps:
+ParsingHttphandler
parses the HTTP request into a manageable format, both body and metadata such as headers.AuthorizingHttpHandler
verifies if the request is authorized to access the targeted resource.OperationHandler
determines which action is required based on the HTTP method.ResourceStore
does all the relevant data work.ParsingHttphandler
eventually receives the response data, or an error, and handles the output.Below are sections that go deeper into the specific steps.
+ + + +flowchart TD
+ ParsingHttphandler("<br>ParsingHttphandler")
+ ParsingHttphandler --> ParsingHttphandlerArgs
+
+ subgraph ParsingHttphandlerArgs[" "]
+ RequestParser("<strong>RequestParser</strong><br>BasicRequestParser")
+ AuthorizingHttpHandler("<strong></strong><br>AuthorizingHttpHandler")
+ ErrorHandler("<strong>ErrorHandler</strong><br><i>ErrorHandler</i>")
+ ResponseWriter("<strong>ResponseWriter</strong><br>BasicResponseWriter")
+ end
+A ParsingHttpHandler
handles both the parsing of the input data, and the serializing of the output data.
+It follows these 3 steps:
RequestParser
to convert the incoming data into an Operation
.Operation
to the AuthorizingHttpHandler
to receive either a Representation
if the operation was a success,
+ or an Error
in case something went wrong.ErrorHandler
will convert the Error
into a ResponseDescription
.ResponseWriter
to output the ResponseDescription
as an HTTP response.flowchart TD
+ RequestParser("<strong>RequestParser</strong><br>BasicRequestParser") --> RequestParserArgs
+ subgraph RequestParserArgs[" "]
+ TargetExtractor("<strong>TargetExtractor</strong><br>OriginalUrlExtractor")
+ PreferenceParser("<strong>PreferenceParser</strong><br>AcceptPreferenceParser")
+ MetadataParser("<strong>MetadataParser</strong><br><i>MetadataParser</i>")
+ BodyParser("<br><i>Bodyparser</i>")
+ Conditions("<br>BasicConditionsParser")
+ end
+
+ OriginalUrlExtractor --> IdentifierStrategy("<strong>IdentifierStrategy</strong><br><i>IdentifierStrategy</i>")
+The BasicRequestParser
is mostly an aggregator of multiple smaller parsers that each handle a very specific part.
+This is a single class, the OriginalUrlExtractor
, but fulfills the very important role
+of making sure input URLs are handled consistently.
The query parameters will always be completely removed from the URL.
+There is also an algorithm to make sure all URLs have a "canonical" version as for example both &
and %26
+can be interpreted in the same way.
+Specifically all special characters will be encoded into their percent encoding.
The IdentifierStrategy
it gets as input is used to determine if the resulting URL is within the scope of the server.
+This can differ depending on if the server uses subdomains or not.
The resulting identifier will be stored in the target
field of an Operation
object.
The AcceptPreferenceParser
parses the Accept
header and all the relevant Accept-*
headers.
+These will all be put into the preferences
field of an Operation
object.
+These will later be used to handle the content negotiation.
For example, when sending an Accept: text/turtle; q=0.9
header,
+this wil result in the preferences object { type: { 'text/turtle': 0.9 } }
.
Several other headers can have relevant metadata,
+such as the Content-Type
header,
+or the Link: <http://www.w3.org/ns/ldp#Container>; rel="type"
header
+which is used to indicate to the server that a request intends to create a container.
Such headers are converted to RDF triples and stored in the RepresentationMetadata
object,
+which will be part of the body
field in the Operation
.
The default MetadataParser
is a ParallelHandler
that contains several smaller parsers,
+each looking at a specific header.
In case of most requests, the input data stream is used directly in the body
field of the Operation
,
+with a few minor checks to make sure the HTTP specification is being followed.
In the case of PATCH requests though, +there are several specific body parsers that will convert the request +into a JavaScript object containing all the necessary information to execute such a PATCH. +Several validation checks will already take place there as well.
+The BasicConditionsParser
parses everything related to conditions headers,
+such as if-none-match
or if-modified-since
,
+and stores the relevant information in the conditions
field of the Operation
.
+These will later be used to make sure the request should be aborted or not.
In case a request is successful, the AuthorizingHttpHandler
will return a ResponseDescription
,
+and if not it will throw an error.
In case an error gets thrown, this will be caught by the ErrorHandler
and converted into a ResponseDescription
.
+The request preferences will be used to make sure the serialization is one that is preferred.
Either way we will have a ResponseDescription
,
+which will be sent to the BasicResponseWriter
to convert into output headers, data and a status code.
To convert the metadata into headers, it uses a MetadataWriter
,
+which functions as the reverse of the MetadataParser
mentioned above:
+it has multiple writers which each convert certain metadata into a specific header.
The interface of a ResourceStore
is mostly a 1-to-1 mapping of the HTTP methods:
getRepresentation
setRepresentation
addResource
deleteResource
modifyResource
The corresponding OperationHandler
of the relevant method
+is responsible for calling the correct ResourceStore
function.
In practice, the community server has multiple resource stores chained together, +each handling a specific part of the request and then calling the next store in the chain. +The default configurations come with the following stores:
+MonitoringStore
IndexRepresentationStore
LockingResourceStore
PatchingStore
RepresentationConvertingStore
DataAccessorBasedStore
This chain can be seen in the configuration part in config/storage/middleware/default.json
+and all the entries in config/storage/backend
.
This store emits the events that are necessary to emit notifications when resources change.
+There are 4 different events that can be emitted:
+- this.emit('changed', identifier, activity)
: is emitted for every resource that was changed/effected by a call to the store.
+ With activity being undefined or one of the available ActivityStream terms.
+- this.emit(AS.Create, identifier)
: is emitted for every resource that was created by the call to the store.
+- this.emit(AS.Update, identifier)
: is emitted for every resource that was updated by the call to the store.
+- this.emit(AS.Delete, identifier)
: is emitted for every resource that was deleted by the call to the store.
A changed
event will always be emitted if a resource was changed.
+If the correct metadata was set by the source ResourceStore
, an additional field will be sent along indicating the type of change,
+and an additional corresponding event will be emitted, depending on what the change is.
When doing a GET request on a container /container/
,
+this container returns the contents of /container/index.html
instead if HTML is the preferred response type.
+All these values are the defaults and can be configured for other resources and media types.
To prevent data corruption, the server locks resources when being targeted by a request. +Locks are only released when an operation is completely finished, +in the case of read operations this means the entire data stream is read, +and in the case of write operations this happens when all relevant data is written. +The default lock that is used is a readers-writer lock. +This allows simultaneous read requests on the same resource, +but only while no write request is in progress.
+PATCH operations in Solid apply certain transformations on the target resource,
+which makes them more complicated than only reading or writing data since it involves both.
+The PatchingStore
provides a generic solution for backends that do not implement the modifyResource
function
+so new backends can be added more easily.
+In case the next store in the chain does not support PATCH,
+the PatchingStore
will GET the data from the next store,
+apply the transformation on that data,
+and then PUT it back to the store.
This store handles everything related to content negotiation. +In case the resulting data of a GET request does not match the preferences of a request, +it will be converted here. +Similarly, if incoming data does not match the type expected by the store, +the SPARQL backend only accepts triples for example, +that is also handled here
+Large parts of the requirements of the Solid protocol specification are resolved by the DataAccessorBasedStore
:
+POST only working on containers,
+DELETE not working on non-empty containers,
+generating ldp:contains
triples for containers, etc.
+Most of this behaviour is independent of how the data is stored which is why it can be generalized here.
+The store's name comes from the fact that it makes use of DataAccessor
s to handle the read/write of resources.
+A DataAccessor
is a simple interface that only focuses on handling the data.
+It does not concern itself with any of the necessary Solid checks as it assumes those have already been made.
+This means that if a storage method needs to be supported,
+only a new DataAccessor
needs to be made,
+after which it can be plugged into the rest of the server.
The initial architecture document the project was started from can be found +here. +Many things have been added since the original inception of the project, +but the core ideas within that document are still valid.
+As can be seen from the architecture, an important idea is the modularity of all components. +No actual implementations are defined there, only their interfaces. +Making all the components independent of each other in such a way provides us with an enormous flexibility: +they can all be replaced by a different implementation, without impacting anything else. +This is how we can provide many different configurations for the server, +and why it is impossible to provide ready solutions for all possible combinations.
+Having a modular architecture makes it more difficult to give a complete architecture overview. +We will limit ourselves to the more commonly used default configurations we provide, +and in certain cases we might give examples of what differences there are +based on what configurations are being imported.
+To do this we will make use of architecture diagrams. +We will use an example below to explain the formatting used throughout the architecture documentation:
+flowchart TD
+ LdpHandler("<strong>LdpHandler</strong><br>ParsingHttphandler")
+ LdpHandler --> LdpHandlerArgs
+
+ subgraph LdpHandlerArgs[" "]
+ RequestParser("<strong>RequestParser</strong><br>BasicRequestParser")
+ Auth("<br>AuthorizingHttpHandler")
+ ErrorHandler("<strong>ErrorHandler</strong><br><i>ErrorHandler</>")
+ ResponseWriter("<strong>ResponseWriter</strong><br>BasicResponseWriter")
+ end
+Below is a summary of how to interpret such diagrams:
+urn:solid-server:default:
to the shorthand identifier.For example, in the above, LdpHandler is a shorthand for the actual identifier urn:solid-server:default:LdpHandler
+and is an instance of ParsingHttpHandler
. It has 4 parameters,
+one of which has no identifier but is an instance of AuthorizingHttpHandler
.
Below are the sections that go deeper into the features of the server and how those work.
+ + + +\n {translation(\"search.result.term.missing\")}: {...missing}\n
\n }\n