1/* Part of SWI-Prolog 2 3 Author: Jan Wielemaker 4 E-mail: J.Wielemaker@vu.nl 5 WWW: http://www.swi-prolog.org 6 Copyright (c) 2007-2016, University of Amsterdam 7 VU University Amsterdam 8 All rights reserved. 9 10 Redistribution and use in source and binary forms, with or without 11 modification, are permitted provided that the following conditions 12 are met: 13 14 1. Redistributions of source code must retain the above copyright 15 notice, this list of conditions and the following disclaimer. 16 17 2. Redistributions in binary form must reproduce the above copyright 18 notice, this list of conditions and the following disclaimer in 19 the documentation and/or other materials provided with the 20 distribution. 21 22 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 POSSIBILITY OF SUCH DAMAGE. 34*/ 35 36:- module(http_ssl_plugin, []). 37:- use_module(library(ssl)). 38:- use_module(library(socket)). 39:- use_module(library(debug)). 40:- use_module(library(option)). 41:- use_module(library(http/thread_httpd)). 42:- use_module(library(http/http_header)). 43 44/** <module> SSL plugin for HTTP libraries 45 46This module can be loaded next to library(thread_httpd) and 47library(http_open) to provide secure HTTP (HTTPS) services and client 48access. 49 50An example secure server using self-signed certificates can be found in 51the <plbase>/doc/packages/examples/ssl/https.pl, where <plbase> is the 52SWI-Prolog installation directory. 53*/ 54 55:- multifile 56 thread_httpd:make_socket_hook/3, 57 thread_httpd:accept_hook/2, 58 thread_httpd:open_client_hook/6, 59 http:http_protocol_hook/5, 60 http:open_options/2, 61 http:http_connection_over_proxy/6, 62 http:ssl_server_create_hook/3, 63 http:ssl_server_open_client_hook/3. 64 65 66 /******************************* 67 * SERVER HOOKS * 68 *******************************/ 69 70%! thread_httpd:make_socket_hook(?Port, :OptionsIn, -OptionsOut) 71%! is semidet. 72% 73% Hook into http_server/2 to create an SSL server if the option 74% ssl(SSLOptions) is provided. 75% 76% @see thread_httpd:accept_hook/2 handles the corresponding accept 77 78thread_httpdmake_socket_hook(Port, M:Options0, Options) :- 79 select(ssl(SSLOptions0), Options0, Options1), 80 !, 81 add_secure_ciphers(SSLOptions0, SSLOptions1), 82 disable_sslv3(SSLOptions1, SSLOptions), 83 make_socket(Port, Socket, Options1), 84 ssl_context(server, 85 SSL0, 86 M:[ close_parent(true) 87 | SSLOptions 88 ]), 89 ( http:ssl_server_create_hook(SSL0, SSL, Options1) 90 -> true 91 ; SSL = SSL0 92 ), 93 atom_concat('httpsd', Port, Queue), 94 Options = [ queue(Queue), 95 tcp_socket(Socket), 96 ssl_instance(SSL) 97 | Options1 98 ]. 99 100%! add_secure_ciphers(+SSLOptions0, -SSLOptions) 101% 102% Add ciphers from ssl_secure_ciphers/1 if no ciphers are provided. 103 104add_secure_ciphers(SSLOptions0, SSLOptions) :- 105 ( option(cipher_list(_), SSLOptions0) 106 -> SSLOptions = SSLOptions0 107 ; ssl_secure_ciphers(Ciphers), 108 SSLOptions = [cipher_list(Ciphers)|SSLOptions0] 109 ). 110 111%! disable_sslv3(+SSLOptions0, -SSLOptions) 112% 113% Disable SSLv3, which is considered insecure unless the caller 114% specifies the allowed versions explicitly, so we assume s/he knows 115% what s/he is doing. 116 117disable_sslv3(SSLOptions0, SSLOptions) :- 118 ( option(min_protocol_version(_), SSLOptions0) 119 ; option(disable_ssl_methods(_), SSLOptions0) 120 ), 121 !, 122 SSLOptions = SSLOptions0. 123disable_sslv3(SSLOptions0, 124 [ disable_ssl_methods([sslv3,sslv23]), % old OpenSSL versions 125 min_protocol_version(tlsv1) % OpenSSL 1.1.0 and later 126 | SSLOptions0 127 ]). 128 129 130make_socket(_Port, Socket, Options) :- 131 option(tcp_socket(Socket), Options), 132 !. 133make_socket(Port, Socket, _Options) :- 134 tcp_socket(Socket), 135 tcp_setopt(Socket, reuseaddr), 136 tcp_bind(Socket, Port), 137 tcp_listen(Socket, 5). 138 139 140%! thread_httpd:accept_hook(:Goal, +Options) is semidet. 141% 142% Implement the accept for HTTPS connections. 143 144thread_httpdaccept_hook(Goal, Options) :- 145 memberchk(ssl_instance(SSL), Options), 146 !, 147 memberchk(queue(Queue), Options), 148 memberchk(tcp_socket(Socket), Options), 149 tcp_accept(Socket, Client, Peer), 150 debug(http(connection), 'New HTTPS connection from ~p', [Peer]), 151 http_enough_workers(Queue, accept, Peer), 152 thread_send_message(Queue, ssl_client(SSL, Client, Goal, Peer)). 153 154%! http:ssl_server_create_hook(+SSL0, -SSL, +Options) is semidet. 155% 156% Extensible predicate that is called once after creating an HTTPS 157% server. If this predicate succeeds, SSL is the context that is used 158% for negotiating new connections. Otherwise, SSL0 is used, which is 159% the context that was created with the given options. 160% 161% @see ssl_context/3 for creating an SSL context 162 163 164%! http:ssl_server_open_client_hook(+SSL0, -SSL, +Options) is semidet. 165% 166% Extensible predicate that is called before each connection that the 167% server negotiates with a client. If this predicate succeeds, SSL is 168% the context that is used for the new connection. Otherwise, SSL0 is 169% used, which is the context that was created when launching the 170% server. 171% 172% @see ssl_context/3 for creating an SSL context 173 174 175thread_httpdopen_client_hook(ssl_client(SSL0, Client, Goal, Peer), 176 Goal, In, Out, 177 [peer(Peer), protocol(https)], 178 Options) :- 179 ( http:ssl_server_open_client_hook(SSL0, SSL, Options) 180 -> true 181 ; SSL = SSL0 182 ), 183 option(timeout(TMO), Options, 60), 184 tcp_open_socket(Client, Read, Write), 185 set_stream(Read, timeout(TMO)), 186 set_stream(Write, timeout(TMO)), 187 catch(ssl_negotiate(SSL, Read, Write, In, Out), 188 E, 189 ssl_failed(Read, Write, E)). 190 191ssl_failed(Read, Write, E) :- 192 close(Write, [force(true)]), 193 close(Read, [force(true)]), 194 throw(E). 195 196 197 /******************************* 198 * CLIENT HOOKS * 199 *******************************/ 200 201%! http:http_protocol_hook(+Scheme, +Parts, +PlainStreamPair, 202%! -StreamPair, +Options) is semidet. 203% 204% Hook for http_open/3 to connect to an HTTPS (SSL-based HTTP) 205% server. This plugin also passes the default option 206% `cacert_file(system(root_certificates))` to ssl_context/3. 207 208httphttp_protocol_hook(https, Parts, PlainStreamPair, StreamPair, Options) :- 209 ssl_protocol_hook(Parts, PlainStreamPair, StreamPair, Options). 210httphttp_protocol_hook(wss, Parts, PlainStreamPair, StreamPair, Options) :- 211 ssl_protocol_hook(Parts, PlainStreamPair, StreamPair, Options). 212 213ssl_protocol_hook(Parts, PlainStreamPair, StreamPair, Options) :- 214 memberchk(host(Host), Parts), 215 ssl_context(client, SSL, [ host(Host), 216 close_parent(true) 217 | Options 218 ]), 219 stream_pair(PlainStreamPair, PlainIn, PlainOut), 220 % if an exception arises, http_open/3 closes the stream for us 221 ssl_negotiate(SSL, PlainIn, PlainOut, In, Out), 222 stream_pair(StreamPair, In, Out). 223 224%! http:open_options(Parts, Options) is nondet. 225% 226% Implementation of the multifile hook http:open_options/2 used by 227% library(http/http_open). By default, we use the system trusted 228% root certificate database for validating an SSL certificate. 229 230httpopen_options(Parts, Options) :- 231 memberchk(scheme(S), Parts), 232 ssl_scheme(S), 233 Options = [cacert_file(system(root_certificates))]. 234 235ssl_scheme(https). 236ssl_scheme(wss). 237 238%! http:http_connection_over_proxy(+Proxy, +Parts, +HostPort, -StreamPair, 239%! +OptionsIn, -OptionsOut) 240% 241% Facilitate an HTTPS connection via a proxy using HTTP CONNECT. 242% Note that most proxies will only support this for connecting on 243% port 443 244 245httphttp_connection_over_proxy(proxy(ProxyHost, ProxyPort), Parts, 246 Host:Port, StreamPair, Options, Options) :- 247 memberchk(scheme(https), Parts), 248 !, 249 tcp_connect(ProxyHost:ProxyPort, StreamPair, [bypass_proxy(true)]), 250 catch(negotiate_http_connect(StreamPair, Host:Port), 251 Error, 252 ( close(StreamPair, [force(true)]), 253 throw(Error) 254 )). 255 256negotiate_http_connect(StreamPair, Address):- 257 format(StreamPair, 'CONNECT ~w HTTP/1.1\r\n\r\n', [Address]), 258 flush_output(StreamPair), 259 http_read_reply_header(StreamPair, Header), 260 memberchk(status(_, Status, Message), Header), 261 ( Status == ok 262 -> true 263 ; throw(error(proxy_rejection(Message), _)) 264 )