view src/swipl/http_openid.pl @ 100:a4cd935561d4 dml-dockerised tip

small updates and version pin for cliopatria
author wolffd <wolffd.mail@googlemail.com>
date Fri, 29 Jun 2018 17:48:41 +0100
parents c7720fefea26
children
line wrap: on
line source
/*  Part of SWI-Prolog

    Author:        Jan Wielemaker
    E-mail:        J.Wielemaker@cs.vu.nl
    WWW:           http://www.swi-prolog.org
    Copyright (C): 2007-2013, University of Amsterdam,
			      VU University Amsterdam

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    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 General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

    As a special exception, if you link this library with other files,
    compiled with a Free Software compiler, to produce an executable, this
    library does not by itself cause the resulting executable to be covered
    by the GNU General Public License. This exception does not however
    invalidate any other reasons why the executable file might be covered by
    the GNU General Public License.
*/

:- module(http_openid,
	  [ openid_login/1,		% +OpenID
	    openid_logout/1,		% +OpenID
	    openid_logged_in/1,		% -OpenID

					% transparent login
	    openid_user/3,		% +Request, -User, +Options

					% low-level primitives
	    openid_verify/2,		% +Options, +Request
	    openid_authenticate/4,	% +Request, -Server, -Identity, -ReturnTo
	    openid_associate/3,		% +OpenIDServer, -Handle, -Association
	    openid_associate/4,		% +OpenIDServer, -Handle, -Association,
					% +Options
	    openid_server/2,		% +Options, +Request
	    openid_server/3,		% ?OpenIDLogin, ?OpenID, ?Server
	    openid_grant/1,		% +Request

	    openid_login_form//2,	% +ReturnTo, +Options, //

	    openid_current_url/2,	% +Request, -URL
	    openid_current_host/3	% +Request, -Host, -Port
	  ]).
:- use_module(library(http/http_open)).
:- use_module(library(http/html_write)).
:- use_module(library(http/http_parameters)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_session)).
:- use_module(library(http/http_host)).
:- use_module(library(http/http_path)).
:- use_module(library(http/html_head)).
:- use_module(library(http/http_server_files), []).
:- use_module(library(http/yadis)).
:- use_module(library(http/ax)).
:- use_module(library(utf8)).
:- use_module(library(error)).
:- use_module(library(xpath)).
:- use_module(library(sgml)).
:- use_module(library(uri)).
:- use_module(library(occurs)).
:- use_module(library(base64)).
:- use_module(library(debug)).
:- use_module(library(record)).
:- use_module(library(option)).
:- use_module(library(sha)).
:- use_module(library(lists)).
:- use_module(library(settings)).

:- predicate_options(openid_login_form/4, 2,
		     [ action(atom),
		       buttons(list),
		       show_stay(boolean)
		     ]).
:- predicate_options(openid_server/2, 1,
		     [ expires_in(any)
		     ]).
:- predicate_options(openid_user/3, 3,
		     [ login_url(atom)
		     ]).
:- predicate_options(openid_verify/2, 1,
		     [ return_to(atom),
		       trust_root(atom),
		       realm(atom),
		       ax(any)
		     ]).

/** <module> OpenID consumer and server library

This library implements the OpenID protocol (http://openid.net/). OpenID
is a protocol to share identities on   the  network. The protocol itself
uses simple basic  HTTP,  adding   reliability  using  digitally  signed
messages.

Steps, as seen from the _consumer_ (or _|relying partner|_).

	1. Show login form, asking for =openid_identifier=
	2. Get HTML page from =openid_identifier= and lookup
	   =|<link rel="openid.server" href="server">|=
	3. Associate to _server_
	4. Redirect browser (302) to server using mode =checkid_setup=,
	   asking to validate the given OpenID.
	5. OpenID server redirects back, providing digitally signed
	   conformation of the claimed identity.
	6. Validate signature and redirect to the target location.

A *consumer* (an application that allows OpenID login) typically uses
this library through openid_user/3. In addition, it must implement the
hook http_openid:openid_hook(trusted(OpenId, Server)) to define accepted
OpenID servers. Typically, this hook is used to provide a white-list of
aceptable servers. Note that accepting any OpenID server is possible,
but anyone on the internet can setup a dummy OpenID server that simply
grants and signs every request. Here is an example:

    ==
    :- multifile http_openid:openid_hook/1.

    http_openid:openid_hook(trusted(_, OpenIdServer)) :-
	(   trusted_server(OpenIdServer)
	->  true
	;   throw(http_reply(moved_temporary('/openid/trustedservers')))
	).

    trusted_server('http://www.myopenid.com/server').
    ==

By default, information who is logged on  is maintained with the session
using http_session_assert/1 with the term   openid(Identity).  The hooks
login/logout/logged_in can be used to provide alternative administration
of logged-in users (e.g., based on client-IP, using cookies, etc.).

To create a *server*,  you  must  do   four  things:  bind  the handlers
openid_server/2  and  openid_grant/1  to  HTTP    locations,  provide  a
user-page for registered users and   define  the grant(Request, Options)
hook to verify  your  users.  An  example   server  is  provided  in  in
<plbase>/doc/packages/examples/demo_openid.pl
*/

		 /*******************************
		 *	  CONFIGURATION		*
		 *******************************/

http:location(openid, root(openid), [priority(-100)]).

%%	openid_hook(+Action)
%
%	Call hook on the OpenID management library.  Defined hooks are:
%
%	  * login(+OpenID)
%	  Consider OpenID logged in.
%
%	  * logout(+OpenID)
%	  Logout OpenID
%
%	  * logged_in(?OpenID)
%	  True if OpenID is logged in
%
%	  * grant(+Request, +Options)
%	  Server: Reply positive on OpenID
%
%	  * trusted(+OpenID, +Server)
%	  True if Server is a trusted OpenID server
%
%	  * ax(Values)
%	  Called if the server provided AX attributes
%
%	  * x_parameter(+Server, -Name, -Value)
%	  Called to find additional HTTP parameters to send with the
%	  OpenID verify request.

:- multifile
	openid_hook/1.			% +Action

		 /*******************************
		 *	 DIRECT LOGIN/OUT	*
		 *******************************/

%%	openid_login(+OpenID) is det.
%
%	Associate the current  HTTP  session   with  OpenID.  If another
%	OpenID is already associated, this association is first removed.

openid_login(OpenID) :-
	openid_hook(login(OpenID)), !,
	handle_stay_signed_in(OpenID).
openid_login(OpenID) :-
	openid_logout(_),
	http_session_assert(openid(OpenID)),
	handle_stay_signed_in(OpenID).

%%	openid_logout(+OpenID) is det.
%
%	Remove the association of the current session with any OpenID

openid_logout(OpenID) :-
	openid_hook(logout(OpenID)), !.
openid_logout(OpenID) :-
	http_session_retractall(openid(OpenID)).

%%	openid_logged_in(-OpenID) is semidet.
%
%	True if session is associated with OpenID.

openid_logged_in(OpenID) :-
	openid_hook(logged_in(OpenID)), !.
openid_logged_in(OpenID) :-
	http_in_session(_SessionId),		% test in session
	http_session_data(openid(OpenID)).


		 /*******************************
		 *	      TOPLEVEL		*
		 *******************************/

%%	openid_user(+Request:http_request, -OpenID:url, +Options) is det.
%
%	True if OpenID is a validated OpenID associated with the current
%	session. The scenario for which this predicate is designed is to
%	allow  an  HTTP  handler  that  requires    a   valid  login  to
%	use the transparent code below.
%
%	  ==
%	  handler(Request) :-
%		openid_user(Request, OpenID, []),
%		...
%	  ==
%
%	If the user is not yet logged on a sequence of redirects will
%	follow:
%
%	  1. Show a page for login (default: page /openid/login),
%	     predicate reply_openid_login/1)
%	  2. By default, the OpenID login page is a form that is
%	     submitted to the =verify=, which calls openid_verify/2.
%	  3. openid_verify/2 does the following:
%	     - Find the OpenID claimed identity and server
%	     - Associate to the OpenID server
%	     - redirects to the OpenID server for validation
%	  4. The OpenID server will redirect here with the authetication
%	     information.  This is handled by openid_authenticate/4.
%
%	Options:
%
%	  * login_url(Login)
%	    (Local) URL of page to enter OpenID information. Default
%	    is the handler for openid_login_page/1
%
%	@see openid_authenticate/4 produces errors if login is invalid
%	or cancelled.

:- http_handler(openid(login),	      openid_login_page,   [priority(-10)]).
:- http_handler(openid(verify),	      openid_verify([]),   []).
:- http_handler(openid(authenticate), openid_authenticate, []).
:- http_handler(openid(xrds),	      openid_xrds,	   []).

openid_user(_Request, OpenID, _Options) :-
	openid_logged_in(OpenID), !.
openid_user(Request, _OpenID, Options) :-
	http_link_to_id(openid_login_page, [], DefLoginPage),
	option(login_url(LoginPage), Options, DefLoginPage),
	openid_current_url(Request, Here),
	(  member(referer(Referer),Request)
	-> ReturnTo=Referer
	;  ReturnTo=Here
	),
	redirect_browser(LoginPage,
			 [ 'openid.return_to' = ReturnTo
			 ]).

%%	openid_xrds(Request)
%
%	Reply to a request  for   "Discovering  OpenID Relying Parties".
%	This may happen as part of  the provider verification procedure.
%	The  provider  will   do   a    Yadis   discovery   request   on
%	=openid.return=  or  =openid.realm=.  This  is    picked  up  by
%	openid_user/3, pointing the provider to   openid(xrds).  Now, we
%	reply with the locations marked =openid=  and the locations that
%	have actually been doing OpenID validations.

openid_xrds(Request) :-
	http_link_to_id(openid_authenticate, [], Autheticate),
	public_url(Request, Autheticate, Public),
	format('Content-type: text/xml\n\n'),
	format('<?xml version="1.0" encoding="UTF-8"?>\n'),
	format('<xrds:XRDS\n'),
	format('    xmlns:xrds="xri://$xrds"\n'),
	format('    xmlns="xri://$xrd*($v*2.0)">\n'),
	format('  <XRD>\n'),
	format('    <Service>\n'),
	format('      <Type>http://specs.openid.net/auth/2.0/return_to</Type>\n'),
	format('      <URI>~w</URI>\n', [Public]),
	format('    </Service>\n'),
	format('  </XRD>\n'),
	format('</xrds:XRDS>\n').


%%	openid_login_page(+Request) is det.
%
%	Present a login-form for OpenID. There  are two ways to redefine
%	this  default  login  page.  One  is    to  provide  the  option
%	=login_url= to openid_user/3 and the other   is  to define a new
%	handler for =|/openid/login|= using http_handler/3.

openid_login_page(Request) :-
	http_open_session(_, []),
	http_parameters(Request,
			[ 'openid.return_to'(Target, [])
			]),
	reply_html_page([ title('OpenID login')
			],
			[ \openid_login_form(Target, [])
			]).

%%	openid_login_form(+ReturnTo, +Options)// is det.
%
%	Create the OpenID  form.  This  exported   as  a  seperate  DCG,
%	allowing applications to redefine /openid/login   and reuse this
%	part of the page.  Options processed:
%
%	  - action(Action)
%	  URL of action to call.  Default is the handler calling
%	  openid_verify/1.
%	  - buttons(+Buttons)
%	  Buttons is a list of =img= structures where the =href=
%	  points to an OpenID 2.0 endpoint.  These buttons are
%	  displayed below the OpenID URL field.  Clicking the
%	  button sets the URL field and submits the form.  Requires
%	  Javascript support.
%
%	  If the =href= is _relative_, clicking it opens the given
%	  location after adding 'openid.return_to' and `stay'.
%	  - show_stay(+Boolean)
%	  If =true=, show a checkbox that allows the user to stay
%	  logged on.

openid_login_form(ReturnTo, Options) -->
	{ http_link_to_id(openid_verify, [], VerifyLocation),
	  option(action(Action), Options, VerifyLocation),
	  http_session_retractall(openid(_)),
	  http_session_retractall(openid_login(_,_,_,_)),
	  http_session_retractall(ax(_))
	},
	html(div([ class('openid-login')
		 ],
		 [ \openid_title,
		   form([ name(login),
			  id(login),
			  action(Action),
			  method('GET')
			],
			[ \hidden('openid.return_to', ReturnTo),
			  div([ input([ class('openid-input'),
					name(openid_url),
					id(openid_url),
					size(30),
					placeholder('Your OpenID URL')
				      ]),
				input([ type(submit),
					value('Verify!')
				      ])
			      ]),
			  \buttons(Options),
			  \stay_logged_on(Options)
			])
		 ])).

stay_logged_on(Options) -->
	{ option(show_stay(true), Options) }, !,
	html(div(class('openid-stay'),
		 [ input([ type(checkbox), id(stay), name(stay), value(yes)]),
		   'Stay signed in'
		 ])).
stay_logged_on(_) --> [].

buttons(Options) -->
	{ option(buttons(Buttons), Options),
	  Buttons \== []
	},
	html(div(class('openid-buttons'),
		 [ 'Sign in with '
		 | \prelogin_buttons(Buttons)
		 ])).
buttons(_) --> [].

prelogin_buttons([]) --> [].
prelogin_buttons([H|T]) --> prelogin_button(H), prelogin_buttons(T).

%%	prelogin_button(+Image)// is det.
%
%	Handle OpenID 2.0 and other pre-login  buttons. If the image has
%	a =href= attribute that is absolute, it   is  taken as an OpenID
%	2.0 endpoint. Otherwise it is taken  as   a  link on the current
%	server. This allows us to present  non-OpenId logons in the same
%	screen. The dedicated  handler  is   passed  the  HTTP paramters
%	=openid.return_to= and =stay=.

prelogin_button(img(Attrs)) -->
	{ select_option(href(HREF), Attrs, RestAttrs),
	  uri_is_global(HREF), !
	},
	html(img([ onClick('javascript:{$("#openid_url").val("'+HREF+'");'+
			   '$("form#login").submit();}'
			  )
		 | RestAttrs
		 ])).
prelogin_button(img(Attrs)) -->
	{ select_option(href(HREF), Attrs, RestAttrs)
	},
	html(img([ onClick('window.location = "'+HREF+
			   '?openid.return_to="'+
			   '+encodeURIComponent($("#return_to").val())'+
			   '+"&stay="'+
			   '+$("#stay").val()')
		 | RestAttrs
		 ])).


		 /*******************************
		 *	    HTTP REPLIES	*
		 *******************************/

%%	openid_verify(+Options, +Request)
%
%	Handle the initial login  form  presented   to  the  user by the
%	relying party (consumer). This predicate   discovers  the OpenID
%	server, associates itself with  this   server  and redirects the
%	user's  browser  to  the  OpenID  server,  providing  the  extra
%	openid.X name-value pairs. Options is,  against the conventions,
%	placed in front of the Request   to allow for smooth cooperation
%	with http_dispatch.pl.  Options processes:
%
%	  * return_to(+URL)
%	  Specifies where the OpenID provider should return to.
%	  Normally, that is the current location.
%	  * trust_root(+URL)
%	  Specifies the =openid.trust_root= attribute.  Defaults to
%	  the root of the current server (i.e., =|http://host[.port]/|=).
%	  * realm(+URL)
%	  Specifies the =openid.realm= attribute.  Default is the
%	  =trust_root=.
%	  * ax(+Spec)
%	  Request the exchange of additional attributes from the
%	  identity provider.  See http_ax_attributes/2 for details.
%
%	The OpenId server will redirect to the =openid.return_to= URL.
%
%	@throws	http_reply(moved_temporary(Redirect))

openid_verify(Options, Request) :-
	http_parameters(Request,
			[ openid_url(URL, [length>1]),
			  'openid.return_to'(ReturnTo0, [optional(true)]),
			  stay(Stay, [optional(true), default(no)])
			]),
	(   option(return_to(ReturnTo1), Options)	% Option
	->  openid_current_url(Request, CurrentLocation),
	    global_url(ReturnTo1, CurrentLocation, ReturnTo)
	;   nonvar(ReturnTo0)
	->  ReturnTo = ReturnTo0			% Form-data
	;   openid_current_url(Request, CurrentLocation),
	    ReturnTo = CurrentLocation			% Current location
	),
	public_url(Request, /, CurrentRoot),
	option(trust_root(TrustRoot), Options, CurrentRoot),
	option(realm(Realm), Options, TrustRoot),
	openid_resolve(URL, OpenIDLogin, OpenID, Server, ServerOptions),
	trusted(OpenID, Server),
	openid_associate(Server, Handle, _Assoc),
	assert_openid(OpenIDLogin, OpenID, Server, ReturnTo),
	stay(Stay),
	option(ns(NS), Options, 'http://specs.openid.net/auth/2.0'),
	(   realm_attribute(NS, RealmAttribute)
	->  true
	;   domain_error('openid.ns', NS)
	),
	findall(P=V, openid_hook(x_parameter(Server, P, V)), XAttrs, AXAttrs),
	debug(openid(verify), 'XAttrs: ~p', [XAttrs]),
	ax_options(ServerOptions, Options, AXAttrs),
	http_link_to_id(openid_authenticate, [], AuthenticateLoc),
	public_url(Request, AuthenticateLoc, Authenticate),
	redirect_browser(Server, [ 'openid.ns'		 = NS,
				   'openid.mode'         = checkid_setup,
				   'openid.identity'     = OpenID,
				   'openid.claimed_id'   = OpenID,
				   'openid.assoc_handle' = Handle,
				   'openid.return_to'    = Authenticate,
				   RealmAttribute        = Realm
				 | XAttrs
				 ]).

realm_attribute('http://specs.openid.net/auth/2.0', 'openid.realm').
realm_attribute('http://openid.net/signon/1.1',     'openid.trust_root').


%%	stay(+Response)
%
%	Called if the user  ask  to  stay   signed  in.  This  is called
%	_before_ control is handed to the   OpenID server. It leaves the
%	data openid_stay_signed_in(true) in the current session.

stay(yes) :- !,
	http_session_assert(openid_stay_signed_in(true)).
stay(_).

%%	handle_stay_signed_in(+OpenID)
%
%	Handle stay_signed_in option after the user has logged on

handle_stay_signed_in(OpenID) :-
	http_session_retract(openid_stay_signed_in(true)), !,
	http_set_session(timeout(0)),
	ignore(openid_hook(stay_signed_in(OpenID))).
handle_stay_signed_in(_).

%%	assert_openid(+OpenIDLogin, +OpenID, +Server, +Target) is det.
%
%	Associate the OpenID  as  typed  by   the  user,  the  OpenID as
%	validated by the Server with the current HTTP session.
%
%	@param OpenIDLogin Canonized OpenID typed by user
%	@param OpenID OpenID verified by Server.

assert_openid(OpenIDLogin, OpenID, Server, Target) :-
	openid_identifier_select_url(OpenIDLogin),
	openid_identifier_select_url(OpenID), !,
	http_session_assert(openid_login(Identity, Identity, Server, Target)).
assert_openid(OpenIDLogin, OpenID, Server, Target) :-
	http_session_assert(openid_login(OpenIDLogin, OpenID, Server, Target)).

%%	openid_server(?OpenIDLogin, ?OpenID, ?Server) is nondet.
%
%	True if OpenIDLogin is the typed id for OpenID verified by
%	Server.
%
%	@param OpenIDLogin ID as typed by user (canonized)
%	@param OpenID ID as verified by server
%	@param Server URL of the OpenID server

openid_server(OpenIDLogin, OpenID, Server) :-
	openid_server(OpenIDLogin, OpenID, Server, _Target).

openid_server(OpenIDLogin, OpenID, Server, Target) :-
	http_in_session(_),
	http_session_data(openid_login(OpenIDLogin, OpenID, Server, Target)), !.


%%	public_url(+Request, +Path, -URL) is det.
%
%	True when URL is a publically useable  URL that leads to Path on
%	the current server.

public_url(Request, Path, URL) :-
	openid_current_host(Request, Host, Port),
	setting(http:public_scheme, Scheme),
	set_port(Scheme, Port, AuthC),
	uri_authority_data(host, AuthC, Host),
	uri_authority_components(Auth, AuthC),
	uri_data(scheme, Components, Scheme),
	uri_data(authority, Components, Auth),
	uri_data(path, Components, Path),
	uri_components(URL, Components).

set_port(Scheme, Port, _) :-
	scheme_port(Scheme, Port), !.
set_port(_, Port, AuthC) :-
	uri_authority_data(port, AuthC, Port).

scheme_port(http, 80).
scheme_port(https, 443).


%%	openid_current_url(+Request, -URL) is det.
%
%	@deprecated	New code should use http_public_url/2 with the
%			same semantics.

openid_current_url(Request, URL) :-
	http_public_url(Request, URL).

%%	openid_current_host(Request, Host, Port)
%
%	Find current location of the server.
%
%	@deprecated	New code should use http_current_host/4 with the
%			option global(true).

openid_current_host(Request, Host, Port) :-
	http_current_host(Request, Host, Port,
			  [ global(true)
			  ]).


%%	redirect_browser(+URL, +FormExtra)
%
%	Generate a 302 temporary redirect to  URL, adding the extra form
%	information from FormExtra. The specs says   we  must retain the
%	search specification already attached to the URL.

redirect_browser(URL, FormExtra) :-
	uri_components(URL, C0),
	uri_data(search, C0, Search0),
	(   var(Search0)
	->  uri_query_components(Search, FormExtra)
	;   uri_query_components(Search0, Form0),
	    append(FormExtra, Form0, Form),
	    uri_query_components(Search, Form)
	),
	uri_data(search, C0, Search, C),
	uri_components(Redirect, C),
	throw(http_reply(moved_temporary(Redirect))).


		 /*******************************
		 *	       RESOLVE		*
		 *******************************/

%%	openid_resolve(+URL, -OpenIDOrig, -OpenID, -Server, -ServerOptions)
%
%	True if OpenID is the claimed  OpenID   that  belongs to URL and
%	Server is the URL of the  OpenID   server  that  can be asked to
%	verify this claim.
%
%	@param  URL The OpenID typed by the user
%	@param	OpenIDOrig Canonized OpenID typed by user
%	@param	OpenID Possibly delegated OpenID
%	@param  Server OpenID server that must validate OpenID
%	@param	ServerOptions provides additional XRDS information about
%		the server.  Currently supports xrds_types(Types).
%	@tbd	Implement complete URL canonization as defined by the
%		OpenID 2.0 proposal.

openid_resolve(URL, OpenID, OpenID, Server, [xrds_types(Types)]) :-
	xrds_dom(URL, DOM),
	xpath(DOM, //(_:'Service'), Service),
	findall(Type, xpath(Service, _:'Type'(text), Type), Types),
	memberchk('http://specs.openid.net/auth/2.0/server', Types),
	xpath(Service, _:'URI'(text), Server), !,
	debug(openid(yadis), 'Yadis: server: ~q, types: ~q', [Server, Types]),
	(   xpath(Service, _:'LocalID'(text), OpenID)
	->  true
	;   openid_identifier_select_url(OpenID)
	).
openid_resolve(URL, OpenID0, OpenID, Server, []) :-
	debug(openid(resolve), 'Opening ~w ...', [URL]),
	dtd(html, DTD),
	setup_call_cleanup(
	    http_open(URL, Stream,
		      [ final_url(OpenID0),
			cert_verify_hook(ssl_verify)
		      ]),
	    load_structure(Stream, Term,
			   [ dtd(DTD),
			     dialect(sgml),
			     shorttag(false),
			     syntax_errors(quiet)
			   ]),
	    close(Stream)),
	debug(openid(resolve), 'Scanning HTML document ...', [URL]),
	contains_term(element(head, _, Head), Term),
	(   link(Head, 'openid.server', Server)
	->  debug(openid(resolve), 'OpenID Server=~q', [Server])
	;   debug(openid(resolve), 'No server in ~q', [Head]),
	    fail
	),
	(   link(Head, 'openid.delegate', OpenID)
	->  debug(openid(resolve), 'OpenID = ~q (delegated)', [OpenID])
	;   OpenID = OpenID0,
	    debug(openid(resolve), 'OpenID = ~q', [OpenID])
	).

openid_identifier_select_url(
    'http://specs.openid.net/auth/2.0/identifier_select').

:- public ssl_verify/5.

%%	ssl_verify(+SSL, +ProblemCert, +AllCerts, +FirstCert, +Error)
%
%	Accept all certificates. We do not care  too much. Only the user
%	cares s/he is not entering her  credentials with a spoofed side.
%	As we redirect, the browser will take care of this.

ssl_verify(_SSL,
	   _ProblemCertificate, _AllCertificates, _FirstCertificate,
	   _Error).


link(DOM, Type, Target) :-
	sub_term(element(link, Attrs, []), DOM),
	memberchk(rel=Type, Attrs),
	memberchk(href=Target, Attrs).


		 /*******************************
		 *	   AUTHENTICATE		*
		 *******************************/

%%	openid_authenticate(+Request)
%
%	HTTP handler when redirected back from the OpenID provider.

openid_authenticate(Request) :-
	memberchk(accept(Accept), Request),
	Accept = [media(application/'xrds+xml',_,_,_)], !,
	http_link_to_id(openid_xrds, [], XRDSLocation),
	http_absolute_uri(XRDSLocation, XRDSServer),
	debug(openid(yadis), 'Sending XRDS server: ~q', [XRDSServer]),
	format('X-XRDS-Location: ~w\n', [XRDSServer]),
	format('Content-type: text/plain\n\n').
openid_authenticate(Request) :-
	openid_authenticate(Request, _OpenIdServer, OpenID, _ReturnTo),
	openid_server(User, OpenID, _, Target),
	openid_login(User),
	redirect_browser(Target, []).


%%	openid_authenticate(+Request, -Server:url, -OpenID:url,
%%			    -ReturnTo:url) is semidet.
%
%	Succeeds if Request comes from the   OpenID  server and confirms
%	that User is a verified OpenID   user. ReturnTo provides the URL
%	to return to.
%
%	After openid_verify/2 has redirected the   browser to the OpenID
%	server, and the OpenID server did   its  magic, it redirects the
%	browser back to this address.  The   work  is fairly trivial. If
%	=mode= is =cancel=, the OpenId server   denied. If =id_res=, the
%	OpenId server replied positive, but  we   must  verify  what the
%	server told us by checking the HMAC-SHA signature.
%
%	This call fails silently if their is no =|openid.mode|= field in
%	the request.
%
%	@throws	openid(cancel)
%		if request was cancelled by the OpenId server
%	@throws openid(signature_mismatch)
%		if the HMAC signature check failed

openid_authenticate(Request, OpenIdServer, Identity, ReturnTo) :-
	memberchk(method(get), Request),
	http_parameters(Request,
			[ 'openid.mode'(Mode, [optional(true)])
			]),
	(   var(Mode)
	->  fail
	;   Mode == cancel
	->  throw(openid(cancel))
	;   Mode == id_res
	->  debug(openid(authenticate), 'Mode=id_res, validating response', []),
	    http_parameters(Request,
			    [ 'openid.identity'(Identity, []),
			      'openid.assoc_handle'(Handle, []),
			      'openid.return_to'(ReturnTo, []),
			      'openid.signed'(AtomFields, []),
			      'openid.sig'(Base64Signature, []),
			      'openid.invalidate_handle'(Invalidate,
							 [optional(true)])
			    ],
			    [ form_data(Form)
			    ]),
	    atomic_list_concat(SignedFields, ',', AtomFields),
	    check_obligatory_fields(SignedFields),
	    signed_pairs(SignedFields,
			 [ mode-Mode,
			   identity-Identity,
			   assoc_handle-Handle,
			   return_to-ReturnTo,
			   invalidate_handle-Invalidate
			 ],
			 Form,
			 SignedPairs),
	    (	openid_associate(OpenIdServer, Handle, Assoc)
	    ->  signature(SignedPairs, Assoc, Sig),
		atom_codes(Base64Signature, Base64SigCodes),
		phrase(base64(Signature), Base64SigCodes),
		(   Sig == Signature
		->  true
		;   throw(openid(signature_mismatch))
		)
	    ;	check_authentication(Request, Form)
	    ),
	    ax_store(Form)
	).

%%	signed_pairs(+FieldNames, +Pairs:list(Field-Value),
%%		     +Form, -SignedPairs) is det.
%
%	Extract the signed field in the order they appear in FieldNames.

signed_pairs([], _, _, []).
signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
	memberchk(Field-Value, Pairs), !,
	signed_pairs(T0, Pairs, Form, T).
signed_pairs([Field|T0], Pairs, Form, [Field-Value|T]) :-
	atom_concat('openid.', Field, OpenIdField),
	memberchk(OpenIdField=Value, Form), !,
	signed_pairs(T0, Pairs, Form, T).
signed_pairs([Field|T0], Pairs, Form, T) :-
	format(user_error, 'Form = ~p~n', [Form]),
	throw(error(existence_error(field, Field),
		    context(_, 'OpenID Signed field is not present'))),
	signed_pairs(T0, Pairs, Form, T).


%%	check_obligatory_fields(+SignedFields:list) is det.
%
%	Verify fields from obligatory_field/1 are   in  the signed field
%	list.
%
%	@error	existence_error(field, Field)

check_obligatory_fields(Fields) :-
	(   obligatory_field(Field),
	    (	memberchk(Field, Fields)
	    ->	true
	    ;	throw(error(existence_error(field, Field),
			    context(_, 'OpenID field is not in signed fields')))
	    ),
	    fail
	;   true
	).

obligatory_field(identity).


%%	check_authentication(+Request, +Form) is semidet.
%
%	Implement the stateless verification method.   This seems needed
%	for stackexchange.com, which provides the   =res_id=  with a new
%	association handle.

check_authentication(_Request, Form) :-
	openid_server(_OpenIDLogin, _OpenID, Server),
	debug(openid(check_authentication),
	      'Using stateless verification with ~q form~n~q', [Server, Form]),
	select('openid.mode' = _, Form, Form1),
	setup_call_cleanup(
	    http_open(Server, In,
		      [ post(form([ 'openid.mode' = check_authentication
				  | Form1
				  ])),
			cert_verify_hook(ssl_verify)
		      ]),
	    read_stream_to_codes(In, Reply),
	    close(In)),
	debug(openid(check_authentication),
	      'Reply: ~n~s~n', [Reply]),
	key_values_data(Pairs, Reply),
	forall(member(invalidate_handle-Handle, Pairs),
	       retractall(association(_, Handle, _))),
	memberchk(is_valid-true, Pairs).


		 /*******************************
		 *	    AX HANDLING		*
		 *******************************/

%%	ax_options(+ServerOptions, +Options, +AXAttrs) is det.
%
%	True when AXAttrs is a  list   of  additional attribute exchange
%	options to add to the OpenID redirect request.

ax_options(ServerOptions, Options, AXAttrs) :-
	option(ax(Spec), Options),
	option(xrds_types(Types), ServerOptions),
	memberchk('http://openid.net/srv/ax/1.0', Types), !,
	http_ax_attributes(Spec, AXAttrs),
	debug(openid(ax), 'AX attributes: ~q', [AXAttrs]).
ax_options(_, _, []) :-
	debug(openid(ax), 'AX: not supported', []).

%%	ax_store(+Form)
%
%	Extract reported AX data and  store   this  into the session. If
%	there is a non-empty list of exchanged values, this calls
%
%	    openid_hook(ax(Values))
%
%	If this hook fails, Values are added   to the session data using
%	http_session_assert(ax(Values)).

ax_store(Form) :-
	debug(openid(ax), 'Form: ~q', [Form]),
	ax_form_attributes(Form, Values),
	debug(openid(ax), 'AX: ~q', [Values]),
	(   Values \== []
	->  (   openid_hook(ax(Values))
	    ->  true
	    ;   http_session_assert(ax(Values))
	    )
	;   true
	).


		 /*******************************
		 *	   OPENID SERVER	*
		 *******************************/

:- dynamic
	server_association/3.		% URL, Handle, Term

%%	openid_server(+Options, +Request)
%
%	Realise the OpenID server. The protocol   demands a POST request
%	here.

openid_server(Options, Request) :-
	http_parameters(Request,
			[ 'openid.mode'(Mode)
			],
			[ attribute_declarations(openid_attribute),
			  form_data(Form)
			]),
	(   Mode == associate
	->  associate_server(Request, Form, Options)
	;   Mode == checkid_setup
	->  checkid_setup_server(Request, Form, Options)
	).

%%	associate_server(+Request, +Form, +Options)
%
%	Handle the association-request. If successful,   create a clause
%	for server_association/3 to record the current association.

associate_server(Request, Form, Options) :-
	memberchk('openid.assoc_type'	      =	AssocType,   Form),
	memberchk('openid.session_type'	      =	SessionType, Form),
	memberchk('openid.dh_modulus'	      =	P64,	     Form),
	memberchk('openid.dh_gen'	      =	G64,	     Form),
	memberchk('openid.dh_consumer_public' =	CPX64,       Form),
	base64_btwoc(P, P64),
	base64_btwoc(G, G64),
	base64_btwoc(CPX, CPX64),
	Y is 1+random(P-1),		% Our secret
	DiffieHellman is powm(CPX, Y, P),
	btwoc(DiffieHellman, DHBytes),
	signature_algorithm(SessionType, SHA_Algo),
	sha_hash(DHBytes, SHA1, [encoding(octet), algorithm(SHA_Algo)]),
	CPY is powm(G, Y, P),
	base64_btwoc(CPY, CPY64),
	mackey_bytes(SessionType, MacBytes),
	new_assoc_handle(MacBytes, Handle),
	random_bytes(MacBytes, MacKey),
	xor_codes(MacKey, SHA1, EncKey),
	phrase(base64(EncKey), Base64EncKey),
	DefExpriresIn is 24*3600,
	option(expires_in(ExpriresIn), Options, DefExpriresIn),

	get_time(Now),
	ExpiresAt is integer(Now+ExpriresIn),
	make_association([ session_type(SessionType),
			   expires_at(ExpiresAt),
			   mac_key(MacKey)
			 ],
			 Record),
	memberchk(peer(Peer), Request),
	assert(server_association(Peer, Handle, Record)),

	key_values_data([ assoc_type-AssocType,
			  assoc_handle-Handle,
			  expires_in-ExpriresIn,
			  session_type-SessionType,
			  dh_server_public-CPY64,
			  enc_mac_key-Base64EncKey
			],
			Text),
	format('Content-type: text/plain~n~n~s', [Text]).

mackey_bytes('DH-SHA1',   20).
mackey_bytes('DH-SHA256', 32).

new_assoc_handle(Length, Handle) :-
	random_bytes(Length, Bytes),
	phrase(base64(Bytes), HandleCodes),
	atom_codes(Handle, HandleCodes).


%%	checkid_setup_server(+Request, +Form, +Options)
%
%	Validate an OpenID for a TrustRoot and redirect the browser back
%	to the ReturnTo argument.  There   are  many  possible scenarios
%	here:
%
%		1. Check some cookie and if present, grant immediately
%		2. Use a 401 challenge page
%		3. Present a normal grant/password page
%		4. As (3), but use HTTPS for the exchange
%		5. etc.
%
%	First thing to check is the immediate acknowledgement.

checkid_setup_server(_Request, Form, _Options) :-
	memberchk('openid.identity'	= Identity,  Form),
	memberchk('openid.assoc_handle'	= Handle,    Form),
	memberchk('openid.return_to'	= ReturnTo,  Form),
	memberchk('openid.trust_root'	= TrustRoot, Form),

	server_association(_, Handle, _Association),		% check

	reply_html_page(
	    [ title('OpenID login')
	    ],
	    [ \openid_title,
	      div(class('openid-message'),
		  ['Site ', a(href(TrustRoot), TrustRoot),
		   ' requests permission to login with OpenID ',
		   a(href(Identity), Identity), '.'
		  ]),
	      table(class('openid-form'),
		    [ tr(td(form([ action(grant), method('GET') ],
				 [ \hidden('openid.grant', yes),
				   \hidden('openid.identity', Identity),
				   \hidden('openid.assoc_handle', Handle),
				   \hidden('openid.return_to', ReturnTo),
				   \hidden('openid.trust_root', TrustRoot),
				   div(['Password: ',
					input([ type(password),
						name('openid.password')
					      ]),
					input([ type(submit),
						value('Grant')
					      ])
				       ])
				 ]))),
		      tr(td(align(right),
			    form([ action(grant), method('GET') ],
				 [ \hidden('openid.grant', no),
				   \hidden('openid.return_to', ReturnTo),
				   input([type(submit), value('Deny')])
				 ])))
		    ])
	    ]).

hidden(Name, Value) -->
	html(input([type(hidden), id(return_to), name(Name), value(Value)])).


openid_title -->
	{ http_absolute_location(icons('openid-logo-square.png'), SRC, []) },
	html_requires(css('openid.css')),
	html(div(class('openid-title'),
		 [ a(href('http://openid.net/'),
		     img([ src(SRC), alt('OpenID') ])),
		   span('Login')
		 ])).


%%	openid_grant(+Request)
%
%	Handle the reply from checkid_setup_server/3.   If  the reply is
%	=yes=, check the authority (typically the   password) and if all
%	looks good redirect the browser to   ReturnTo, adding the OpenID
%	properties needed by the Relying Party to verify the login.

openid_grant(Request) :-
	http_parameters(Request,
			[ 'openid.grant'(Grant),
			  'openid.return_to'(ReturnTo)
			],
			[ attribute_declarations(openid_attribute)
			]),
	(   Grant == yes
	->  http_parameters(Request,
			    [ 'openid.identity'(Identity),
			      'openid.assoc_handle'(Handle),
			      'openid.trust_root'(TrustRoot),
			      'openid.password'(Password)
			    ],
			    [ attribute_declarations(openid_attribute)
			    ]),
	    server_association(_, Handle, Association),
	    grant_login(Request,
			[ identity(Identity),
			  password(Password),
			  trustroot(TrustRoot)
			]),
	    SignedPairs = [ 'mode'-id_res,
			    'identity'-Identity,
			    'assoc_handle'-Handle,
			    'return_to'-ReturnTo
			  ],
	    signed_fields(SignedPairs, Signed),
	    signature(SignedPairs, Association, Signature),
	    phrase(base64(Signature), Bas64Sig),
	    redirect_browser(ReturnTo,
			     [ 'openid.mode' = id_res,
			       'openid.identity' = Identity,
			       'openid.assoc_handle' = Handle,
			       'openid.return_to' = ReturnTo,
			       'openid.signed' = Signed,
			       'openid.sig' = Bas64Sig
			     ])
	;   redirect_browser(ReturnTo,
			     [ 'openid.mode' = cancel
			     ])
	).


%%	grant_login(+Request, +Options) is det.
%
%	Validate login from Request (can  be   used  to get cookies) and
%	Options, which contains at least:
%
%		* identity(Identity)
%		* password(Password)
%		* trustroot(TrustRoot)

grant_login(Request, Options) :-
	openid_hook(grant(Request, Options)).

%%	trusted(+OpenID, +Server)
%
%	True if we  trust  the  given   OpenID  server.  Must  throw  an
%	exception, possibly redirecting to a   page with trusted servers
%	if the given server is not trusted.

trusted(OpenID, Server) :-
	openid_hook(trusted(OpenID, Server)).


%%	signed_fields(+Pairs, -Signed) is det.
%
%	Create a comma-separated  atom  from   the  field-names  without
%	'openid.' from Pairs.

signed_fields(Pairs, Signed) :-
	signed_field_names(Pairs, Names),
	atomic_list_concat(Names, ',', Signed).

signed_field_names([], []).
signed_field_names([H0-_|T0], [H|T]) :-
	(   atom_concat('openid.', H, H0)
	->  true
	;   H = H0
	),
	signed_field_names(T0, T).

%%	signature(+Pairs, +Association, -Signature)
%
%	Determine the signature for Pairs

signature(Pairs, Association, Signature) :-
	key_values_data(Pairs, TokenContents),
	association_mac_key(Association, MacKey),
	association_session_type(Association, SessionType),
	signature_algorithm(SessionType, SHA),
	hmac_sha(MacKey, TokenContents, Signature, [algorithm(SHA)]),
	debug(openid(crypt),
	      'Signed:~n~s~nSignature: ~w', [TokenContents, Signature]).

signature_algorithm('DH-SHA1',   sha1).
signature_algorithm('DH-SHA256', sha256).


		 /*******************************
		 *	      ASSOCIATE		*
		 *******************************/

:- dynamic
	association/3.			% URL, Handle, Data

:- record
	association(session_type='DH-SHA1',
		    expires_at,		% time-stamp
		    mac_key).		% code-list

%%	openid_associate(?URL, ?Handle, ?Assoc) is det.
%
%	Calls openid_associate/4 as
%
%	    ==
%	    openid_associate(URL, Handle, Assoc, []).
%	    ==

openid_associate(URL, Handle, Assoc) :-
	openid_associate(URL, Handle, Assoc, []).

%%	openid_associate(+URL, -Handle, -Assoc, +Options) is det.
%%	openid_associate(?URL, +Handle, -Assoc, +Options) is semidet.
%
%	Associate with an open-id server.  We   first  check for a still
%	valid old association. If there is  none   or  it is expired, we
%	esstablish one and remember it.  Options:
%
%	  * ns(URL)
%	  One of =http://specs.openid.net/auth/2.0= (default) or
%	  =http://openid.net/signon/1.1=.
%
%	@tbd	Should we store known associations permanently?  Where?

openid_associate(URL, Handle, Assoc, _Options) :-
	nonvar(Handle), !,
	debug(openid(associate),
	      'OpenID: Lookup association with handle ~q', [Handle]),
	(   association(URL, Handle, Assoc)
	->  true
	;   debug(openid(associate),
		  'OpenID: no association with handle ~q', [Handle]),
	    fail
	).
openid_associate(URL, Handle, Assoc, _Options) :-
	must_be(atom, URL),
	association(URL, Handle, Assoc),
	association_expires_at(Assoc, Expires),
	get_time(Now),
	(   Now < Expires
	->  !,
	    debug(openid(associate),
		  'OpenID: Reusing association with ~q', [URL])
	;   retractall(association(URL, Handle, _)),
	    fail
	).
openid_associate(URL, Handle, Assoc, Options) :-
	associate_data(Data, P, _G, X, Options),
	debug(openid(associate), 'OpenID: Associating with ~q', [URL]),
	setup_call_cleanup(
	    http_open(URL, In,
		      [ post(form(Data)),
			cert_verify_hook(ssl_verify)
		      ]),
	    read_stream_to_codes(In, Reply),
	    close(In)),
	debug(openid(associate), 'Reply: ~n~s', [Reply]),
	key_values_data(Pairs, Reply),
	shared_secret(Pairs, P, X, MacKey),
	expires_at(Pairs, ExpiresAt),
	memberchk(assoc_handle-Handle, Pairs),
	memberchk(session_type-Type, Pairs),
	make_association([ session_type(Type),
			   expires_at(ExpiresAt),
			   mac_key(MacKey)
			 ], Assoc),
	assert(association(URL, Handle, Assoc)).


%%	shared_secret(+Pairs, +P, +X, -Secret:list(codes))
%
%	Find the shared secret from the peer's reply and our data. First
%	clause deals with the (deprecated) non-encoded version.

shared_secret(Pairs, _, _, Secret) :-
	memberchk(mac_key-Base64, Pairs), !,
	atom_codes(Base64, Base64Codes),
	phrase(base64(Base64Codes), Secret).
shared_secret(Pairs, P, X, Secret) :-
	memberchk(dh_server_public-Base64Public, Pairs),
	memberchk(enc_mac_key-Base64EncMacKey, Pairs),
	memberchk(session_type-SessionType, Pairs),
	base64_btwoc(ServerPublic, Base64Public),
	DiffieHellman is powm(ServerPublic, X, P),
	atom_codes(Base64EncMacKey, Base64EncMacKeyCodes),
	phrase(base64(EncMacKey), Base64EncMacKeyCodes),
	btwoc(DiffieHellman, DiffieHellmanBytes),
	signature_algorithm(SessionType, SHA_Algo),
	sha_hash(DiffieHellmanBytes, DHHash,
		 [encoding(octet), algorithm(SHA_Algo)]),
	xor_codes(DHHash, EncMacKey, Secret).


%%	expires_at(+Pairs, -Time) is det.
%
%	Unify Time with  a  time-stamp   stating  when  the  association
%	exires.

expires_at(Pairs, Time) :-
	memberchk(expires_in-ExpAtom, Pairs),
	atom_number(ExpAtom, Seconds),
	get_time(Now),
	Time is integer(Now)+Seconds.


%%	associate_data(-Data, -P, -G, -X, +Options) is det.
%
%	Generate the data to initiate an association using Diffie-Hellman
%	shared secret key negotiation.

associate_data(Data, P, G, X, Options) :-
	openid_dh_p(P),
	openid_dh_g(G),
	X is 1+random(P-1),			% 1<=X<P-1
	CP is powm(G, X, P),
	base64_btwoc(P, P64),
	base64_btwoc(G, G64),
	base64_btwoc(CP, CP64),
	option(ns(NS), Options, 'http://specs.openid.net/auth/2.0'),
	(   assoc_type(NS, DefAssocType, DefSessionType)
	->  true
	;   domain_error('openid.ns', NS)
	),
	option(assoc_type(AssocType), Options, DefAssocType),
	option(assoc_type(SessionType), Options, DefSessionType),
	Data = [ 'openid.ns'		     = NS,
		 'openid.mode'		     = associate,
		 'openid.assoc_type'	     = AssocType,
		 'openid.session_type'	     = SessionType,
		 'openid.dh_modulus'	     = P64,
		 'openid.dh_gen'	     = G64,
		 'openid.dh_consumer_public' = CP64
	       ].

assoc_type('http://specs.openid.net/auth/2.0',
	   'HMAC-SHA256',
	   'DH-SHA256').
assoc_type('http://openid.net/signon/1.1',
	   'HMAC-SHA1',
	   'DH-SHA1').


		 /*******************************
		 *	      RANDOM		*
		 *******************************/

%%	random_bytes(+N, -Bytes) is det.
%
%	Bytes is a list of N random bytes (integers 0..255).

random_bytes(N, [H|T]) :-
	N > 0, !,
	H is random(256),
	N2 is N - 1,
	random_bytes(N2, T).
random_bytes(_, []).


		 /*******************************
		 *	     CONSTANTS		*
		 *******************************/

openid_dh_p(155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443).

openid_dh_g(2).


		 /*******************************
		 *	       UTIL		*
		 *******************************/

%%	key_values_data(+KeyValues:list(Key-Value), -Data:list(code)) is det.
%%	key_values_data(-KeyValues:list(Key-Value), +Data:list(code)) is det.
%
%	Encoding  and  decoding  of  key-value  pairs  for  OpenID  POST
%	messages  according  to   Appendix   C    of   the   OpenID  1.1
%	specification.

key_values_data(Pairs, Data) :-
	nonvar(Data), !,
	phrase(data_form(Pairs), Data).
key_values_data(Pairs, Data) :-
	phrase(gen_data_form(Pairs), Data).

data_form([Key-Value|Pairs]) -->
	utf8_string(KeyCodes), ":", utf8_string(ValueCodes), "\n", !,
	{ atom_codes(Key, KeyCodes),
	  atom_codes(Value, ValueCodes)
	},
	data_form(Pairs).
data_form([]) -->
	ws.

%%	utf8_string(-Codes)// is nondet.
%
%	Take a short UTF-8 code-list from input. Extend on backtracking.

utf8_string([]) -->
	[].
utf8_string([H|T]) -->
	utf8_codes([H]),
	utf8_string(T).

ws -->
	[C],
	{ C =< 32 }, !,
	ws.
ws -->
	[].


gen_data_form([]) -->
	[].
gen_data_form([Key-Value|T]) -->
	field(Key), ":", field(Value), "\n",
	gen_data_form(T).

field(Field) -->
	{ to_codes(Field, Codes)
	},
	utf8_codes(Codes).

to_codes(Codes, Codes) :-
	is_list(Codes), !.
to_codes(Atomic, Codes) :-
	atom_codes(Atomic, Codes).

%%	base64_btwoc(+Int, -Base64:list(code)) is det.
%%	base64_btwoc(-Int, +Base64:list(code)) is det.
%%	base64_btwoc(-Int, +Base64:atom) is det.

base64_btwoc(Int, Base64) :-
	integer(Int), !,
	btwoc(Int, Bytes),
	phrase(base64(Bytes), Base64).
base64_btwoc(Int, Base64) :-
	atom(Base64), !,
	atom_codes(Base64, Codes),
	phrase(base64(Bytes), Codes),
	btwoc(Int, Bytes).
base64_btwoc(Int, Base64) :-
	phrase(base64(Bytes), Base64),
	btwoc(Int, Bytes).


%%	btwoc(+Integer, -Bytes) is det.
%%	btwoc(-Integer, +Bytes) is det.
%
%	Translate between a big integer and and its representation in
%	bytes.  The first bit is always 0, as Integer is nonneg.

btwoc(Int, Bytes) :-
	integer(Int), !,
	int_to_bytes(Int, Bytes).
btwoc(Int, Bytes) :-
	is_list(Bytes),
	bytes_to_int(Bytes, Int).

int_to_bytes(Int, Bytes) :-
	int_to_bytes(Int, [], Bytes).

int_to_bytes(Int, Bytes0, [Int|Bytes0]) :-
	Int < 128, !.
int_to_bytes(Int, Bytes0, Bytes) :-
	Last is Int /\ 0xff,
	Int1 is Int >> 8,
	int_to_bytes(Int1, [Last|Bytes0], Bytes).


bytes_to_int([B|T], Int) :-
	bytes_to_int(T, B, Int).

bytes_to_int([], Int, Int).
bytes_to_int([B|T], Int0, Int) :-
	Int1 is (Int0<<8)+B,
	bytes_to_int(T, Int1, Int).


%%	xor_codes(+C1:list(int), +C2:list(int), -XOR:list(int)) is det.
%
%	Compute xor of two strings.
%
%	@error	length_mismatch(L1, L2) if the two lists do not have equal
%		length.

xor_codes([], [], []) :- !.
xor_codes([H1|T1], [H2|T2], [H|T]) :- !,
	H is H1 xor H2, !,
	xor_codes(T1, T2, T).
xor_codes(L1, L2, _) :-
	throw(error(length_mismatch(L1, L2), _)).


		 /*******************************
		 *	  HTTP ATTRIBUTES	*
		 *******************************/

openid_attribute('openid.mode',
		 [ oneof([ associate,
			   checkid_setup,
			   cancel,
			   id_res
			 ])
		 ]).
openid_attribute('openid.assoc_type',
		 [ oneof(['HMAC-SHA1'])
		 ]).
openid_attribute('openid.session_type',
		 [ oneof([ 'DH-SHA1',
			   'DH-SHA256'
			 ])
		 ]).
openid_attribute('openid.dh_modulus',	      [length > 1]).
openid_attribute('openid.dh_gen',	      [length > 1]).
openid_attribute('openid.dh_consumer_public', [length > 1]).
openid_attribute('openid.assoc_handle',	      [length > 1]).
openid_attribute('openid.return_to',	      [length > 1]).
openid_attribute('openid.trust_root',	      [length > 1]).
openid_attribute('openid.identity',	      [length > 1]).
openid_attribute('openid.password',	      [length > 1]).
openid_attribute('openid.grant',	      [oneof([yes,no])]).