summaryrefslogtreecommitdiff
path: root/src/scm/webid-oidc/catalog.scm
diff options
context:
space:
mode:
authorVivien Kraus <vivien@planete-kraus.eu>2021-07-28 16:58:13 +0200
committerVivien Kraus <vivien@planete-kraus.eu>2021-07-29 18:43:54 +0200
commit80aff364dbd8d78392582124b51b46b556db7e4c (patch)
tree85f3331d1f614b1ac9d380c1aceb81a72bafbbdc /src/scm/webid-oidc/catalog.scm
parentdbd7407c825f9fc0ddbbeba6e7eef6f5430c8cf2 (diff)
The server supports XML catalogs.
This will be used to set up an integration test suite with HTTPS, without needing to provide certificates, because the test harness will have an XML catalog mapping HTTPS targets to local HTTP mockups.
Diffstat (limited to 'src/scm/webid-oidc/catalog.scm')
-rw-r--r--src/scm/webid-oidc/catalog.scm264
1 files changed, 264 insertions, 0 deletions
diff --git a/src/scm/webid-oidc/catalog.scm b/src/scm/webid-oidc/catalog.scm
new file mode 100644
index 0000000..11e0877
--- /dev/null
+++ b/src/scm/webid-oidc/catalog.scm
@@ -0,0 +1,264 @@
+;; webid-oidc, implementation of the Solid specification
+;; Copyright (C) 2020, 2021 Vivien Kraus
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+(define-module (webid-oidc catalog)
+ #:use-module (web uri)
+ #:use-module (web client)
+ #:use-module (ice-9 match)
+ #:use-module (web client)
+ #:use-module (rnrs bytevectors)
+ #:use-module (sxml simple)
+ #:use-module (sxml match)
+ #:use-module (ice-9 optargs)
+ #:use-module (ice-9 receive)
+ #:export (resolve-uri))
+
+(define useful-namespaces
+ '((ct . "urn:oasis:names:tc:entity:xmlns:xml:catalog")
+ (xml . "http://www.w3.org/XML/1998/namespace")))
+
+(define (run-catalog-entry do-examine-catalog base first-entry rest other-entries
+ uri
+ candidate match-length next-catalogs-rev)
+ ;; do-examine-catalog is the recursive function to examine a new catalog.
+ ;; base is the current base.
+ ;; rest is a catalog containing the other entries.
+
+ ;; This function is there because I can’t nest two sxml pattern
+ ;; matchings.
+ (sxml-match
+ first-entry
+ ((ct:group (@ (xml:base ,new-base)) ,group-entries ...)
+ (let ((new-base (string->uri* base new-base)))
+ (receive (candidate match-length next-catalogs-rev)
+ (do-examine-catalog
+ `(*TOP*
+ (ct:catalog (@ (xml:base ,(uri->string new-base)))
+ ,@group-entries))
+ candidate match-length next-catalogs-rev)
+ (do-examine-catalog rest candidate match-length next-catalogs-rev))))
+ ((ct:group ,group-entries ...)
+ ;; The group does not have a specific base, we can merge the
+ ;; entries with other-entries.
+ (do-examine-catalog
+ `(*TOP*
+ (ct:catalog (@ (xml:base ,(uri->string base)))
+ ,@group-entries
+ ,@other-entries))
+ candidate match-length next-catalogs-rev))
+ ((ct:uri (@ (name ,exact-pattern)
+ (uri ,replacement)
+ (xml:base ,new-base)))
+ ;; exact-pattern is not relative to xml:base
+ (if (equal? (string->uri exact-pattern) uri)
+ ;; Perfect replacement, stop here
+ (values
+ ;; but replacement is relative to xml:base
+ (string->uri* base new-base replacement)
+ (string-length (uri->string uri))
+ '())
+ (do-examine-catalog rest candidate match-length next-catalogs-rev)))
+ ((ct:rewriteUri (@ (name ,pattern)
+ (uri ,replacement)
+ (xml:base ,new-base)))
+ (when (and (string-prefix? pattern (uri->string uri))
+ (> (string-length pattern) match-length))
+ ;; The match is significant
+ (set! candidate
+ (string->uri* base
+ new-base
+ (string-append
+ replacement
+ (substring (uri->string uri)
+ (string-length pattern)))))
+ (set! match-length (string-length pattern)))
+ ;; Maybe the match was perfect, in which case we must stop
+ ;; there.
+ (if (eqv? match-length (string-length (uri->string uri)))
+ (values candidate match-length '())
+ (do-examine-catalog rest candidate match-length next-catalogs-rev)))
+ ((ct:uriSuffix (@ (uriSuffix ,suffix)
+ (uri ,replacement)
+ (base ,new-base)))
+ (when (and (string-suffix? suffix (uri->string uri))
+ (> (string-length suffix) match-length))
+ (let* ((length-kept
+ (- (string-length (uri->string uri))
+ (string-length suffix)))
+ (part-kept (substring (uri->string uri) 0 length-kept)))
+ (set! candidate
+ (string->uri* base new-base (string-append part-kept suffix)))
+ (set! match-length (string-length suffix))))
+ (if (eqv? match-length (string-length (uri->string uri)))
+ (values candidate match-length '())
+ (do-examine-catalog rest candidate match-length next-catalogs-rev)))
+ ((ct:delegateURI (@ (uriStartString ,pattern)
+ (catalog ,next-catalog)
+ (base ,new-base)))
+ (if (string-prefix? pattern (uri->string uri))
+ ;; Perfect match
+ (values #f 0 (list (string->uri* base new-base next-catalog)))
+ (do-examine-catalog rest candidate match-length next-catalogs-rev)))
+ ((ct:nextCatalog (@ (catalog ,next-catalog)
+ (base ,new-base)))
+ (do-examine-catalog rest candidate match-length
+ (cons (string->uri* base new-base next-catalog)
+ next-catalogs)))
+ (,else
+ (match first-entry
+ ((node-name attributes/children ...)
+ ;; The node is simply missing a base
+ (do-examine-catalog
+ `(*TOP*
+ (ct:catalog (@ (xml:base ,(uri->string base)))
+ (,node-name (@ (xml:base ,(uri->string base))) ,@attributes/children)
+ ,@other-entries))
+ candidate match-length next-catalogs-rev))
+ (else
+ (do-examine-catalog rest candidate match-length next-catalogs-rev))))))
+
+(define (make-absolute base uri)
+ (when (string? base)
+ (set! base (string->uri base)))
+ (let ((final-scheme (or (uri-scheme uri) (uri-scheme base)))
+ (uri-with-final-authority
+ (if (or (uri-scheme uri)
+ (uri-userinfo uri)
+ (uri-host uri)
+ (uri-port uri))
+ ;; we should use that of uri
+ uri
+ ;; nothing specified, use base
+ base))
+ (final-path
+ (if (or (uri-scheme uri)
+ (uri-userinfo uri)
+ (uri-host uri)
+ (uri-port uri)
+ (string-prefix? "/" (uri-path uri)))
+ (uri-path uri)
+ (let with-absolute-relative-path
+ ((defined '())
+ (components
+ (append (split-and-decode-uri-path (uri-path base))
+ (split-and-decode-uri-path (uri-path uri)))))
+ (match components
+ (() (string-append "/" (encode-and-join-uri-path (reverse defined))))
+ (("" components ...) (with-absolute-relative-path defined components))
+ (("." components ...) (with-absolute-relative-path defined components))
+ ((".." components ...)
+ (match defined
+ (() (error "Invalid relative URI"))
+ ((dropped kept ...)
+ (with-absolute-relative-path kept components))))
+ ((head components ...)
+ (with-absolute-relative-path `(,head ,@defined) components))))))
+ (uri-with-final-query
+ (if (or (uri-scheme uri)
+ (uri-userinfo uri)
+ (uri-host uri)
+ (uri-port uri)
+ (not (equal? (uri-path uri) ""))
+ (uri-query uri))
+ uri
+ base)))
+ (build-uri final-scheme
+ #:userinfo (uri-userinfo uri-with-final-authority)
+ #:host (uri-host uri-with-final-authority)
+ #:port (uri-port uri-with-final-authority)
+ #:path final-path
+ #:query (uri-query uri-with-final-query)
+ #:fragment (uri-fragment uri))))
+
+(define (string->uri** base str)
+ (make-absolute base (string->uri-reference str)))
+
+(define (string->uri* base str . rest)
+ (if (null? rest)
+ (string->uri** base str)
+ (apply string->uri* (string->uri** base str) rest)))
+
+(define (resolve-uri-in-catalog uri catalog-uri catalog other-catalog-uris)
+ (let do-examine-catalog ((catalog catalog)
+ (candidate uri)
+ (match-length 0)
+ (next-catalogs-rev (reverse other-catalog-uris)))
+ (sxml-match
+ catalog
+ ((*TOP*
+ (ct:catalog))
+ ;; End of the catalog
+ (values candidate
+ match-length
+ (if (eqv? match-length 0)
+ (reverse next-catalogs-rev)
+ '())))
+ ((*TOP*
+ (ct:catalog (@ (xml:base ,base))
+ ,first-entry
+ ,other-entries ...))
+ (let* ((base (string->uri* catalog-uri base))
+ (rest `(*TOP*
+ (ct:catalog (@ (xml:base ,(uri->string base)))
+ ,@other-entries))))
+ (run-catalog-entry do-examine-catalog base first-entry rest other-entries
+ uri
+ candidate match-length next-catalogs-rev)))
+ ((*TOP*
+ (ct:catalog ,entries ...))
+ ;; No explicit xml:base
+ (do-examine-catalog
+ `(*TOP*
+ (ct:catalog (@ (xml:base ,(uri->string catalog-uri)))
+ ,@entries))
+ candidate
+ match-length
+ next-catalogs-rev)))))
+
+(define* (get-catalog uri #:key (http-get http-get))
+ (case (uri-scheme uri)
+ ((http https)
+ (receive (response response-body) (http-get uri)
+ (when (bytevector? response-body)
+ (set! response-body (utf8->string response-body)))
+ (xml->sxml response-body #:namespaces useful-namespaces)))
+ ((file)
+ (call-with-input-file (uri-path uri)
+ (lambda (port)
+ (xml->sxml port #:namespaces useful-namespaces))))
+ (else
+ (error (format #f "Unsupported delegate catalog URI scheme: ~s\n" (uri-scheme uri))))))
+
+(define* (resolve-uri uri #:key (http-get http-get))
+ (when (string? uri)
+ (set! uri (string->uri uri)))
+ (let do-examine ((uris
+ (let ((var
+ (getenv "XML_CATALOG_FILES")))
+ (if var
+ (map string->uri
+ (string-split var #\space))
+ '()))))
+ (match uris
+ (() uri)
+ ((catalog-uri uris ...)
+ (let ((catalog (get-catalog catalog-uri
+ #:http-get http-get)))
+ (receive (candidate match-length next-uris)
+ (resolve-uri-in-catalog uri catalog-uri catalog uris)
+ (if (null? next-uris)
+ candidate
+ (do-examine next-uris))))))))