;; 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)))))