From 41b0d9d92ab3b879b65d8687e474c6458ad10327 Mon Sep 17 00:00:00 2001 From: Vivien Kraus Date: Thu, 3 Jun 2021 18:16:10 +0200 Subject: Add an API to update server resource paths --- src/scm/webid-oidc/errors.scm | 105 ++++++++++--- src/scm/webid-oidc/server/resource/Makefile.am | 6 +- src/scm/webid-oidc/server/resource/path.scm | 197 +++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 24 deletions(-) create mode 100644 src/scm/webid-oidc/server/resource/path.scm (limited to 'src/scm') diff --git a/src/scm/webid-oidc/errors.scm b/src/scm/webid-oidc/errors.scm index 52f5db8..60e45f7 100644 --- a/src/scm/webid-oidc/errors.scm +++ b/src/scm/webid-oidc/errors.scm @@ -870,6 +870,56 @@ (raise-exception ((record-constructor &no-provider-candidates) webid causes))) +;; Server-side exceptions + +(define-exception-type + &path-not-found + &external-error + make-path-not-found + path-not-found? + (path path-not-found-path)) + +(export &path-not-found + make-path-not-found + path-not-found? + path-not-found-path) + +(define-exception-type + &uri-slash-semantics-error + &external-error + make-uri-slash-semantics-error + uri-slash-semantics-error? + (path uri-slash-semantics-error-path) + (expected-path uri-slash-semantics-error-expected-path)) + +(export &uri-slash-semantics-error + make-uri-slash-semantics-error + uri-slash-semantics-error? + uri-slash-semantics-error-path + uri-slash-semantics-error-expected-path) + +(define-exception-type + &cannot-delete-root + &external-error + make-cannot-delete-root + cannot-delete-root?) + +(export &cannot-delete-root + make-cannot-delete-root + cannot-delete-root?) + +(define-exception-type + &container-not-empty + &external-error + make-container-not-empty + container-not-empty? + (path container-not-empty-path)) + +(export &container-not-empty + make-container-not-empty + container-not-empty? + container-not-empty-path) + (define*-public (error->str err #:key (max-depth #f)) (if (record? err) (let* ((type (record-type-descriptor err)) @@ -1193,28 +1243,39 @@ ((&unconfirmed-provider) (format #f (G_ "~s does not admit ~s as an identity provider") (get 'subject) (get 'provider))) - ((&neither-identity-provider-nor-webid) - (format #f (G_ "~a is neither an identity provider (because ~a) nor a webid (because ~a)") - (uri->string (get 'uri)) - (recurse (get 'why-not-identity-provider)) - (recurse (get 'why-not-webid)))) - ((&token-request-failed) - (format #f (G_ "the token request failed (because ~a)") - (recurse (get 'cause)))) - ((&profile-not-found) - (format #f (G_ "you don’t have a refresh token for identity ~a certified by ~a in ~s") - (uri->string (get 'webid)) - (uri->string (get 'iss)) - (get 'dir))) - ((&no-provider-candidates) - (format #f (G_ "all identity provider candidates for ~a failed: ~a") - (uri->string (get 'webid)) - (string-join - (map (lambda (cause) - (format #f (G_ "~s failed (because ~a)") - (uri->string (car cause)) (recurse (cdr cause)))) - (get 'causes)) - (G_ ", ")))) + ((&neither-identity-provider-nor-webid) + (format #f (G_ "~a is neither an identity provider (because ~a) nor a webid (because ~a)") + (uri->string (get 'uri)) + (recurse (get 'why-not-identity-provider)) + (recurse (get 'why-not-webid)))) + ((&token-request-failed) + (format #f (G_ "the token request failed (because ~a)") + (recurse (get 'cause)))) + ((&profile-not-found) + (format #f (G_ "you don’t have a refresh token for identity ~a certified by ~a in ~s") + (uri->string (get 'webid)) + (uri->string (get 'iss)) + (get 'dir))) + ((&no-provider-candidates) + (format #f (G_ "all identity provider candidates for ~a failed: ~a") + (uri->string (get 'webid)) + (string-join + (map (lambda (cause) + (format #f (G_ "~s failed (because ~a)") + (uri->string (car cause)) (recurse (cdr cause)))) + (get 'causes)) + (G_ ", ")))) + ((&path-not-found) + (format #f (G_ "no resource has been found to serve URI path ~s") + (get 'path))) + ((&uri-slash-semantics-error) + (format #f (G_ "no resource has been found to serve URI path ~s, but ~s exists") + (get 'path) (get 'expected-path))) + ((&cannot-delete-root) + (format #f (G_ "the root storage cannot be deleted"))) + ((&container-not-empty) + (format #f (G_ "the container ~s should be emptied before being deleted") + (get 'path))) ((&compound-exception) (let ((components (get 'components))) (if (null? components) diff --git a/src/scm/webid-oidc/server/resource/Makefile.am b/src/scm/webid-oidc/server/resource/Makefile.am index 88103cc..49cc912 100644 --- a/src/scm/webid-oidc/server/resource/Makefile.am +++ b/src/scm/webid-oidc/server/resource/Makefile.am @@ -1,5 +1,7 @@ dist_resourceserverwebidoidcmod_DATA += \ - %reldir%/content.scm + %reldir%/content.scm \ + %reldir%/path.scm resourceserverwebidoidcgo_DATA += \ - %reldir%/content.go + %reldir%/content.go \ + %reldir%/path.go diff --git a/src/scm/webid-oidc/server/resource/path.scm b/src/scm/webid-oidc/server/resource/path.scm new file mode 100644 index 0000000..59ec9e0 --- /dev/null +++ b/src/scm/webid-oidc/server/resource/path.scm @@ -0,0 +1,197 @@ +(define-module (webid-oidc server resource path) + #:use-module (webid-oidc errors) + #:use-module ((webid-oidc stubs) #:prefix stubs:) + #:use-module (webid-oidc rdf-index) + #:use-module ((webid-oidc refresh-token) #:prefix refresh:) + #:use-module (web uri) + #:use-module (rnrs bytevectors) + #:use-module (ice-9 exceptions) + #:use-module (ice-9 receive) + #:use-module (ice-9 optargs) + #:use-module (ice-9 iconv) + #:use-module (ice-9 textual-ports) + #:use-module (ice-9 binary-ports) + #:use-module (ice-9 threads) + #:use-module (rnrs bytevectors) + #:use-module (oop goops) + #:export + ( + + read-path + update-path + + )) + +(define (default-dir) + (string-append (refresh:default-dir) "/server")) + +(define (hash-path/lock path) + (let ((h (stubs:hash 'SHA-256 path)) + (dir (default-dir))) + (let ((first-char (substring h 0 1)) + (rest (substring h 1))) + (values + (format #f "~a/path/~a/~a" dir first-char rest) + (format #f "~a/path/~a/.lock" dir first-char))))) + +(define (hash-path path) + (receive (h lock) (hash-path/lock path) + h)) + +(define (lock-file-name path) + (receive (h lock) (hash-path/lock path) + lock)) + +(define (read-path path) + (let ((h (hash-path path))) + (with-exception-handler + (lambda (error) + (let ((with-slash (string-append path "/")) + (without-slash + (if (string-suffix? "/" path) + (substring path 0 (- (string-length path) (string-length "/"))) + path))) + (let ((with-slash-exists (file-exists? (hash-path with-slash))) + (without-slash-exists (file-exists? (hash-path without-slash)))) + (cond + (with-slash-exists + (raise-exception + (make-exception + (make-path-not-found path) + (make-uri-slash-semantics-error path with-slash)))) + (without-slash-exists + (raise-exception + (make-exception + (make-path-not-found path) + (make-uri-slash-semantics-error path with-slash)))) + (else + (raise-exception (make-path-not-found path))))))) + (lambda () + (call-with-input-file h + (lambda (port) + (let ((main-etag (read port))) + (let ((auxiliary (read port))) + (values main-etag + (map (lambda (cell) + (let ((key (string->uri (car cell))) + (value (cdr cell))) + (cons key value))) + auxiliary)))))))))) + +(define* (update-path path f content-type contained static-content create delete + #:key (create-intermediate-containers? #f)) + (let ((h (hash-path path)) + (lock (lock-file-name path)) + (garbage (make-hash-table)) + (has-been-created? #f) + (has-been-deleted? #f) + (parent-path + (let ((components (split-and-decode-uri-path path))) + (cond + ((null? components) + #f) + ((null? (cdr components)) + "/") + (else + (string-append + "/" + (encode-and-join-uri-path + (reverse + (cdr + (reverse components)))) + "/")))))) + (stubs:atomically-update-file + h + lock + (lambda (port) + (receive (etag auxiliary) + (with-exception-handler + (lambda (error) + (unless (path-not-found? error) + (raise-exception error)) + (set! has-been-created? #t) + (values #f #f)) + (lambda () + (read-path path)) + #:unwind? #t + #:unwind-for-type &path-not-found) + (when etag + (hash-set! garbage etag #t)) + (when auxiliary + (for-each + (lambda (cell) + (when (cdr cell) + (hash-set! garbage (cdr cell) #t))) + auxiliary)) + (call-with-values + (lambda () + (f etag auxiliary)) + (case-lambda + ((false) + (when false + (error "You’re using the API wrong.")) + ;; Delete the resource + (unless (null? (contained etag)) + (raise-exception (make-container-not-empty path))) + (when (equal? path "/") + (raise-exception (make-cannot-delete-root))) + (set! has-been-deleted? #t) + #f) + ((new-etag new-auxiliary) + (unless (and (string? new-etag) (list? new-auxiliary)) + (error "You’re using the API wrong.")) + (hash-remove! garbage new-etag) + (when new-auxiliary + (for-each + (lambda (cell) + (hash-remove! garbage (cdr cell))) + new-auxiliary)) + (write new-etag port) + (write (map (lambda (cell) + (cons (uri->string (car cell)) + (cdr cell))) + new-auxiliary) + port) + #t)))))) + (when (and parent-path has-been-created? (not has-been-deleted?)) + (update-path + parent-path + (lambda (etag auxiliary) + ;; Add path as a child of the resource at etag + (unless create-intermediate-containers? + (unless etag + ;; Typically, POST to a non-existing path + (raise-exception (make-path-not-found parent-path)))) + (unless auxiliary + (set! auxiliary '())) + (let ((content-type (if etag (content-type etag) 'text/turtle)) + (other-children (if etag (contained etag) '())) + (static-content (if etag (static-content etag) (string->utf8 "")))) + (unless (eq? content-type 'text/turtle) + (raise-exception (make-not-a-container parent-path content-type))) + (let ((new-etag + (create content-type (cons path other-children) static-content))) + (values new-etag auxiliary)))) + content-type contained static-content create delete + #:create-intermediate-containers? create-intermediate-containers?)) + (when (and parent-path has-been-deleted? (not has-been-created?)) + (update-path + parent-path + (lambda (etag auxiliary) + (unless etag + (raise-exception (make-path-not-found parent-path))) + (let ((content-type (content-type etag)) + (all-children (contained etag)) + (static-content (static-content etag))) + (values + (create content-type + (filter (lambda (x) + (not (equal? x path))) + all-children) + static-content) + auxiliary))) + content-type contained static-content create delete + #:create-intermediate-containers? create-intermediate-containers?)) + (for-each + delete + (hash-map->list (lambda (garbage false) garbage) garbage)))) -- cgit v1.2.3