View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2015-2016, VU University Amsterdam
    7    All rights reserved.
    8
    9    Redistribution and use in source and binary forms, with or without
   10    modification, are permitted provided that the following conditions
   11    are met:
   12
   13    1. Redistributions of source code must retain the above copyright
   14       notice, this list of conditions and the following disclaimer.
   15
   16    2. Redistributions in binary form must reproduce the above copyright
   17       notice, this list of conditions and the following disclaimer in
   18       the documentation and/or other materials provided with the
   19       distribution.
   20
   21    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   22    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   23    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   24    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   25    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   26    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   27    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   28    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   29    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   30    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   31    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   32    POSSIBILITY OF SUCH DAMAGE.
   33*/
   34
   35:- module(http_digest,
   36          [ http_digest_challenge//2,      % +Realm, +Options
   37            http_digest_password_hash/4,   % +User, +Realm, +Passwd, -Hash
   38                                           % client support
   39            http_parse_digest_challenge/2, % +Challenge, -Fields
   40            http_digest_response/5         % +Fields, +User, +Password,
   41                                           % -Reply +Opts
   42          ]).   43:- use_module(library(http/http_authenticate)).   44:- use_module(library(http/http_stream)).   45:- use_module(library(dcg/basics)).   46:- use_module(library(md5)).   47:- use_module(library(error)).   48:- use_module(library(option)).   49:- use_module(library(debug)).   50:- use_module(library(settings)).   51:- use_module(library(base64)).   52:- use_module(library(broadcast)).   53:- use_module(library(uri)).   54:- use_module(library(apply)).   55
   56
   57/** <module> HTTP Digest authentication
   58
   59This library implements HTTP  _Digest   Authentication_  as per RFC2617.
   60Unlike  _Basic  Authentication_,  digest  authentication   is  based  on
   61challenge-reponse and therefore does not need  to send the password over
   62the (insecure) connection. In addition, it   provides  a count mechanism
   63that ensure that old  credentials  cannot   be  reused,  which  prevents
   64attackers  from  using  old  credentials  with  a  new  request.  Digest
   65authentication have the following advantages and disadvantages:
   66
   67  - Advantages
   68    - Authentication without exchanging the password
   69    - No re-use of authentication data
   70  - Disadvantages
   71    - An extra round trip is needed for the first authentication
   72    - Server-side storage of the password is the MD5 hash of the
   73      user, _realm_ and password.  As MD5 hashes are quick to
   74      compute, one needs strong passwords.  This fixed algorithm
   75      also allows for _rainbow table_ attacks, although their
   76      value is limited because you need to precompute the rainbow
   77      table for every server (_realm_) and user.
   78    - The connection is sensitive to man-in-the-middle attack,
   79      where the attacker can both change the request and response.
   80    - Both client and server need to keep an administration of
   81      issued _nonce_ values and associated _nonce count_ values.
   82
   83And, of course, the connection  itself   remains  insecure. Digest based
   84authentication is a viable alternative if HTTPS is not a good option and
   85security of the data itself is not an issue.
   86
   87This library acts as plugin   for library(http/http_dispatch), where the
   88registered handler (http_handler/3) can be  given   the  option below to
   89initiate digest authentication.
   90
   91  - authentication(digest(PasswdFile, Realm))
   92
   93Above, `PasswdFile` is a file containing lines  of the from below, where
   94PasswordHash is computed  using   http_digest_password_hash/4.  See also
   95library(http/http_authenticate),       http_read_passwd_file/2       and
   96http_write_passwd_file/2.
   97
   98  ==
   99  User ":" PasswordHash (":" Extra)*
  100  ==
  101
  102This library also  hooks  into   library(http/http_open)  if  the option
  103authorization(digest(User, Password)) is given.
  104
  105@see https://tools.ietf.org/html/rfc2617
  106*/
  107
  108:- setting(nonce_timeout, number, 3600,
  109           "Validity time for a server nonce").  110:- setting(client_nonce_timeout, number, 3600,
  111           "Validity time for a client nonce").  112
  113                 /*******************************
  114                 *      TRACK CONNECTIONS       *
  115                 *******************************/
  116
  117:- dynamic
  118    nonce_key/1,                    % Our nonce private key
  119    nonce/2,                        % Nonce, CreatedTime
  120    nonce_nc/3,                     % Nonce, NC, Time
  121    nonce_nc_first/2,               % Nonce, NC
  122    nonce_gc_time/1.                % Time of last nonce GC
  123
  124%!  register_nonce(+Nonce, +Created) is det.
  125%
  126%   Register a nonce created by the  server.   We  need  to do so to
  127%   ensure the client uses our nonce  and that the connection should
  128%   not considered timed out.
  129
  130register_nonce(Nonce64, Created) :-
  131    broadcast(http_digest(nonce(Nonce64, Created))),
  132    assertz(nonce(Nonce64, Created)),
  133    gc_nonce.
  134
  135%!  nonce_ok(+Nonce, +NC, -Stale) is semidet.
  136%
  137%   True if Nonce at nonce-count NC   is  acceptable. That means the
  138%   nonce has not timed out and we   have not seen the same sequence
  139%   number  before.  Note  that  requests   may  be  concurrent  and
  140%   therefore NC values may not come in order.
  141
  142nonce_ok(Nonce, NC, Stale) :-
  143    get_time(Now),
  144    nonce_not_timed_out(Nonce, Now, Stale),
  145    nonce_nc_ok(Nonce, NC, Now).
  146
  147nonce_not_timed_out(Nonce, Now, Stale) :-
  148    (   nonce(Nonce, Created)
  149    ->  setting(nonce_timeout, TimeOut),
  150        (   Now - Created < TimeOut
  151        ->  Stale = false
  152        ;   forget_nonce(Nonce),
  153            debug(http(nonce), 'Nonce timed out: ~q', [Nonce]),
  154            Stale = true
  155        )
  156    ;   our_nonce(Nonce, _Stamp)
  157    ->  Stale = true
  158    ;   debug(http(nonce), 'Unknown nonce: ~q', [Nonce]),
  159        fail
  160    ).
  161
  162nonce_nc_ok(Nonce, NC, _Now) :-
  163    (   nonce_nc(Nonce, NC, _)
  164    ;   nonce_nc_first(Nonce, First),
  165        NC @=< First
  166    ),
  167    !,
  168    debug(http(nonce), 'Nonce replay attempt: ~q@~q', [Nonce, NC]),
  169    fail.
  170nonce_nc_ok(Nonce, NC, Now) :-
  171    assertz(nonce_nc(Nonce, NC, Now)).
  172
  173forget_nonce(Nonce) :-
  174    retractall(nonce(Nonce, _)),
  175    retractall(nonce_nc(Nonce, _, _)),
  176    retractall(nonce_nc_first(Nonce, _)).
  177
  178%!  gc_nonce
  179%
  180%   Garbage collect server nonce.
  181
  182gc_nonce :-
  183    nonce_gc_time(Last),
  184    get_time(Now),
  185    setting(nonce_timeout, TimeOut),
  186    Now-Last < TimeOut/4,
  187    !.
  188gc_nonce :-
  189    with_mutex(http_digest_gc_nonce,
  190               gc_nonce_sync).
  191
  192gc_nonce_sync :-
  193    get_time(Now),
  194    asserta(nonce_gc_time(Now)),
  195    forall(( nonce_gc_time(T),
  196             T \== Now
  197           ),
  198           retractall(nonce_gc_time(T))),
  199    setting(nonce_timeout, TimeOut),
  200    Before is Now - TimeOut,
  201    forall(nonce_timed_out(Nonce, Before),
  202           forget_nonce(Nonce)),
  203    NCBefore is Now - 60,
  204    forall(nonce(Nonce, _Created),
  205           gc_nonce_nc(Nonce, NCBefore)).
  206
  207nonce_timed_out(Nonce, Before) :-
  208    nonce(Nonce, Created),
  209    Created < Before.
  210
  211gc_nonce_nc(Nonce, Before) :-
  212    findall(NC, gc_nonce_nc(Nonce, Before, NC), List),
  213    sort(0, @>, List, [Max|_]),
  214    !,
  215    asserta(nonce_nc_first(Nonce, Max)),
  216    forall(( nonce_nc_first(Nonce, NC),
  217             NC \== Max
  218           ),
  219           retractall(nonce_nc_first(Nonce, NC))).
  220gc_nonce_nc(_, _).
  221
  222gc_nonce_nc(Nonce, Before, NC) :-
  223    nonce_nc(Nonce, NC, Time),
  224    Time < Before,
  225    retractall(nonce_nc(Nonce, NC, Time)).
  226
  227
  228
  229%!  private_key(-PrivateKey) is det.
  230%
  231%   Return our private key.
  232
  233private_key(PrivateKey) :-
  234    nonce_key(PrivateKey),
  235    !.
  236private_key(PrivateKey) :-
  237    with_mutex(http_digest,
  238               private_key_sync(PrivateKey)).
  239
  240private_key_sync(PrivateKey) :-
  241    nonce_key(PrivateKey),
  242    !.
  243private_key_sync(PrivateKey) :-
  244    PrivateKey is random(1<<63-1),
  245    assertz(nonce_key(PrivateKey)).
  246
  247%!  our_nonce(+Nonce, -Stamp:string) is semidet.
  248%
  249%   True if we created Nonce at time Stamp.
  250%
  251%   @arg  Stamp  is  the  stamp  as  created  by  nonce//1:  a  time
  252%   stamp*1000+sequence number.
  253
  254our_nonce(Nonce64, Stamp) :-
  255    base64(Nonce, Nonce64),
  256    split_string(Nonce, ":", "", [Stamp,HNonceContent]),
  257    private_key(PrivateKey),
  258    atomics_to_string([Stamp,PrivateKey], ":", NonceContent),
  259    hash(NonceContent, HNonceContent).
  260
  261
  262                 /*******************************
  263                 *            GRAMMAR           *
  264                 *******************************/
  265
  266%!  http_digest_challenge(+Realm, +Options)//
  267%
  268%   Generate the content for  a   401  =|WWW-Authenticate:  Digest|=
  269%   header field.
  270
  271http_digest_challenge(Realm, Options) -->
  272    %       "Digest ",
  273            realm(Realm),
  274            domain(Options),
  275            nonce(Options),
  276            option_value(opaque, Options),
  277            stale(Options),
  278    %       algorithm(Options),
  279            qop_options(Options).
  280%       auth_param(Options).
  281
  282realm(Realm) -->
  283    { no_dquote(realm, Realm) },
  284    "realm=\"", atom(Realm), "\"".
  285
  286domain(Options) -->
  287    { option(domain(Domain), Options) },
  288    !,
  289    sep, "domain=\"", uris(Domain), "\"".
  290domain(_) --> "".
  291
  292uris(Domain) -->
  293    { atomic(Domain) },
  294    !,
  295    uri(Domain).
  296uris(Domains) -->
  297    { must_be(list(atomic), Domains)
  298    },
  299    uri_list(Domains).
  300
  301uri_list([]) --> "".
  302uri_list([H|T]) -->
  303    uri(H),
  304    (   {T \== []}
  305    ->  " ", uri_list(T)
  306    ;   ""
  307    ).
  308
  309uri(URI) -->
  310    { no_dquote(uri, URI) },
  311    atom(URI).
  312
  313%!  nonce(+Options)
  314%
  315%   Compute the server _nonce_ value.  Note   that  we  should never
  316%   generate the same nonce twice for   the  same client. The client
  317%   _may_ issue multiple requests without   an  authorization header
  318%   for resources appearing on a page. As long as we return distinct
  319%   nonce values, this is ok. If we do not, the server will reuse NC
  320%   counters on the same nonce, which will break the authentication.
  321
  322nonce(Options) -->
  323    { get_time(Now),
  324      flag(http_digest_nonce_seq, Seq, Seq+1),
  325      Stamp is floor(Now)*1000+(Seq mod 1000),
  326      private_key(PrivateKey),
  327      atomics_to_string([Stamp,PrivateKey], ":", NonceContent),
  328      hash(NonceContent, HNonceContent),
  329      atomics_to_string([Stamp,HNonceContent], ":", NonceText),
  330      base64(NonceText, Nonce),
  331      option(nonce(Nonce-Now), Options, _),
  332      debug(http(authenticate), 'Server nonce: ~q', [Nonce])
  333    },
  334    sep, "nonce=\"", atom(Nonce), "\"".
  335
  336stale(Options) -->
  337    { option(stale(true), Options), !
  338    },
  339    sep, "stale=true".
  340stale(_) --> "".
  341
  342qop_options(_Options) -->
  343    sep, "qop=\"auth,auth-int\"".
  344
  345option_value(Key, Options) -->
  346    { Opt =.. [Key,Value],
  347      option(Opt, Options), !
  348    },
  349    key_qvalue(Key, Value).
  350option_value(_, _) --> "".
  351
  352key_value(Key, Value)  -->
  353    atom(Key), "=", atom(Value).
  354key_qvalue(Key, Value) -->
  355    { no_dquote(Key, Value) },
  356    atom(Key), "=\"", atom(Value), "\"".
  357
  358no_dquote(Key, Value) :-
  359    nonvar(Value),
  360    sub_atom(Value, _, _, _, '"'),
  361    !,
  362    domain_error(Key, value).
  363no_dquote(_, _).
  364
  365sep --> ", ".
  366
  367hash(Text, Hash) :-
  368    md5_hash(Text, Hash, []).
  369
  370%!  http_digest_authenticate(+Request, -User, -UserFields, +Options)
  371%
  372%   Validate the client reponse from the Request header. On success,
  373%   User is the validated user and  UserFields are additional fields
  374%   from the password file. Options include:
  375%
  376%     - passwd_file(+File)
  377%     Validate passwords agains the given password file.  The
  378%     file is read using http_current_user/3 from
  379%     library(http/http_authenticate).
  380%     - stale(-Stale)
  381%     The request may succeed on a timed-out server nonce.  In
  382%     that case, Stale is unified with `true`.
  383
  384http_digest_authenticate(Request, [User|Fields], Options) :-
  385    memberchk(authorization(Authorization), Request),
  386    debug(http(authenticate), 'Authorization: ~w', [Authorization]),
  387    digest_authenticate(Authorization, User, Fields, Options).
  388
  389digest_authenticate(Authorization, User, Fields, Options) :-
  390    string_codes(Authorization, AuthorizationCodes),
  391    phrase(parse_digest_reponse(AuthValues), AuthorizationCodes),
  392    memberchk(username(User), AuthValues),
  393    memberchk(realm(Realm), AuthValues),
  394    memberchk(nonce(ServerNonce), AuthValues),
  395    memberchk(uri(Path), AuthValues),
  396    memberchk(qop(QOP), AuthValues),
  397    memberchk(nc(NC), AuthValues),
  398    memberchk(cnonce(ClientNonce), AuthValues),
  399    memberchk(response(Response), AuthValues),
  400    user_ha1_details(User, Realm, HA1, Fields, Options),
  401    option(method(Method), Options, get),
  402    ha2(Method, Path, HA2),
  403    atomics_to_string([ HA1,
  404                        ServerNonce,
  405                        NC,
  406                        ClientNonce,
  407                        QOP,
  408                        HA2
  409                      ], ":", ResponseText),
  410    debug(http(authenticate), 'ResponseText: ~w', [ResponseText]),
  411    hash(ResponseText, ResponseExpected),
  412    (   Response == ResponseExpected
  413    ->  debug(http(authenticate), 'We have a match!', [])
  414    ;   debug(http(authenticate),
  415              '~q \\== ~q', [Response, ResponseExpected]),
  416        fail
  417    ),
  418    nonce_ok(ServerNonce, NC, Stale),
  419    (   option(stale(Stale), Options)
  420    ->  true
  421    ;   Stale == false
  422    ).
  423
  424user_ha1_details(User, _Realm, HA1, Fields, Options) :-
  425    option(passwd_file(File), Options),
  426    http_current_user(File, User, [HA1|Fields]).
  427
  428%!  parse_digest_request(-Fields)//
  429%
  430%   Parse a digest request into a list of Name(Value) terms.
  431
  432parse_digest_request(Fields) -->
  433    "Digest", whites,
  434    digest_values(Fields).
  435
  436%!  parse_digest_reponse(-ResponseValues)//
  437
  438parse_digest_reponse(ResponseValues) -->
  439    "Digest", whites,
  440    digest_values(ResponseValues).
  441
  442
  443digest_values([H|T]) -->
  444    digest_value(H),
  445    !,
  446    whites,
  447    (   ","
  448    ->  whites,
  449        digest_values(T)
  450    ;   {T = []}
  451    ).
  452
  453digest_value(V) -->
  454    string_without(`=`, NameCodes), "=",
  455    { atom_codes(Name, NameCodes) },
  456    digest_value(Name, V).
  457
  458digest_value(Name, V) -->
  459    "\"",
  460    !,
  461    string_without(`"`, ValueCodes), "\"",
  462    { parse_value(Name, ValueCodes, Value),
  463      V =.. [Name,Value]
  464    }.
  465digest_value(stale, stale(V)) -->
  466    !,
  467    boolean(V).
  468digest_value(Name, V) -->
  469    string_without(`, `, ValueCodes),
  470    { parse_value(Name, ValueCodes, Value),
  471      V =.. [Name,Value]
  472    }.
  473
  474
  475parse_value(domain, Codes, Domain) :-
  476    !,
  477    string_codes(String, Codes),
  478    atomic_list_concat(Domain, ' ', String).
  479parse_value(Name, Codes, Value) :-
  480    atom_value(Name),
  481    atom_codes(Value, Codes).
  482parse_value(_Name, Codes, Value) :-
  483    string_codes(Value, Codes).
  484
  485atom_value(realm).
  486atom_value(username).
  487atom_value(response).
  488atom_value(nonce).
  489atom_value(stale).              % for misbehaving servers that quote stale
  490
  491boolean(true) --> "true".
  492boolean(false) --> "false".
  493
  494
  495                 /*******************************
  496                 *           CLIENT             *
  497                 *******************************/
  498
  499%!  http_parse_digest_challenge(+Challenge, -Fields) is det.
  500%
  501%   Parse the value of an HTTP =|WWW-Authenticate|= header into
  502%   a list of Name(Value) terms.
  503
  504http_parse_digest_challenge(Challenge, Fields) :-
  505    string_codes(Challenge, ReqCodes),
  506    phrase(parse_digest_request(Fields), ReqCodes).
  507
  508%!  http_digest_response(+Challenge, +User, +Password, -Reply, +Options)
  509%
  510%   Formulate a reply to a digest authentication request.  Options:
  511%
  512%     - path(+Path)
  513%     The request URI send along with the authentication.  Defaults
  514%     to `/`
  515%     - method(+Method)
  516%     The HTTP method.  Defaults to `'GET'`
  517%     - nc(+Integer)
  518%     The nonce-count as an integer.  This is formatted as an
  519%     8 hex-digit string.
  520%
  521%   @arg    Challenge is a list Name(Value), normally from
  522%           http_parse_digest_challenge/2.  Must contain
  523%           `realm` and  `nonce`.  Optionally contains
  524%           `opaque`.
  525%   @arg    User is the user we want to authenticated
  526%   @arg    Password is the user's password
  527%   @arg    Options provides additional options
  528
  529http_digest_response(Fields, User, Password, Reply, Options) :-
  530    phrase(http_digest_response(Fields, User, Password, Options), Codes),
  531    string_codes(Reply, Codes).
  532
  533http_digest_response(Fields, User, Password, Options) -->
  534    { memberchk(nonce(ServerNonce), Fields),
  535      memberchk(realm(Realm), Fields),
  536      client_nonce(ClientNonce),
  537      http_digest_password_hash(User, Realm, Password, HA1),
  538      QOP = 'auth',
  539      option(path(Path), Options, /),
  540      option(method(Method), Options, 'GET'),
  541      option(nc(NC), Options, 1),
  542      format(string(NCS), '~`0t~16r~8+', [NC]),
  543      ha2(Method, Path, HA2),
  544      atomics_to_string([ HA1,
  545                          ServerNonce,
  546                          NCS,
  547                          ClientNonce,
  548                          QOP,
  549                          HA2
  550                        ], ":", ResponseText),
  551      hash(ResponseText, Response)
  552    },
  553    "Digest ",
  554    key_qvalue(username, User),
  555    sep, key_qvalue(realm, Realm),
  556    sep, key_qvalue(nonce, ServerNonce),
  557    sep, key_qvalue(uri, Path),
  558    sep, key_value(qop, QOP),
  559    sep, key_value(nc, NCS),
  560    sep, key_qvalue(cnonce, ClientNonce),
  561    sep, key_qvalue(response, Response),
  562    (   { memberchk(opaque(Opaque), Fields) }
  563    ->  sep, key_qvalue(opaque, Opaque)
  564    ;   ""
  565    ).
  566
  567client_nonce(Nonce) :-
  568    V is random(1<<32),
  569    format(string(Nonce), '~`0t~16r~8|', [V]).
  570
  571ha2(Method, Path, HA2) :-
  572    string_upper(Method, UMethod),
  573    atomics_to_string([UMethod,Path], ":", A2),
  574    hash(A2, HA2).
  575
  576%!  http_digest_password_hash(+User, +Realm, +Password, -Hash) is det.
  577%
  578%   Compute the password hash for the HTTP password file.  Note that
  579%   the HTTP digest mechanism does allow us to use a seeded expensive
  580%   arbitrary hash function.  Instead, the hash is defined as the MD5
  581%   of the following components:
  582%
  583%     ==
  584%     <user>:<realm>:<password>.
  585%     ==
  586%
  587%   The inexpensive MD5 algorithm makes the hash sensitive to brute
  588%   force attacks while the lack of seeding make the hashes sensitive
  589%   for _rainbow table_ attacks, although the value is somewhat limited
  590%   because the _realm_ and _user_ are part of the hash.
  591
  592http_digest_password_hash(User, Realm, Password, HA1) :-
  593    atomics_to_string([User,Realm,Password], ":", A1),
  594    hash(A1, HA1).
  595
  596
  597                 /*******************************
  598                 *   PLUGIN FOR HTTP_DISPATCH   *
  599                 *******************************/
  600
  601:- multifile
  602    http:authenticate/3.  603
  604%!  http:authenticate(+Digest, +Request, -Fields)
  605%
  606%   Plugin  for  library(http_dispatch)  to    perform   basic  HTTP
  607%   authentication.  Note that we keep the authentication details
  608%   cached to avoid a `nonce-replay' error in the case that the
  609%   application tries to verify multiple times.
  610%
  611%   This predicate throws http_reply(authorise(digest(Digest)))
  612%
  613%   @arg    Digest is a term digest(File, Realm, Options)
  614%   @arg    Request is the HTTP request
  615%   @arg    Fields describes the authenticated user with the option
  616%           user(User) and with the option user_details(Fields) if
  617%           the password file contains additional fields after the
  618%           user and password.
  619
  620http:authenticate(digest(File, Realm), Request, Details) :-
  621    http:authenticate(digest(File, Realm, []), Request, Details).
  622http:authenticate(digest(File, Realm, Options), Request, Details) :-
  623    current_output(CGI),
  624    cgi_property(CGI, id(Id)),
  625    (   nb_current('$http_digest_user', Id-Details)
  626    ->  true
  627    ;   authenticate(digest(File, Realm, Options), Request, Details),
  628        nb_setval('$http_digest_user', Id-Details)
  629    ).
  630
  631authenticate(digest(File, Realm, Options), Request,
  632             [ user(User)
  633             | Details
  634             ]) :-
  635    (   option(method(Method), Request, get),
  636        http_digest_authenticate(Request, [User|Fields],
  637                                 [ passwd_file(File),
  638                                   stale(Stale),
  639                                   method(Method)
  640                                 ])
  641    ->  (   Stale == false
  642        ->  (   Fields == []
  643            ->  Details = []
  644            ;   Details = [user_details(Fields)]
  645            ),
  646            Ok = true
  647        ;   true
  648        )
  649    ;   true
  650    ),
  651    (   Ok == true
  652    ->  true
  653    ;   add_option(nonce(Nonce-Created), Options, Options1),
  654        add_stale(Stale, Options1, Options2),
  655        phrase(http_digest_challenge(Realm, Options2), DigestCodes),
  656        string_codes(Digest, DigestCodes),
  657        register_nonce(Nonce, Created),
  658        throw(http_reply(authorise(digest(Digest))))
  659    ).
  660
  661add_option(Option, Options0, _) :-
  662    option(Option, Options0),
  663    !.
  664add_option(Option, Options0, [Option|Options0]).
  665
  666add_stale(Stale, Options0, Options) :-
  667    Stale == true,
  668    !,
  669    Options = [stale(true)|Options0].
  670add_stale(_, Options, Options).
  671
  672
  673                 /*******************************
  674                 *     PLUGIN FOT HTTP_OPEN     *
  675                 *******************************/
  676
  677:- multifile
  678    http:authenticate_client/2.  679:- dynamic
  680    client_nonce/4,                 % Authority, Domains, Keep, Time
  681    client_nonce_nc/3,              % Nonce, Count, Time
  682    client_nonce_gc_time/1.         % Time
  683
  684%!  http:authenticate_client(+URL, +Action) is semidet.
  685%
  686%   This hooks is called by http_open/3 with the following Action
  687%   value:
  688%
  689%     - send_auth_header(+AuthData, +Out, +Options)
  690%     Called when sending the initial request.  AuthData contains
  691%     the value for the http_open/3 option authorization(AuthData)
  692%     and Out is a stream on which to write additional HTTP headers.
  693%     - auth_reponse(+Headers, +OptionsIn, -Options)
  694%     Called if the server replies with a 401 code, challenging the
  695%     client.  Our implementation adds a
  696%     request_header(authorization=Digest) header to Options, causing
  697%     http_open/3 to retry the request with the additional option.
  698
  699http:authenticate_client(URL, auth_reponse(Headers, OptionsIn, Options)) :-
  700    debug(http(authenticate), "Got 401 with ~p", [Headers]),
  701    memberchk(www_authenticate(Authenticate), Headers),
  702    http_parse_digest_challenge(Authenticate, Fields),
  703    user_password(OptionsIn, User, Password),
  704    !,
  705    uri_components(URL, Components),
  706    uri_data(path, Components, Path),
  707    http_digest_response(Fields, User, Password, Digest,
  708                             [ path(Path)
  709                             | OptionsIn
  710                             ]),
  711    merge_options([ request_header(authorization=Digest)
  712                  ],
  713                  OptionsIn, Options),
  714    keep_digest_credentials(URL, Fields).
  715http:authenticate_client(URL, send_auth_header(Auth, Out, Options)) :-
  716    authorization_data(Auth, User, Password),
  717    uri_components(URL, Components),
  718    uri_data(authority, Components, Authority),
  719    uri_data(path, Components, Path),
  720    digest_credentials(Authority, Path, Nonce, Fields),
  721    !,
  722    next_nonce_count(Nonce, NC),
  723    debug(http(authenticate), "Continue ~p nc=~q", [URL, NC]),
  724    http_digest_response(Fields, User, Password, Digest,
  725                         [ nc(NC),
  726                           path(Path)
  727                         | Options
  728                         ]),
  729    format(Out, 'Authorization: ~w\r\n', [Digest]).
  730http:authenticate_client(URL, send_auth_header(Auth, _Out, _Options)) :-
  731    debug(http(authenticate), "Failed ~p", [URL]),
  732    authorization_data(Auth, _User, _Password).
  733
  734
  735user_password(Options, User, Password) :-
  736    option(authorization(Auth), Options),
  737    authorization_data(Auth, User, Password).
  738
  739authorization_data(digest(User, Password), User, Password).
  740
  741%!  digest_credentials(+Authority, +Path, -Nonce, -Fields) is semidet.
  742%
  743%   True if we have digest credentials for Authority on Path with the
  744%   server _nonce_ Nonce and additional Fields.
  745
  746digest_credentials(Authority, Path, Nonce, Fields) :-
  747    client_nonce(Authority, Domains, Fields, _Created),
  748    in_domain(Path, Domains),
  749    memberchk(nonce(Nonce), Fields),
  750    !.
  751
  752in_domain(Path, Domains) :-
  753    member(Domain, Domains),
  754    sub_atom(Path, 0, _, _, Domain),
  755    !.
  756
  757next_nonce_count(Nonce, NC) :-
  758    with_mutex(http_digest_client,
  759               next_nonce_count_sync(Nonce, NC)).
  760
  761next_nonce_count_sync(Nonce, NC) :-
  762    retract(client_nonce_nc(Nonce, NC0, _)),
  763    !,
  764    NC1 is NC0+1,
  765    get_time(Now),
  766    assert(client_nonce_nc(Nonce, NC1, Now)),
  767    NC = NC1.
  768next_nonce_count_sync(Nonce, 2) :-
  769    get_time(Now),
  770    assert(client_nonce_nc(Nonce, 2, Now)).
  771
  772%!  keep_digest_credentials(+URL, +Fields)
  773%
  774%   Keep the digest credentials for subsequent connections.
  775
  776keep_digest_credentials(URL, Fields) :-
  777    get_time(Now),
  778    uri_components(URL, Components),
  779    uri_data(authority, Components, Authority),
  780    include(keep_field, Fields, Keep),
  781    (   memberchk(domain(Domains), Fields)
  782    ->  true
  783    ;   Domains = [/]
  784    ),
  785    assertz(client_nonce(Authority, Domains, Keep, Now)),
  786    gc_client_nonce.
  787
  788keep_field(realm(_)).
  789keep_field(nonce(_)).
  790keep_field(opaque(_)).
  791
  792gc_client_nonce :-
  793    client_nonce_gc_time(Last),
  794    get_time(Now),
  795    setting(client_nonce_timeout, TimeOut),
  796    Now-Last < TimeOut/4,
  797    !.
  798gc_client_nonce :-
  799    get_time(Now),
  800    retractall(client_nonce_gc_time(_)),
  801    asserta(client_nonce_gc_time(Now)),
  802    setting(client_nonce_timeout, TimeOut),
  803    Before is Now-TimeOut,
  804    forall(client_nonce_expired(Nonce, Before),
  805           forget_client_nonce(Nonce)).
  806
  807client_nonce_expired(Nonce, Before) :-
  808    client_nonce(_Authority, _Domains, Fields, Created),
  809    Created < Before,
  810    memberchk(nonce(Nonce), Fields),
  811    \+ ( client_nonce_nc(Nonce, _, Last),
  812         Last < Before
  813       ).
  814
  815forget_client_nonce(Nonce) :-
  816    client_nonce(_, _, Fields, Created),
  817    memberchk(nonce(Nonce), Fields),
  818    !,
  819    retractall(client_nonce(_, _, Fields, Created)),
  820    retractall(client_nonce_nc(Nonce, _, _))