changeset 53:c7720fefea26

Added helpers and info about building SWI Prolog
author daniel
date Thu, 14 May 2015 15:35:04 +0100
parents 9f461581a518
children 6fea6e085b81
files LICENSES etc/INSTALL src/swipl/README.md src/swipl/build src/swipl/http_openid.pl
diffstat 5 files changed, 1765 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/LICENSES	Thu May 14 15:18:47 2015 +0100
+++ b/LICENSES	Thu May 14 15:35:04 2015 +0100
@@ -5,6 +5,7 @@
 
 .vim/bundle/vim-commentary/ - Copyright Tim Pope, VIM license
 .vim/syntax/humdrum.vim     - Craig Sapp, 2000
+src/swipl/http_openid.pl    - Patched version, from SWI Prolog (LGPL)
 
 This repository contains no other code to speak of, apart from a some
 trivial bash scripts in bin/ and a couple of install scripts under etc/.
--- a/etc/INSTALL	Thu May 14 15:18:47 2015 +0100
+++ b/etc/INSTALL	Thu May 14 15:35:04 2015 +0100
@@ -25,3 +25,4 @@
 Finally, to make score sonification work, you'll need some soundfonts in
 ~/lib/sounds/sf2. Better instructions to follow at a later data.
 
+See src/swipl/README.md for more information about keeping SWI Prolog updated.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/swipl/README.md	Thu May 14 15:35:04 2015 +0100
@@ -0,0 +1,36 @@
+# To build a new SWI Prolog:
+
+1. Download the source archive.
+2. Unpack it here. This will result in a new directory which
+	I will refer to as <new source root>.
+3. Copy http_openid.pl to <new source root>/packages/http/
+4. Copy build into <new source root>
+5. CD into <new source root> and build.
+6. Give SUDO password when prompted to install into /usr/local
+7. Start SWIPL and rebuild any packages that have foreign libraries
+
+Eg, for the latest version as of today (14/5/2015) 7.3.0, I did:
+
+	$ wget http://www.swi-prolog.org/download/devel/src/swipl-7.3.0.tar.gz
+	$ tar xzf swipl-7.3.0.tar.gz
+
+Alternatively, if you want to be cool, try
+
+	$ curl http://www.swi-prolog.org/download/devel/src/swipl-7.3.0.tar.gz | tar xz
+
+Then
+
+	$ cd swipl-7.3.0
+	$ cp -p ../http_openid.pl packages/http
+	$ cp -p ../build .
+	$ ./build
+	$ swipl
+
+	?- maplist(pack_rebuild,[real,prosqlite,plml]).
+	?- halt.
+
+	$
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/swipl/build	Thu May 14 15:35:04 2015 +0100
@@ -0,0 +1,232 @@
+#!/usr/bin/env bash
+#
+# This is the script we use to   build  SWI-Prolog and all its packages.
+# Copy the script to  `build',  edit   to  suit  the  local installation
+# requirements and run it. Once correct, upgrading   to a new release is
+# now limited to getting the new sources and run ./build.
+
+# [EDIT] Prefix location of the installation. It is _not_ adviced to use
+# a    versioned    prefix.    The    system      will     install    in
+# $PREFIX/lib/pl-<version> and create symlinks from  $PREFIX/bin for the
+# main  programs.  Users  can  always  use  older  versions  by  running
+# $PREFIX/lib/pl-<version>/bin/<arch>/pl
+#
+# If you change PREFIX such that the system  is installed in a place for
+# which you have no write access, set  SUDO   to  the command to run the
+# remainder of the commandline as privilaged   user. E.g., if you change
+# PREFIX to /usr/local you typically must change SUDO to "sudo"
+
+PREFIX=/usr/local
+SUDO="sudo"
+
+# [EDIT] Version of make to use.  This   must  be GNU-make. On many Unix
+# systems this is installed as 'gmake'. On most GNU-based systems (e.g.,
+# linux), the default make is GNU-make.  You can use 'make --jobs=<max>'
+# to build the system faster using  all   your  cores. The optimal value
+# depends a lot on your hardware. Using   4 jobs on a dual-core machines
+# seems enough to keep both cores busy.
+
+MAKE=make
+# MAKE='make --jobs=4'
+
+# [EDIT] Compiler and options.
+#
+#	CC:	 Which C-compiler to use
+#	COFLAGS: Flags for the optimizer such as "-O3" or "-g"
+#	CMFLAGS: Machine flags such as "-m64" (64-bits on gcc)
+#	CIFLAGS: Include-path such as "-I/opt/include"
+#	LDFLAGS: Link flags such as "-L/opt/lib"
+#
+# Leaving an option blank leaves the  choice to configure. The commented
+# values below enable much better C-level debugging with almost the same
+# performance on GCC based systems (the default is to compile using -O3)
+# For optiomal performance, see also --enable-useprofile below.
+
+# export CC=
+# export COFLAGS="-O2 -gdwarf-2 -g3"
+# export CMFLAGS=
+# export CIFLAGS=
+# export LDFLAGS="-O2 -g"
+
+# On MacOS you need this to  get   some  libraries  from Macports. Since
+# recently, there are three C compilers:   gcc-llvm  and clang come with
+# XCode. Native gcc can  be  installed   in  various  ways  (e.g., using
+# Macports). Current SWI-Prolog sources compile and  work with all these
+# alternatives. If you want the last bit   of performance and don't mind
+# some extra work, get a recent  native   GCC,  set  CC below and enable
+# --enable-useprofile (see below).
+#
+# Recent versions of Quartz  (X11)  seem   to  install  the headers into
+# /opt/X11/include. We put this  after   /opt/local/include,  to use the
+# Macport version of X11 if this is installed.
+#
+# As of MacOS 10.9, Apple's  Java  does   not  include  the  headers for
+# linking to C. Therefore you  need  to   install  Oracle's  JDK and set
+# $JAVAPREFIX to the bin directory holding =java=.  We try to find it at
+# the most likely places below.
+
+if [ "`uname`" = Darwin ]; then
+  export LIBRARY_PATH=/usr/lib:/opt/local/lib
+  export CPATH=/usr/include:/opt/local/include:/opt/X11/include
+  export PKG_CONFIG_PATH=/usr/X11R6/lib/pkgconfig:/opt/local/lib/pkgconfig
+  if [ -f "$JAVA_HOME/bin/java" ]; then
+    export JAVAPREFIX="$JAVA_HOME/bin"
+  elif [ -f /Library/Java/Home/bin/java ]; then
+    export JAVAPREFIX=/Library/Java/Home/bin
+  elif [ -d /Library/Java/JavaVirtualMachines/*/Contents/Home/bin ]; then
+    export JAVAPREFIX="`echo /Library/Java/JavaVirtualMachines/*/Contents/Home/bin`"
+  fi
+  # export CC="gcc-4.2"
+  # export CXX="g++-4.2"
+  # export CXXCPP="g++-4.2 -E"
+fi
+
+# [EDIT] On Solaris also puts there stuff everywhere ...
+# export CIFLAGS=-I/opt/csw/include/ncurses
+# export LDFLAGS=-L/opt/csw/lib
+
+# [EDIT] On FreeBSD, java is installed under /usr/local/jdk<version>,
+# and the executables are _copied_ to /usr/local/bin. Unfortunately, the
+# copy leaves the headers out, so the original files must be used.
+# export JAVAC=/usr/local/jdk1.6.0/bin/javac
+
+# [EDIT] On FreeBSD, the following is needed to fetch the headers for
+# GMP.
+# export CIFLAGS='-I/usr/local/include'
+
+export CFLAGS="$COFLAGS $CMFLAGS $CIFLAGS"
+
+################################################################
+# Package (add-ons) selection
+################################################################
+
+# [EDIT] Packages to configure. Leaving it   blank  compiles all default
+# packages. The final set of packages is
+#
+#	${PKG-<default>} + $EXTRA_PKGS - $DISABLE_PKGS
+
+# export PKG=
+
+# [EDIT] Packages to skip.  Leaving it blank compiles all default packages.
+export DISABLE_PKGS="jasmine PDT R jpl"
+
+# [EDIT] Packages to add.
+# export EXTRA_PKGS="db ltx2htm space"
+
+# [EDIT] Where to find the jar for Junit 3.8.  Needed to test jpl
+# export JUNIT=/opt/local/share/java/junit.jar
+
+################################################################
+# Misc stuff
+################################################################
+
+# [EDIT] Extra options to pass to the toplevel configure.
+
+# --link
+# Using --link, the system is installed using symbolic links. This means
+# you cannot remove or clean  the   sources,  but  it largely simplifies
+# editing the system Prolog files during development.
+#
+# --enable-useprofile
+# The config --enable-useprofile exploits GCC  -fprofile-use option. The
+# system  is  compiled,  profiled   and    re-compiled   to  get  better
+# branch-prediction. This makes the system approx.   10%  faster. Do not
+# use this for developing the kernel because it complicates maintenance.
+#
+# --disable-libdirversion
+# By default, the system is  installed in $libdir/swipl-<version>. Using
+# this option drops <version>. Using versions,  you can install multiple
+# versions side-by-site and run old  versions   at  any time by starting
+# $libdir/swipl-<version>/bin/$arch/swipl. Without, the system is always
+# at a nice stable place, so external foreign objects linked against the
+# binary need not be updated with a Prolog update.
+#
+# --enable-shared
+# Use this to create a shared object  for the Prolog kernel. The default
+# depends on the platform. Creating a shared   object  is the default on
+# most platforms, either because it is  needed   or  because  it does no
+# harm. The only exception to this rule  is Linux on x86 (Intel 32-bit).
+# It is not needed on this platform  and Linux shared object model costs
+# a CPU register. Given the limited number   of CPU registers on the x86
+# platform, this results in a performance degradation of about 10%.
+
+# EXTRACFG+=" --link"
+# EXTRACFG+=" --enable-useprofile"
+# EXTRACFG+=" --disable-libdirversion"
+# EXTRACFG+=" --enable-shared"
+export EXTRACFG
+
+# One possiblity to make relocatable executables   on  Linux is by using
+# the RPATH mechanism. See  ld.so(1)   and  chrpath(1). However, chrpath
+# cannot enlarge the path. Uncommenting the   line below adds :xxx... to
+# the RPATH, where the given count is the number of x-s.
+#
+# export RPATH_RESERVE=70
+
+################################################################
+# No edit should be needed below this line
+################################################################
+
+V=`cat VERSION`
+config=true
+make=true
+install=true
+done=false
+setvars=false
+
+while test "$done" = false; do
+case "$1" in
+   --config)	make=false
+		install=false
+		shift
+		;;
+   --make)	config=false
+		install=false
+		shift
+		;;
+   --install)	config=false
+		make=false
+		shift
+		;;
+   --prefix=*)  PREFIX=`echo "$1" | sed 's/--prefix=//'`
+		shift
+		;;
+   --setvars)	setvars=true
+		shift
+		;;
+   *)		done=true
+		;;
+esac
+done
+
+if [ "$setvars" = "false" ]; then
+  rm -f packages/.failed.*
+
+  if [ "$config" = "true" ]; then
+    ./configure --prefix=$PREFIX --with-world $EXTRACFG $@ 2>&1 | tee configure.out
+    if [ "${PIPESTATUS[0]}" != 0 ]; then exit 1; fi
+  fi
+
+  if [ "$make" = "true" ]; then
+    $MAKE $@ 2>&1 | tee make.out
+    if [ "${PIPESTATUS[0]}" != 0 ]; then exit 1; fi
+  fi
+
+  if [ "$install" = "true" ]; then
+    $SUDO $MAKE install $@ 2>&1 | tee make-install.out
+    if [ "${PIPESTATUS[0]}" != 0 ]; then exit 1; fi
+  fi
+
+  if [ -z "$DESTDIR" ]; then
+    make check-installation
+  fi
+
+  # Parse build log for warnings that may indicate serious runtime issues
+  if [ "$make" = "true" ]; then
+    [ -f make.out ] && scripts/check_build_log.sh make.out
+  fi
+
+  # See whether any package failed to build
+  ./packages/report-failed || exit 1
+fi # setvars
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/swipl/http_openid.pl	Thu May 14 15:35:04 2015 +0100
@@ -0,0 +1,1495 @@
+/*  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])]).