;; disfluid, 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 . (define-module (webid-oidc client) #:use-module (webid-oidc errors) #:use-module (webid-oidc provider-confirmation) #:use-module (webid-oidc oidc-id-token) #:use-module (webid-oidc dpop-proof) #:use-module (webid-oidc jwk) #:use-module (webid-oidc client-manifest) #:use-module (webid-oidc web-i18n) #:use-module (webid-oidc server endpoint) #:use-module (webid-oidc server endpoint client) #:use-module ((webid-oidc parameters) #:prefix p:) #:use-module ((webid-oidc stubs) #:prefix stubs:) #:use-module ((webid-oidc config) #:prefix cfg:) #:use-module ((webid-oidc cache) #:prefix cache:) #:use-module ((webid-oidc client accounts) #:prefix account:) #:use-module ((webid-oidc client client) #:prefix client:) #:use-module (web uri) #:use-module (web request) #:use-module (web response) #:use-module (web server) #:use-module (web http) #:use-module (ice-9 optargs) #:use-module (ice-9 receive) #:use-module (srfi srfi-9) #:use-module (srfi srfi-19) #:use-module (srfi srfi-26) #:use-module (rnrs bytevectors) #:use-module (ice-9 i18n) #:use-module (ice-9 getopt-long) #:use-module (ice-9 suspendable-ports) #:use-module (ice-9 match) #:use-module (sxml simple) #:use-module (oop goops) #:duplicates (merge-generics) #:re-export ( (client: . ) (client:client-id . client-id) (client:key-pair . key-pair) (client:redirect-uri . redirect-uri) (client:client . client) (account:authorization-process . authorization-process) (account:authorization-state . authorization-state) ) #:export ( request ) #:declarative? #t) ;; For syntax highlighting (define account:) (define client:) (define (setup-headers!) ;; HACK: guile does not support other authentication schemes in ;; WWW-Authenticate than Basic, so it will crash when a response ;; containing that header will be issued. (declare-header! "WWW-Authenticate" (cute parse-header 'pragma <>) (lambda (value) (and (list? value) (let check-value ((schemes value)) (match schemes (() #t) (((? symbol?) tl ...) (check-value tl)) (((hd . args) tl ...) (and (symbol? hd) (let check-args ((args args)) (match args (() #t) (((key . value) tl ...) (and (symbol? key) (string? value) (check-args tl))))) (check-value tl))))))) (cute write-header 'pragma <> <>)) ;; The same applies for the authorization header. (let ((original-parser (header-parser 'authorization)) (original-writer (header-writer 'authorization))) (declare-header! "Authorization" original-parser (lambda (value) #t) (match-lambda* ((('dpop . dpop) port) (format port "DPoP ~a" dpop)) ((value port) (original-writer value port)))))) (define* (initial-login client issuer) (setup-headers!) (parameterize ((client:client client)) (make #:issuer issuer))) (define (request account uri . other-args) (setup-headers!) (unless (account:access-token account) (set! account (account:refresh account))) (define (do-with-headers method headers non-header-args can-fail?) (let* ((access-token (account:access-token account)) (dpop-proof (let ((key-pair (account:key-pair account))) (issue key-pair #:jwk (public-key key-pair) #:htm method #:htu uri #:access-token access-token)))) (let ((all-headers `((dpop . ,dpop-proof) (authorization . (dpop . ,access-token)) (accept-language . ((2 . ,(G_ "accept-language-header|en-us")) (1 . "en-us"))) ,@headers))) (receive (response body) (apply (p:anonymous-http-request) uri #:method method #:headers all-headers non-header-args) (let ((code (response-code response))) (if (and (eqv? code 401) can-fail?) ;; Code expired (begin (set! account (account:refresh (account:invalidate-access-token account))) ;; retry (do-with-headers method headers non-header-args #f)) (values account response body))))))) (let scan-arguments ((args other-args) (headers #f) (non-header-args '()) (method #f)) (match args (() (cond ((not headers) (scan-arguments args '() non-header-args method)) ((not method) (scan-arguments args headers non-header-args 'GET)) (else (do-with-headers method headers (reverse non-header-args) #t)))) ((#:method new-method args ...) (scan-arguments args headers non-header-args (or method new-method))) ((#:headers (new-headers ...) args ...) (scan-arguments args (or headers new-headers) non-header-args method)) ((kw value args ...) (scan-arguments args headers `(,value ,kw ,@non-header-args) method)))))