hsts: add support for Strict-Transport-Security

- enable in the build (configure)
- header parsing
- host name lookup
- unit tests for the above
- CI build
- CURL_VERSION_HSTS bit
- curl_version_info support
- curl -V output
- curl-config --features
- CURLOPT_HSTS_CTRL
- man page for CURLOPT_HSTS_CTRL
- curl --hsts (sets CURLOPT_HSTS_CTRL and works with --libcurl)
- man page for --hsts
- save cache to disk
- load cache from disk
- CURLOPT_HSTS
- man page for CURLOPT_HSTS
- added docs/HSTS.md
- fixed --version docs
- adjusted curl_easy_duphandle

Closes #5896
This commit is contained in:
Daniel Stenberg 2020-11-02 23:17:01 +01:00
parent 9f43b28f78
commit 7385610d0c
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
38 changed files with 1122 additions and 21 deletions

View File

@ -4882,6 +4882,31 @@ AC_HELP_STRING([--disable-alt-svc],[Disable alt-svc support]),
AC_MSG_RESULT(no) AC_MSG_RESULT(no)
) )
dnl ************************************************************
dnl switch on/off hsts
dnl
curl_hsts_msg="no (--enable-hsts)";
AC_MSG_CHECKING([whether to support HSTS])
AC_ARG_ENABLE(hsts,
AC_HELP_STRING([--enable-hsts],[Enable HSTS support])
AC_HELP_STRING([--disable-hsts],[Disable HSTS support]),
[ case "$enableval" in
no)
AC_MSG_RESULT(no)
;;
*) AC_MSG_RESULT(yes)
curl_hsts_msg="enabled";
enable_hsts="yes"
;;
esac ],
AC_MSG_RESULT(no)
)
if test "$enable_hsts" = "yes"; then
AC_DEFINE(USE_HSTS, 1, [to enable HSTS])
experimental="$experimental HSTS"
fi
dnl ************************************************************* dnl *************************************************************
dnl check whether ECH support, if desired, is actually available dnl check whether ECH support, if desired, is actually available
dnl dnl
@ -4998,6 +5023,9 @@ fi
if test "x$enable_altsvc" = "xyes"; then if test "x$enable_altsvc" = "xyes"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES alt-svc" SUPPORT_FEATURES="$SUPPORT_FEATURES alt-svc"
fi fi
if test "x$enable_hsts" = "xyes"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES HSTS"
fi
if test "x$CURL_DISABLE_CRYPTO_AUTH" != "x1" -a \ if test "x$CURL_DISABLE_CRYPTO_AUTH" != "x1" -a \
\( "x$HAVE_GSSAPI" = "x1" -o "x$USE_WINDOWS_SSPI" = "x1" \); then \( "x$HAVE_GSSAPI" = "x1" -o "x$USE_WINDOWS_SSPI" = "x1" \); then

View File

@ -20,3 +20,5 @@ Experimental support in curl means:
- HTTP/3 support and options - HTTP/3 support and options
- CURLSSLOPT_NATIVE_CA (No configure option, feature built in when supported) - CURLSSLOPT_NATIVE_CA (No configure option, feature built in when supported)
- HSTS support and options

44
docs/HSTS.md Normal file
View File

@ -0,0 +1,44 @@
# HSTS support
curl features **EXPERIMENTAL** support for the Strict-Transport-Security: HTTP
header. Added in curl 7.74.0
## Standard
[HTTP Strict Transport Security](https://tools.ietf.org/html/rfc6797)
## Behavior
libcurl features an in-memory cache for HSTS hosts, so that subsequent
HTTP-only requests to a host name present in the cache will get internally
"redirected" to the HTTPS version.
## `curl_easy_setopt()` options:
- `CURLOPT_HSTS_CTRL` - enable HSTS for this easy handle
- `CURLOPT_HSTS` - specify file name where to store the HSTS cache on close
(and possibly read from at startup)
## curl cmdline options
- `--hsts [filename]` - enable HSTS, use the file as HSTS cache. If filename
is `""` (no length) then no file will be used, only in-memory cache.
## HSTS cache file format
Lines starting with `#` are ignored.
For each hsts entry:
[host name] "YYYYMMDD HH:MM:SS"
The `[host name]` is dot-prefixed if it is a includeSubDomain.
The time stamp is when the entry expires.
I considered using wget's file format for the HSTS cache. However, they store the time stamp as the epoch (number of seconds since 1970) and I strongly disagree with using that format. Instead I opted to use a format similar to the curl alt-svc cache file format.
## Possible future additions
- `CURLOPT_HSTS_PRELOAD` - provide a set of preloaded HSTS host names
- ability to save to something else than a file

View File

@ -63,6 +63,7 @@ EXTRA_DIST = \
GOVERNANCE.md \ GOVERNANCE.md \
HELP-US.md \ HELP-US.md \
HISTORY.md \ HISTORY.md \
HSTS.md \
HTTP-COOKIES.md \ HTTP-COOKIES.md \
HTTP2.md \ HTTP2.md \
HTTP3.md \ HTTP3.md \

View File

@ -85,6 +85,7 @@ DPAGES = \
head.d header.d \ head.d header.d \
help.d \ help.d \
hostpubmd5.d \ hostpubmd5.d \
hsts.d \
http0.9.d \ http0.9.d \
http1.0.d \ http1.0.d \
http1.1.d http2.d \ http1.1.d http2.d \

18
docs/cmdline-opts/hsts.d Normal file
View File

@ -0,0 +1,18 @@
Long: hsts
Arg: <file name>
Protocols: HTTPS
Help: Enable HSTS with this cache file
Added: 7.74.0
Category: http
---
WARNING: this option is experimental. Do not use in production.
This option enables HSTS for the transfer. If the file name points to an
existing HSTS cache file, that will be used. After a completed transfer, the
cache will be saved to the file name again if it has been modified.
Specify a "" file name (zero length) to avoid loading/saving and make curl
just handle HSTS in memory.
If this option is used several times, curl will load contents from all the
files but the last one will be used for saving.

View File

@ -28,6 +28,8 @@ This curl uses a libcurl built with Debug. This enables more error-tracking
and memory debugging etc. For curl-developers only! and memory debugging etc. For curl-developers only!
.IP "GSS-API" .IP "GSS-API"
GSS-API is supported. GSS-API is supported.
.IP "HSTS"
HSTS support is present.
.IP "HTTP2" .IP "HTTP2"
HTTP/2 support has been built-in. HTTP/2 support has been built-in.
.IP "HTTP3" .IP "HTTP3"

View File

@ -319,6 +319,10 @@ Add or control cookies. See \fICURLOPT_COOKIELIST(3)\fP
Specify the Alt-Svc: cache file name. See \fICURLOPT_ALTSVC(3)\fP Specify the Alt-Svc: cache file name. See \fICURLOPT_ALTSVC(3)\fP
.IP CURLOPT_ALTSVC_CTRL .IP CURLOPT_ALTSVC_CTRL
Enable and configure Alt-Svc: treatment. See \fICURLOPT_ALTSVC_CTRL(3)\fP Enable and configure Alt-Svc: treatment. See \fICURLOPT_ALTSVC_CTRL(3)\fP
.IP CURLOPT_HSTS
Set HSTS cache file. See \fICURLOPT_HSTS(3)\fP
.IP CURLOPT_HSTS_CTRL
Enable HSTS. See \fICURLOPT_HSTS_CTRL(3)\fP
.IP CURLOPT_HTTPGET .IP CURLOPT_HTTPGET
Do an HTTP GET request. See \fICURLOPT_HTTPGET(3)\fP Do an HTTP GET request. See \fICURLOPT_HTTPGET(3)\fP
.IP CURLOPT_REQUEST_TARGET .IP CURLOPT_REQUEST_TARGET

View File

@ -143,6 +143,9 @@ to use the current user credentials without the app having to pass them on.
(Added in 7.38.0) (Added in 7.38.0)
.IP CURL_VERSION_GSSNEGOTIATE .IP CURL_VERSION_GSSNEGOTIATE
supports HTTP GSS-Negotiate (added in 7.10.6) supports HTTP GSS-Negotiate (added in 7.10.6)
.IP CURL_VERSION_HSTS
libcurl was built with support for HSTS (HTTP Strict Transport Security)
(Added in 7.74.0)
.IP CURL_VERSION_HTTPS_PROXY .IP CURL_VERSION_HTTPS_PROXY
libcurl was built with support for HTTPS-proxy. libcurl was built with support for HTTPS-proxy.
(Added in 7.52.0) (Added in 7.52.0)

View File

@ -0,0 +1,66 @@
.\" **************************************************************************
.\" * _ _ ____ _
.\" * Project ___| | | | _ \| |
.\" * / __| | | | |_) | |
.\" * | (__| |_| | _ <| |___
.\" * \___|\___/|_| \_\_____|
.\" *
.\" * Copyright (C) 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
.\" *
.\" * This software is licensed as described in the file COPYING, which
.\" * you should have received as part of this distribution. The terms
.\" * are also available at https://curl.haxx.se/docs/copyright.html.
.\" *
.\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
.\" * copies of the Software, and permit persons to whom the Software is
.\" * furnished to do so, under the terms of the COPYING file.
.\" *
.\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
.\" * KIND, either express or implied.
.\" *
.\" **************************************************************************
.\"
.TH CURLOPT_HSTS 3 "5 Feb 2019" "libcurl 7.74.0" "curl_easy_setopt options"
.SH NAME
CURLOPT_HSTS \- set HSTS cache file name
.SH SYNOPSIS
.nf
#include <curl/curl.h>
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HSTS, char *filename);
.fi
.SH EXPERIMENTAL
Warning: this feature is early code and is marked as experimental. It can only
be enabled by explicitly telling configure with \fB--enable-hsts\fP. You are
advised to not ship this in production before the experimental label is
removed.
.SH DESCRIPTION
Make the \fIfilename\fP point to a file name to load an existing HSTS cache
from, and to store the cache in when the easy handle is closed. Setting a file
name with this option will also enable HSTS for this handle (the equivalent of
setting \fICURLHSTS_ENABLE\fP with \fICURLOPT_HSTS_CTRL(3)\fP).
If the given file does not exist or contains no HSTS entries at startup, the
HSTS cache will simply start empty. Setting the file name to NULL or "" will
only enable HSTS without reading from or writing to any file.
If this option is set multiple times, libcurl will load cache entries from
each given file but will only store the last used name for later writing.
.SH DEFAULT
NULL, no file name
.SH PROTOCOLS
HTTPS and HTTP
.SH EXAMPLE
.nf
CURL *curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_HSTS, "/home/user/.hsts-cache");
curl_easy_perform(curl);
}
.fi
.SH AVAILABILITY
Added in 7.74.0
.SH RETURN VALUE
Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not.
.SH "SEE ALSO"
.BR CURLOPT_HSTS_CTRL "(3), " CURLOPT_ALTSVC "(3), " CURLOPT_RESOLVE "(3), "

View File

@ -0,0 +1,73 @@
.\" **************************************************************************
.\" * _ _ ____ _
.\" * Project ___| | | | _ \| |
.\" * / __| | | | |_) | |
.\" * | (__| |_| | _ <| |___
.\" * \___|\___/|_| \_\_____|
.\" *
.\" * Copyright (C) 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
.\" *
.\" * This software is licensed as described in the file COPYING, which
.\" * you should have received as part of this distribution. The terms
.\" * are also available at https://curl.haxx.se/docs/copyright.html.
.\" *
.\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
.\" * copies of the Software, and permit persons to whom the Software is
.\" * furnished to do so, under the terms of the COPYING file.
.\" *
.\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
.\" * KIND, either express or implied.
.\" *
.\" **************************************************************************
.\"
.TH CURLOPT_HSTS_CTRL 3 "4 Sep 2020" "libcurl 7.74.0" "curl_easy_setopt options"
.SH NAME
CURLOPT_HSTS_CTRL \- control HSTS behavior
.SH SYNOPSIS
.nf
#include <curl/curl.h>
#define CURLHSTS_ENABLE (1<<0)
#define CURLHSTS_READONLYFILE (1<<1)
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HSTS_CTRL, long bitmask);
.fi
.SH EXPERIMENTAL
Warning: this feature is early code and is marked as experimental. It can only
be enabled by explicitly telling configure with \fB--enable-hsts\fP. You are
advised to not ship this in production before the experimental label is
removed.
.SH DESCRIPTION
HSTS (HTTP Strict Transport Security) means that an HTTPS server can instruct
the client to not contact it again over clear-text HTTP for a certain period
into the future. libcurl will then automatically redirect HTTP attempts to
such hosts to instead use HTTPS. This is done by libcurl retaining this
knowledge in an in-memory cache.
Populate the long \fIbitmask\fP with the correct set of features to instruct
libcurl how to handle HSTS for the transfers using this handle.
.SH BITS
.IP "CURLHSTS_ENABLE"
Enable the in-memory HSTS cache for this handle.
.IP "CURLHSTS_READONLYFILE"
Make the HSTS file (if specified) read-only - makes libcurl not save the cache
to the file when closing the handle.
.SH DEFAULT
0. HSTS is disabled by default.
.SH PROTOCOLS
HTTPS and HTTP
.SH EXAMPLE
.nf
CURL *curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_HSTS_CTRL, CURLHSTS_ENABLE);
curl_easy_perform(curl);
}
.fi
.SH AVAILABILITY
Added in 7.74.0
.SH RETURN VALUE
Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not.
.SH "SEE ALSO"
.BR CURLOPT_HSTS "(3), " CURLOPT_CONNECT_TO "(3), " CURLOPT_RESOLVE "(3), "
.BR CURLOPT_ALTSVC "(3), "

View File

@ -180,6 +180,8 @@ man_MANS = \
CURLOPT_HEADERDATA.3 \ CURLOPT_HEADERDATA.3 \
CURLOPT_HEADERFUNCTION.3 \ CURLOPT_HEADERFUNCTION.3 \
CURLOPT_HEADEROPT.3 \ CURLOPT_HEADEROPT.3 \
CURLOPT_HSTS.3 \
CURLOPT_HSTS_CTRL.3 \
CURLOPT_HTTP09_ALLOWED.3 \ CURLOPT_HTTP09_ALLOWED.3 \
CURLOPT_HTTP200ALIASES.3 \ CURLOPT_HTTP200ALIASES.3 \
CURLOPT_HTTPAUTH.3 \ CURLOPT_HTTPAUTH.3 \

View File

@ -213,6 +213,8 @@ CURLGSSAPI_DELEGATION_NONE 7.22.0
CURLGSSAPI_DELEGATION_POLICY_FLAG 7.22.0 CURLGSSAPI_DELEGATION_POLICY_FLAG 7.22.0
CURLHEADER_SEPARATE 7.37.0 CURLHEADER_SEPARATE 7.37.0
CURLHEADER_UNIFIED 7.37.0 CURLHEADER_UNIFIED 7.37.0
CURLHSTS_ENABLE 7.74.0
CURLHSTS_READONLYFILE 7.74.0
CURLINFO_ACTIVESOCKET 7.45.0 CURLINFO_ACTIVESOCKET 7.45.0
CURLINFO_APPCONNECT_TIME 7.19.0 CURLINFO_APPCONNECT_TIME 7.19.0
CURLINFO_APPCONNECT_TIME_T 7.61.0 CURLINFO_APPCONNECT_TIME_T 7.61.0
@ -443,6 +445,8 @@ CURLOPT_HEADER 7.1
CURLOPT_HEADERDATA 7.10 CURLOPT_HEADERDATA 7.10
CURLOPT_HEADERFUNCTION 7.7.2 CURLOPT_HEADERFUNCTION 7.7.2
CURLOPT_HEADEROPT 7.37.0 CURLOPT_HEADEROPT 7.37.0
CURLOPT_HSTS 7.74.0
CURLOPT_HSTS_CTRL 7.74.0
CURLOPT_HTTP09_ALLOWED 7.64.0 CURLOPT_HTTP09_ALLOWED 7.64.0
CURLOPT_HTTP200ALIASES 7.10.3 CURLOPT_HTTP200ALIASES 7.10.3
CURLOPT_HTTPAUTH 7.10.6 CURLOPT_HTTPAUTH 7.10.6
@ -1001,6 +1005,7 @@ CURL_VERSION_CURLDEBUG 7.19.6
CURL_VERSION_DEBUG 7.10.6 CURL_VERSION_DEBUG 7.10.6
CURL_VERSION_GSSAPI 7.38.0 CURL_VERSION_GSSAPI 7.38.0
CURL_VERSION_GSSNEGOTIATE 7.10.6 7.38.0 CURL_VERSION_GSSNEGOTIATE 7.10.6 7.38.0
CURL_VERSION_HSTS 7.74.0
CURL_VERSION_HTTP2 7.33.0 CURL_VERSION_HTTP2 7.33.0
CURL_VERSION_HTTP3 7.66.0 CURL_VERSION_HTTP3 7.66.0
CURL_VERSION_HTTPS_PROXY 7.52.0 CURL_VERSION_HTTPS_PROXY 7.52.0

View File

@ -79,6 +79,7 @@
--header (-H) 5.0 --header (-H) 5.0
--help (-h) 4.0 --help (-h) 4.0
--hostpubmd5 7.17.1 --hostpubmd5 7.17.1
--hsts 7.74.0
--http0.9 7.64.0 --http0.9 7.64.0
--http1.0 (-0) 7.9.1 --http1.0 (-0) 7.9.1
--http1.1 7.33.0 --http1.1 7.33.0

View File

@ -954,6 +954,10 @@ typedef enum {
#define CURLALTSVC_H2 (1<<4) #define CURLALTSVC_H2 (1<<4)
#define CURLALTSVC_H3 (1<<5) #define CURLALTSVC_H3 (1<<5)
/* CURLHSTS_* are bits for the CURLOPT_HSTS option */
#define CURLHSTS_ENABLE (long)(1<<0)
#define CURLHSTS_READONLYFILE (long)(1<<1)
/* CURLPROTO_ defines are for the CURLOPT_*PROTOCOLS options */ /* CURLPROTO_ defines are for the CURLOPT_*PROTOCOLS options */
#define CURLPROTO_HTTP (1<<0) #define CURLPROTO_HTTP (1<<0)
#define CURLPROTO_HTTPS (1<<1) #define CURLPROTO_HTTPS (1<<1)
@ -2029,6 +2033,11 @@ typedef enum {
*/ */
CURLOPT(CURLOPT_SSL_EC_CURVES, CURLOPTTYPE_STRINGPOINT, 298), CURLOPT(CURLOPT_SSL_EC_CURVES, CURLOPTTYPE_STRINGPOINT, 298),
/* HSTS bitmask */
CURLOPT(CURLOPT_HSTS_CTRL, CURLOPTTYPE_LONG, 299),
/* HSTS file name */
CURLOPT(CURLOPT_HSTS, CURLOPTTYPE_STRINGPOINT, 300),
CURLOPT_LASTENTRY /* the last unused */ CURLOPT_LASTENTRY /* the last unused */
} CURLoption; } CURLoption;
@ -2900,6 +2909,7 @@ typedef struct curl_version_info_data curl_version_info_data;
#define CURL_VERSION_HTTP3 (1<<25) /* HTTP3 support built-in */ #define CURL_VERSION_HTTP3 (1<<25) /* HTTP3 support built-in */
#define CURL_VERSION_ZSTD (1<<26) /* zstd features are present */ #define CURL_VERSION_ZSTD (1<<26) /* zstd features are present */
#define CURL_VERSION_UNICODE (1<<27) /* Unicode support on Windows */ #define CURL_VERSION_UNICODE (1<<27) /* Unicode support on Windows */
#define CURL_VERSION_HSTS (1<<28) /* HSTS is supported */
/* /*
* NAME curl_version_info() * NAME curl_version_info()

View File

@ -61,7 +61,7 @@ LIB_CFILES = altsvc.c amigaos.c asyn-ares.c asyn-thread.c base64.c \
socks_gssapi.c socks_sspi.c speedcheck.c splay.c strcase.c strdup.c \ socks_gssapi.c socks_sspi.c speedcheck.c splay.c strcase.c strdup.c \
strerror.c strtok.c strtoofft.c system_win32.c telnet.c tftp.c timeval.c \ strerror.c strtok.c strtoofft.c system_win32.c telnet.c tftp.c timeval.c \
transfer.c urlapi.c version.c warnless.c wildcard.c x509asn1.c dynbuf.c \ transfer.c urlapi.c version.c warnless.c wildcard.c x509asn1.c dynbuf.c \
version_win32.c easyoptions.c easygetopt.c version_win32.c easyoptions.c easygetopt.c hsts.c
LIB_HFILES = altsvc.h amigaos.h arpa_telnet.h asyn.h conncache.h connect.h \ LIB_HFILES = altsvc.h amigaos.h arpa_telnet.h asyn.h conncache.h connect.h \
content_encoding.h cookie.h curl_addrinfo.h curl_base64.h curl_ctype.h \ content_encoding.h cookie.h curl_addrinfo.h curl_base64.h curl_ctype.h \
@ -80,7 +80,7 @@ LIB_HFILES = altsvc.h amigaos.h arpa_telnet.h asyn.h conncache.h connect.h \
smb.h smtp.h sockaddr.h socketpair.h socks.h speedcheck.h splay.h strcase.h \ smb.h smtp.h sockaddr.h socketpair.h socks.h speedcheck.h splay.h strcase.h \
strdup.h strerror.h strtok.h strtoofft.h system_win32.h telnet.h tftp.h \ strdup.h strerror.h strtok.h strtoofft.h system_win32.h telnet.h tftp.h \
timeval.h transfer.h urlapi-int.h urldata.h warnless.h wildcard.h \ timeval.h transfer.h urlapi-int.h urldata.h warnless.h wildcard.h \
x509asn1.h dynbuf.h version_win32.h easyoptions.h x509asn1.h dynbuf.h version_win32.h easyoptions.h hsts.h
LIB_RCFILES = libcurl.rc LIB_RCFILES = libcurl.rc

View File

@ -22,7 +22,8 @@
#include "curl_setup.h" #include "curl_setup.h"
#if !defined(CURL_DISABLE_COOKIES) || !defined(CURL_DISABLE_ALTSVC) #if !defined(CURL_DISABLE_COOKIES) || !defined(CURL_DISABLE_ALTSVC)) || \
defined(USE_HSTS)
#include "curl_get_line.h" #include "curl_get_line.h"
#include "curl_memory.h" #include "curl_memory.h"

View File

@ -79,6 +79,7 @@
#include "http2.h" #include "http2.h"
#include "dynbuf.h" #include "dynbuf.h"
#include "altsvc.h" #include "altsvc.h"
#include "hsts.h"
/* The last 3 #include files should be in this order */ /* The last 3 #include files should be in this order */
#include "curl_printf.h" #include "curl_printf.h"
@ -880,6 +881,15 @@ struct Curl_easy *curl_easy_duphandle(struct Curl_easy *data)
if(outcurl->set.str[STRING_ALTSVC]) if(outcurl->set.str[STRING_ALTSVC])
(void)Curl_altsvc_load(outcurl->asi, outcurl->set.str[STRING_ALTSVC]); (void)Curl_altsvc_load(outcurl->asi, outcurl->set.str[STRING_ALTSVC]);
} }
#endif
#ifdef USE_HSTS
if(data->hsts) {
outcurl->hsts = Curl_hsts_init();
if(!outcurl->hsts)
goto fail;
if(outcurl->set.str[STRING_HSTS])
(void)Curl_hsts_load(outcurl->hsts, outcurl->set.str[STRING_HSTS]);
}
#endif #endif
/* Clone the resolver handle, if present, for the new handle */ /* Clone the resolver handle, if present, for the new handle */
if(Curl_resolver_duphandle(outcurl, if(Curl_resolver_duphandle(outcurl,
@ -929,6 +939,7 @@ struct Curl_easy *curl_easy_duphandle(struct Curl_easy *data)
Curl_safefree(outcurl->change.url); Curl_safefree(outcurl->change.url);
Curl_safefree(outcurl->change.referer); Curl_safefree(outcurl->change.referer);
Curl_altsvc_cleanup(&outcurl->asi); Curl_altsvc_cleanup(&outcurl->asi);
Curl_hsts_cleanup(&outcurl->hsts);
Curl_freeset(outcurl); Curl_freeset(outcurl);
free(outcurl); free(outcurl);
} }

View File

@ -115,6 +115,8 @@ struct curl_easyoption Curl_easyopts[] = {
{"HEADERDATA", CURLOPT_HEADERDATA, CURLOT_CBPTR, 0}, {"HEADERDATA", CURLOPT_HEADERDATA, CURLOT_CBPTR, 0},
{"HEADERFUNCTION", CURLOPT_HEADERFUNCTION, CURLOT_FUNCTION, 0}, {"HEADERFUNCTION", CURLOPT_HEADERFUNCTION, CURLOT_FUNCTION, 0},
{"HEADEROPT", CURLOPT_HEADEROPT, CURLOT_VALUES, 0}, {"HEADEROPT", CURLOPT_HEADEROPT, CURLOT_VALUES, 0},
{"HSTS", CURLOPT_HSTS, CURLOT_STRING, 0},
{"HSTS_CTRL", CURLOPT_HSTS_CTRL, CURLOT_LONG, 0},
{"HTTP09_ALLOWED", CURLOPT_HTTP09_ALLOWED, CURLOT_LONG, 0}, {"HTTP09_ALLOWED", CURLOPT_HTTP09_ALLOWED, CURLOT_LONG, 0},
{"HTTP200ALIASES", CURLOPT_HTTP200ALIASES, CURLOT_SLIST, 0}, {"HTTP200ALIASES", CURLOPT_HTTP200ALIASES, CURLOT_SLIST, 0},
{"HTTPAUTH", CURLOPT_HTTPAUTH, CURLOT_VALUES, 0}, {"HTTPAUTH", CURLOPT_HTTPAUTH, CURLOT_VALUES, 0},
@ -342,6 +344,6 @@ struct curl_easyoption Curl_easyopts[] = {
*/ */
int Curl_easyopts_check(void) int Curl_easyopts_check(void)
{ {
return (CURLOPT_LASTENTRY != (298 + 1)); return (CURLOPT_LASTENTRY != (300 + 1));
} }
#endif #endif

430
lib/hsts.c Normal file
View File

@ -0,0 +1,430 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.haxx.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
***************************************************************************/
/*
* The Strict-Transport-Security header is defined in RFC 6797:
* https://tools.ietf.org/html/rfc6797
*/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HSTS)
#include <curl/curl.h>
#include "urldata.h"
#include "llist.h"
#include "hsts.h"
#include "curl_get_line.h"
#include "strcase.h"
#include "sendf.h"
#include "strtoofft.h"
#include "parsedate.h"
#include "rand.h"
#include "rename.h"
/* The last 3 #include files should be in this order */
#include "curl_printf.h"
#include "curl_memory.h"
#include "memdebug.h"
#define MAX_HSTS_LINE 4095
#define MAX_HSTS_HOSTLEN 256
#define MAX_HSTS_HOSTLENSTR "256"
#define MAX_HSTS_SUBLEN 4
#define MAX_HSTS_SUBLENSTR "4"
#define MAX_HSTS_DATELEN 64
#define MAX_HSTS_DATELENSTR "64"
#ifdef DEBUGBUILD
/* to play well with debug builds, we can *set* a fixed time this will
return */
time_t deltatime; /* allow for "adjustments" for unit test purposes */
static time_t debugtime(void *unused)
{
char *timestr = getenv("CURL_TIME");
(void)unused;
if(timestr) {
unsigned long val = strtol(timestr, NULL, 10) + deltatime;
return (time_t)val;
}
return time(NULL);
}
#define time(x) debugtime(x)
#endif
struct hsts *Curl_hsts_init(void)
{
struct hsts *h = calloc(sizeof(struct hsts), 1);
if(h) {
Curl_llist_init(&h->list, NULL);
}
return h;
}
static void hsts_free(struct stsentry *e)
{
free((char *)e->host);
free(e);
}
void Curl_hsts_cleanup(struct hsts **hp)
{
struct hsts *h = *hp;
if(h) {
struct Curl_llist_element *e;
struct Curl_llist_element *n;
for(e = h->list.head; e; e = n) {
struct stsentry *sts = e->ptr;
n = e->next;
hsts_free(sts);
}
free(h->filename);
free(h);
*hp = NULL;
}
}
static struct stsentry *hsts_entry(void)
{
return calloc(sizeof(struct stsentry), 1);
}
static CURLcode hsts_create(struct hsts *h,
const char *hostname,
bool subdomains,
curl_off_t expires)
{
struct stsentry *sts = hsts_entry();
if(!sts)
return CURLE_OUT_OF_MEMORY;
sts->expires = expires;
sts->includeSubDomains = subdomains;
sts->host = strdup(hostname);
if(!sts->host) {
free(sts);
return CURLE_OUT_OF_MEMORY;
}
fprintf(stderr, "*** Add %s %s %d\n", hostname,
subdomains?"SUB":"-", (int)expires);
Curl_llist_insert_next(&h->list, h->list.tail, sts, &sts->node);
return CURLE_OK;
}
CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname,
const char *header)
{
const char *p = header;
curl_off_t expires = 0;
bool gotma = FALSE;
bool gotinc = FALSE;
bool subdomains = FALSE;
struct stsentry *sts;
time_t now = time(NULL);
do {
while(*p && ISSPACE(*p))
p++;
if(Curl_strncasecompare("max-age=", p, 8)) {
bool quoted = FALSE;
CURLofft offt;
char *endp;
if(gotma)
return CURLE_BAD_FUNCTION_ARGUMENT;
p += 8;
while(*p && ISSPACE(*p))
p++;
if(*p == '\"') {
p++;
quoted = TRUE;
}
offt = curlx_strtoofft(p, &endp, 10, &expires);
if(offt == CURL_OFFT_FLOW)
expires = CURL_OFF_T_MAX;
else if(offt)
/* invalid max-age */
return CURLE_BAD_FUNCTION_ARGUMENT;
p = endp;
if(quoted) {
if(*p != '\"')
return CURLE_BAD_FUNCTION_ARGUMENT;
p++;
}
gotma = TRUE;
}
else if(Curl_strncasecompare("includesubdomains", p, 17)) {
if(gotinc)
return CURLE_BAD_FUNCTION_ARGUMENT;
subdomains = TRUE;
p += 17;
gotinc = TRUE;
}
else {
/* unknown directive, do a lame attempt to skip */
while(*p && (*p != ';'))
p++;
}
while(*p && ISSPACE(*p))
p++;
if(*p == ';')
p++;
} while (*p);
if(!gotma)
/* max-age is mandatory */
return CURLE_BAD_FUNCTION_ARGUMENT;
if(!expires) {
/* remove the entry if present verbatim (without subdomain match) */
sts = Curl_hsts(h, hostname, FALSE);
if(sts) {
Curl_llist_remove(&h->list, &sts->node, NULL);
hsts_free(sts);
}
return CURLE_OK;
}
if(CURL_OFF_T_MAX - now < expires)
/* would overflow, use maximum value */
expires = CURL_OFF_T_MAX;
else
expires += now;
/* check if it already exists */
sts = Curl_hsts(h, hostname, FALSE);
if(sts) {
/* just update these fields */
sts->expires = expires;
sts->includeSubDomains = subdomains;
}
else
return hsts_create(h, hostname, subdomains, expires);
return CURLE_OK;
}
/*
* Return TRUE if the given host name is currently an HSTS one.
*
* The 'subdomain' argument tells the function if subdomain matching should be
* attempted.
*/
struct stsentry *Curl_hsts(struct hsts *h, const char *hostname,
bool subdomain)
{
if(h) {
time_t now = time(NULL);
size_t hlen = strlen(hostname);
struct Curl_llist_element *e;
struct Curl_llist_element *n;
for(e = h->list.head; e; e = n) {
struct stsentry *sts = e->ptr;
n = e->next;
if(sts->expires <= now) {
/* remove expired entries */
Curl_llist_remove(&h->list, &sts->node, NULL);
hsts_free(sts);
continue;
}
if(subdomain && sts->includeSubDomains) {
size_t ntail = strlen(sts->host);
if(ntail < hlen) {
size_t offs = hlen - ntail;
if((hostname[offs-1] == '.') &&
Curl_strncasecompare(&hostname[offs], sts->host, ntail))
return sts;
}
}
if(Curl_strcasecompare(hostname, sts->host))
return sts;
}
}
return NULL; /* no match */
}
/*
* Write this single hsts entry to a single output line
*/
static CURLcode hsts_out(struct stsentry *sts, FILE *fp)
{
struct tm stamp;
CURLcode result = Curl_gmtime(sts->expires, &stamp);
if(result)
return result;
fprintf(fp, "%s%s \"%d%02d%02d %02d:%02d:%02d\"\n",
sts->includeSubDomains ? ".": "", sts->host,
stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
stamp.tm_hour, stamp.tm_min, stamp.tm_sec);
return CURLE_OK;
}
/*
* Curl_https_save() writes the HSTS cache to a file.
*/
CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h,
const char *file)
{
struct Curl_llist_element *e;
struct Curl_llist_element *n;
CURLcode result = CURLE_OK;
FILE *out;
char *tempstore;
unsigned char randsuffix[9];
if(!h)
/* no cache activated */
return CURLE_OK;
/* if not new name is given, use the one we stored from the load */
if(!file && h->filename)
file = h->filename;
if((h->flags & CURLHSTS_READONLYFILE) || !file || !file[0])
/* marked as read-only, no file or zero length file name */
return CURLE_OK;
if(Curl_rand_hex(data, randsuffix, sizeof(randsuffix)))
return CURLE_FAILED_INIT;
tempstore = aprintf("%s.%s.tmp", file, randsuffix);
if(!tempstore)
return CURLE_OUT_OF_MEMORY;
out = fopen(tempstore, FOPEN_WRITETEXT);
if(!out)
result = CURLE_WRITE_ERROR;
else {
fputs("# Your HSTS cache. https://curl.haxx.se/docs/hsts.html\n"
"# This file was generated by libcurl! Edit at your own risk.\n",
out);
for(e = h->list.head; e; e = n) {
struct stsentry *sts = e->ptr;
n = e->next;
result = hsts_out(sts, out);
if(result)
break;
}
fclose(out);
if(!result && Curl_rename(tempstore, file))
result = CURLE_WRITE_ERROR;
if(result)
unlink(tempstore);
}
free(tempstore);
return result;
}
/* only returns SERIOUS errors */
static CURLcode hsts_add(struct hsts *h, char *line)
{
/* Example lines:
example.com "20191231 10:00:00"
.example.net "20191231 10:00:00"
*/
char host[MAX_HSTS_HOSTLEN + 1];
char date[MAX_HSTS_DATELEN + 1];
int rc;
rc = sscanf(line,
"%" MAX_HSTS_HOSTLENSTR "s \"%" MAX_HSTS_DATELENSTR "[^\"]\"",
host, date);
if(2 == rc) {
time_t expires = Curl_getdate_capped(date);
CURLcode result;
char *p = host;
bool subdomain = FALSE;
if(p[0] == '.') {
p++;
subdomain = TRUE;
}
result = hsts_create(h, p, subdomain, expires);
if(result)
return result;
}
return CURLE_OK;
}
/*
* Load the HSTS cache from the given file. The text based line-oriented file
* format is documented here:
* https://github.com/curl/curl/wiki/HSTS
*
* This function only returns error on major problems that prevents hsts
* handling to work completely. It will ignore individual syntactical errors
* etc.
*/
static CURLcode hsts_load(struct hsts *h, const char *file)
{
CURLcode result = CURLE_OK;
char *line = NULL;
FILE *fp;
/* we need a private copy of the file name so that the hsts cache file
name survives an easy handle reset */
free(h->filename);
h->filename = strdup(file);
if(!h->filename)
return CURLE_OUT_OF_MEMORY;
fp = fopen(file, FOPEN_READTEXT);
if(fp) {
line = malloc(MAX_HSTS_LINE);
if(!line)
goto fail;
while(Curl_get_line(line, MAX_HSTS_LINE, fp)) {
char *lineptr = line;
while(*lineptr && ISBLANK(*lineptr))
lineptr++;
if(*lineptr == '#')
/* skip commented lines */
continue;
hsts_add(h, lineptr);
}
free(line); /* free the line buffer */
fclose(fp);
}
return result;
fail:
Curl_safefree(h->filename);
free(line);
fclose(fp);
return CURLE_OUT_OF_MEMORY;
}
/*
* Curl_hsts_load() loads HSTS from file.
*/
CURLcode Curl_hsts_load(struct hsts *h, const char *file)
{
CURLcode result;
DEBUGASSERT(h);
result = hsts_load(h, file);
return result;
}
#endif /* CURL_DISABLE_HTTP || USE_HSTS */

60
lib/hsts.h Normal file
View File

@ -0,0 +1,60 @@
#ifndef HEADER_CURL_HSTS_H
#define HEADER_CURL_HSTS_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.haxx.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HSTS)
#include <curl/curl.h>
#include "llist.h"
#ifdef DEBUGBUILD
extern time_t deltatime;
#endif
struct stsentry {
struct Curl_llist_element node;
const char *host;
bool includeSubDomains;
time_t expires; /* the timestamp of this entry's expiry */
};
/* The HSTS cache. Needs to be able to tailmatch host names. */
struct hsts {
struct Curl_llist list;
char *filename;
unsigned int flags;
};
struct hsts *Curl_hsts_init(void);
void Curl_hsts_cleanup(struct hsts **hp);
CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname,
const char *sts);
struct stsentry *Curl_hsts(struct hsts *h, const char *hostname,
bool subdomain);
CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h,
const char *file);
CURLcode Curl_hsts_load(struct hsts *h, const char *file);
#else
#define Curl_hsts_cleanup(x)
#endif /* CURL_DISABLE_HTTP || USE_HSTS */
#endif /* HEADER_CURL_HSTS_H */

View File

@ -77,6 +77,7 @@
#include "connect.h" #include "connect.h"
#include "strdup.h" #include "strdup.h"
#include "altsvc.h" #include "altsvc.h"
#include "hsts.h"
/* The last 3 #include files should be in this order */ /* The last 3 #include files should be in this order */
#include "curl_printf.h" #include "curl_printf.h"
@ -3990,6 +3991,23 @@ CURLcode Curl_http_readwrite_headers(struct Curl_easy *data,
} }
} }
} }
#ifdef USE_HSTS
/* If enabled, the header is incoming and this is over HTTPS */
else if(data->hsts && checkprefix("Strict-Transport-Security:", headp) &&
(conn->handler->flags & PROTOPT_SSL)) {
CURLcode check =
Curl_hsts_parse(data->hsts, data->state.up.hostname,
&headp[ sizeof("Strict-Transport-Security:") -1 ]);
if(check)
infof(data, "Illegal STS header skipped\n");
#ifdef DEBUGBUILD
else
infof(data, "Parsed STS header fine (%d entries)\n",
data->hsts->list.size);
#endif
}
#endif
#ifndef CURL_DISABLE_ALTSVC #ifndef CURL_DISABLE_ALTSVC
/* If enabled, the header is incoming and this is over HTTPS */ /* If enabled, the header is incoming and this is over HTTPS */
else if(data->asi && checkprefix("Alt-Svc:", headp) && else if(data->asi && checkprefix("Alt-Svc:", headp) &&

View File

@ -45,6 +45,7 @@
#include "setopt.h" #include "setopt.h"
#include "multiif.h" #include "multiif.h"
#include "altsvc.h" #include "altsvc.h"
#include "hsts.h"
/* The last 3 #include files should be in this order */ /* The last 3 #include files should be in this order */
#include "curl_printf.h" #include "curl_printf.h"
@ -2839,6 +2840,33 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
data->set.trailer_data = va_arg(param, void *); data->set.trailer_data = va_arg(param, void *);
#endif #endif
break; break;
#ifdef USE_HSTS
case CURLOPT_HSTS:
if(!data->hsts) {
data->hsts = Curl_hsts_init();
if(!data->hsts)
return CURLE_OUT_OF_MEMORY;
}
argptr = va_arg(param, char *);
result = Curl_setstropt(&data->set.str[STRING_HSTS], argptr);
if(result)
return result;
if(argptr)
(void)Curl_hsts_load(data->hsts, argptr);
break;
case CURLOPT_HSTS_CTRL:
arg = va_arg(param, long);
if(arg & CURLHSTS_ENABLE) {
if(!data->hsts) {
data->hsts = Curl_hsts_init();
if(!data->hsts)
return CURLE_OUT_OF_MEMORY;
}
}
else
Curl_hsts_cleanup(&data->hsts);
break;
#endif
#ifndef CURL_DISABLE_ALTSVC #ifndef CURL_DISABLE_ALTSVC
case CURLOPT_ALTSVC: case CURLOPT_ALTSVC:
if(!data->asi) { if(!data->asi) {

View File

@ -96,6 +96,7 @@ bool curl_win32_idn_to_ascii(const char *in, char **out);
#include "getinfo.h" #include "getinfo.h"
#include "urlapi-int.h" #include "urlapi-int.h"
#include "system_win32.h" #include "system_win32.h"
#include "hsts.h"
/* And now for the protocols */ /* And now for the protocols */
#include "ftp.h" #include "ftp.h"
@ -411,6 +412,7 @@ CURLcode Curl_close(struct Curl_easy **datap)
Curl_flush_cookies(data, TRUE); Curl_flush_cookies(data, TRUE);
Curl_altsvc_save(data, data->asi, data->set.str[STRING_ALTSVC]); Curl_altsvc_save(data, data->asi, data->set.str[STRING_ALTSVC]);
Curl_altsvc_cleanup(&data->asi); Curl_altsvc_cleanup(&data->asi);
Curl_hsts_cleanup(&data->hsts);
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH) #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH)
Curl_http_auth_cleanup_digest(data); Curl_http_auth_cleanup_digest(data);
#endif #endif
@ -1911,6 +1913,19 @@ static CURLcode parseurlandfillconn(struct Curl_easy *data,
if(uc) if(uc)
return Curl_uc_to_curlcode(uc); return Curl_uc_to_curlcode(uc);
uc = curl_url_get(uh, CURLUPART_HOST, &data->state.up.hostname, 0);
if(uc) {
if(!strcasecompare("file", data->state.up.scheme))
return CURLE_OUT_OF_MEMORY;
}
#ifdef USE_HSTS
if(data->hsts && strcasecompare("http", data->state.up.scheme)) {
if(Curl_hsts(data->hsts, data->state.up.hostname, TRUE))
infof(data, "Switch from HTTP to HTTPS due to HSTS!\n");
}
#endif
result = findprotocol(data, conn, data->state.up.scheme); result = findprotocol(data, conn, data->state.up.scheme);
if(result) if(result)
return result; return result;
@ -1956,12 +1971,6 @@ static CURLcode parseurlandfillconn(struct Curl_easy *data,
else if(uc != CURLUE_NO_OPTIONS) else if(uc != CURLUE_NO_OPTIONS)
return Curl_uc_to_curlcode(uc); return Curl_uc_to_curlcode(uc);
uc = curl_url_get(uh, CURLUPART_HOST, &data->state.up.hostname, 0);
if(uc) {
if(!strcasecompare("file", data->state.up.scheme))
return CURLE_OUT_OF_MEMORY;
}
uc = curl_url_get(uh, CURLUPART_PATH, &data->state.up.path, 0); uc = curl_url_get(uh, CURLUPART_PATH, &data->state.up.path, 0);
if(uc) if(uc)
return Curl_uc_to_curlcode(uc); return Curl_uc_to_curlcode(uc);

View File

@ -1531,35 +1531,26 @@ enum dupstring {
STRING_RTSP_SESSION_ID, /* Session ID to use */ STRING_RTSP_SESSION_ID, /* Session ID to use */
STRING_RTSP_STREAM_URI, /* Stream URI for this request */ STRING_RTSP_STREAM_URI, /* Stream URI for this request */
STRING_RTSP_TRANSPORT, /* Transport for this session */ STRING_RTSP_TRANSPORT, /* Transport for this session */
STRING_SSH_PRIVATE_KEY, /* path to the private key file for auth */ STRING_SSH_PRIVATE_KEY, /* path to the private key file for auth */
STRING_SSH_PUBLIC_KEY, /* path to the public key file for auth */ STRING_SSH_PUBLIC_KEY, /* path to the public key file for auth */
STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */ STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */
STRING_SSH_KNOWNHOSTS, /* file name of knownhosts file */ STRING_SSH_KNOWNHOSTS, /* file name of knownhosts file */
STRING_PROXY_SERVICE_NAME, /* Proxy service name */ STRING_PROXY_SERVICE_NAME, /* Proxy service name */
STRING_SERVICE_NAME, /* Service name */ STRING_SERVICE_NAME, /* Service name */
STRING_MAIL_FROM, STRING_MAIL_FROM,
STRING_MAIL_AUTH, STRING_MAIL_AUTH,
STRING_TLSAUTH_USERNAME_ORIG, /* TLS auth <username> */ STRING_TLSAUTH_USERNAME_ORIG, /* TLS auth <username> */
STRING_TLSAUTH_USERNAME_PROXY, /* TLS auth <username> */ STRING_TLSAUTH_USERNAME_PROXY, /* TLS auth <username> */
STRING_TLSAUTH_PASSWORD_ORIG, /* TLS auth <password> */ STRING_TLSAUTH_PASSWORD_ORIG, /* TLS auth <password> */
STRING_TLSAUTH_PASSWORD_PROXY, /* TLS auth <password> */ STRING_TLSAUTH_PASSWORD_PROXY, /* TLS auth <password> */
STRING_BEARER, /* <bearer>, if used */ STRING_BEARER, /* <bearer>, if used */
STRING_UNIX_SOCKET_PATH, /* path to Unix socket, if used */ STRING_UNIX_SOCKET_PATH, /* path to Unix socket, if used */
STRING_TARGET, /* CURLOPT_REQUEST_TARGET */ STRING_TARGET, /* CURLOPT_REQUEST_TARGET */
STRING_DOH, /* CURLOPT_DOH_URL */ STRING_DOH, /* CURLOPT_DOH_URL */
STRING_ALTSVC, /* CURLOPT_ALTSVC */ STRING_ALTSVC, /* CURLOPT_ALTSVC */
STRING_HSTS, /* CURLOPT_HSTS */
STRING_SASL_AUTHZID, /* CURLOPT_SASL_AUTHZID */ STRING_SASL_AUTHZID, /* CURLOPT_SASL_AUTHZID */
STRING_TEMP_URL, /* temp URL storage for proxy use */ STRING_TEMP_URL, /* temp URL storage for proxy use */
STRING_DNS_SERVERS, STRING_DNS_SERVERS,
STRING_DNS_INTERFACE, STRING_DNS_INTERFACE,
STRING_DNS_LOCAL_IP4, STRING_DNS_LOCAL_IP4,
@ -1899,6 +1890,9 @@ struct Curl_easy {
NOTE that the 'cookie' field in the NOTE that the 'cookie' field in the
UserDefined struct defines if the "engine" UserDefined struct defines if the "engine"
is to be used or not. */ is to be used or not. */
#ifdef USE_HSTS
struct hsts *hsts;
#endif
#ifndef CURL_DISABLE_ALTSVC #ifndef CURL_DISABLE_ALTSVC
struct altsvcinfo *asi; /* the alt-svc cache */ struct altsvcinfo *asi; /* the alt-svc cache */
#endif #endif

View File

@ -417,6 +417,9 @@ static curl_version_info_data version_info = {
#endif #endif
#ifndef CURL_DISABLE_ALTSVC #ifndef CURL_DISABLE_ALTSVC
| CURL_VERSION_ALTSVC | CURL_VERSION_ALTSVC
#endif
#if defined(USE_HSTS)
| CURL_VERSION_HSTS
#endif #endif
, ,
NULL, /* ssl_version */ NULL, /* ssl_version */

View File

@ -54,6 +54,7 @@ static void free_config_fields(struct OperationConfig *config)
Curl_safefree(config->egd_file); Curl_safefree(config->egd_file);
Curl_safefree(config->useragent); Curl_safefree(config->useragent);
Curl_safefree(config->altsvc); Curl_safefree(config->altsvc);
Curl_safefree(config->hsts);
Curl_safefree(config->cookie); Curl_safefree(config->cookie);
Curl_safefree(config->cookiejar); Curl_safefree(config->cookiejar);
Curl_safefree(config->cookiefile); Curl_safefree(config->cookiefile);

View File

@ -58,6 +58,7 @@ struct OperationConfig {
char *cookiejar; /* write to this file */ char *cookiejar; /* write to this file */
char *cookiefile; /* read from this file */ char *cookiefile; /* read from this file */
char *altsvc; /* alt-svc cache file name */ char *altsvc; /* alt-svc cache file name */
char *hsts; /* HSTS cache file name */
bool cookiesession; /* new session? */ bool cookiesession; /* new session? */
bool encoding; /* Accept-Encoding please */ bool encoding; /* Accept-Encoding please */
bool tr_encoding; /* Transfer-Encoding please */ bool tr_encoding; /* Transfer-Encoding please */

View File

@ -219,6 +219,7 @@ static const struct LongShort aliases[]= {
{"A", "user-agent", ARG_STRING}, {"A", "user-agent", ARG_STRING},
{"b", "cookie", ARG_STRING}, {"b", "cookie", ARG_STRING},
{"ba", "alt-svc", ARG_STRING}, {"ba", "alt-svc", ARG_STRING},
{"bb", "hsts", ARG_STRING},
{"B", "use-ascii", ARG_BOOL}, {"B", "use-ascii", ARG_BOOL},
{"c", "cookie-jar", ARG_STRING}, {"c", "cookie-jar", ARG_STRING},
{"C", "continue-at", ARG_STRING}, {"C", "continue-at", ARG_STRING},
@ -1291,6 +1292,12 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
else else
return PARAM_LIBCURL_DOESNT_SUPPORT; return PARAM_LIBCURL_DOESNT_SUPPORT;
break; break;
case 'b': /* --hsts */
if(curlinfo->features & CURL_VERSION_HSTS)
GetStr(&config->hsts, nextarg);
else
return PARAM_LIBCURL_DOESNT_SUPPORT;
break;
default: /* --cookie string coming up: */ default: /* --cookie string coming up: */
if(nextarg[0] == '@') { if(nextarg[0] == '@') {
nextarg++; nextarg++;

View File

@ -328,6 +328,9 @@ static const struct helptxt helptext[] = {
{" --hostpubmd5 <md5>", {" --hostpubmd5 <md5>",
"Acceptable MD5 hash of the host public key", "Acceptable MD5 hash of the host public key",
CURLHELP_SFTP | CURLHELP_SCP}, CURLHELP_SFTP | CURLHELP_SCP},
{" --hsts <file name>",
"Enable HSTS with this cache file",
CURLHELP_HTTP},
{" --http0.9", {" --http0.9",
"Allow HTTP 0.9 responses", "Allow HTTP 0.9 responses",
CURLHELP_HTTP}, CURLHELP_HTTP},
@ -862,6 +865,7 @@ static const struct feat feats[] = {
{"MultiSSL", CURL_VERSION_MULTI_SSL}, {"MultiSSL", CURL_VERSION_MULTI_SSL},
{"PSL", CURL_VERSION_PSL}, {"PSL", CURL_VERSION_PSL},
{"alt-svc", CURL_VERSION_ALTSVC}, {"alt-svc", CURL_VERSION_ALTSVC},
{"HSTS", CURL_VERSION_HSTS},
}; };
static void print_category(curlhelp_t category) static void print_category(curlhelp_t category)

View File

@ -2072,6 +2072,9 @@ static CURLcode single_transfer(struct GlobalConfig *global,
if(config->altsvc) if(config->altsvc)
my_setopt_str(curl, CURLOPT_ALTSVC, config->altsvc); my_setopt_str(curl, CURLOPT_ALTSVC, config->altsvc);
if(config->hsts)
my_setopt_bitmask(curl, CURLOPT_HSTS_CTRL, CURLHSTS_ENABLE);
#ifdef USE_METALINK #ifdef USE_METALINK
if(!metalink && config->use_metalink) { if(!metalink && config->use_metalink) {
outs->metalink_parser = metalink_parser_context_new(); outs->metalink_parser = metalink_parser_context_new();

View File

@ -62,6 +62,11 @@ const struct NameValue setopt_nv_CURL_SOCKS_PROXY[] = {
NVEND, NVEND,
}; };
const struct NameValueUnsigned setopt_nv_CURLHSTS[] = {
NV(CURLHSTS_ENABLE),
NVEND,
};
const struct NameValueUnsigned setopt_nv_CURLAUTH[] = { const struct NameValueUnsigned setopt_nv_CURLAUTH[] = {
NV(CURLAUTH_ANY), /* combination */ NV(CURLAUTH_ANY), /* combination */
NV(CURLAUTH_ANYSAFE), /* combination */ NV(CURLAUTH_ANYSAFE), /* combination */

View File

@ -64,8 +64,10 @@ extern const struct NameValueUnsigned setopt_nv_CURLSSLOPT[];
extern const struct NameValue setopt_nv_CURL_NETRC[]; extern const struct NameValue setopt_nv_CURL_NETRC[];
extern const struct NameValue setopt_nv_CURLPROTO[]; extern const struct NameValue setopt_nv_CURLPROTO[];
extern const struct NameValueUnsigned setopt_nv_CURLAUTH[]; extern const struct NameValueUnsigned setopt_nv_CURLAUTH[];
extern const struct NameValueUnsigned setopt_nv_CURLHSTS[];
/* Map options to NameValue sets */ /* Map options to NameValue sets */
#define setopt_nv_CURLOPT_HSTS_CTRL setopt_nv_CURLHSTS
#define setopt_nv_CURLOPT_HTTP_VERSION setopt_nv_CURL_HTTP_VERSION #define setopt_nv_CURLOPT_HTTP_VERSION setopt_nv_CURL_HTTP_VERSION
#define setopt_nv_CURLOPT_HTTPAUTH setopt_nv_CURLAUTH #define setopt_nv_CURLOPT_HTTPAUTH setopt_nv_CURLAUTH
#define setopt_nv_CURLOPT_SSLVERSION setopt_nv_CURL_SSLVERSION #define setopt_nv_CURLOPT_SSLVERSION setopt_nv_CURL_SSLVERSION

View File

@ -197,6 +197,7 @@ test1620 test1621 \
test1630 test1631 test1632 test1633 \ test1630 test1631 test1632 test1633 \
\ \
test1650 test1651 test1652 test1653 test1654 test1655 \ test1650 test1651 test1652 test1653 test1654 test1655 \
test1660 \
\ \
test1700 test1701 test1702 \ test1700 test1701 test1702 \
\ \

81
tests/data/test1660 Normal file
View File

@ -0,0 +1,81 @@
<testcase>
<info>
<keywords>
unittest
HSTS
</keywords>
</info>
<client>
<server>
none
</server>
<features>
unittest
HSTS
</features>
<file name="log/input1660">
# Your HSTS cache. https://curl.haxx.se/docs/hsts.html
# This file was generated by libcurl! Edit at your own risk.
.readfrom.example "20211001 04:47:41"
.old.example "20161001 04:47:41"
</file>
# This date is exactly "20190124 22:34:21" UTC
<setenv>
CURL_TIME=1548369261
</setenv>
<name>
HSTS
</name>
<command>
-
</command>
</client>
<verify>
<stdout>
readfrom.example [readfrom.example]: 1633063661 includeSubDomains
'old.example' is not HSTS
'readfrom.example' is not HSTS
example.com [example.com]: 1579905261
example.com [example.com]: 1569905261
example.com [example.com]: 1569905261
example.com [example.com]: 1569905261 includeSubDomains
example.org [example.org]: 1579905261
Input 8: error 43
Input 9: error 43
this.example [this.example]: 1548400797
'this.example' is not HSTS
Input 12: error 43
Input 13: error 43
Input 14: error 43
3.example.com [example.com]: 1569905261 includeSubDomains
3.example.com [example.com]: 1569905261 includeSubDomains
foo.example.com [example.com]: 1569905261 includeSubDomains
'foo.xample.com' is not HSTS
'forexample.net' is not HSTS
'forexample.net' is not HSTS
'example.net' is not HSTS
expire.example [expire.example]: 1548369268
Number of entries: 3
expire.example [expire.example]: 1548369268
expire.example [expire.example]: 1548369268
expire.example [expire.example]: 1548369268
expire.example [expire.example]: 1548369268
expire.example [expire.example]: 1548369268
expire.example [expire.example]: 1548369268
expire.example [expire.example]: 1548369268
'expire.example' is not HSTS
'expire.example' is not HSTS
'expire.example' is not HSTS
</stdout>
<file name="log/hsts1660">
# Your HSTS cache. https://curl.haxx.se/docs/hsts.html
# This file was generated by libcurl! Edit at your own risk.
.example.com "20191001 04:47:41"
example.org "20200124 22:34:21"
</file>
</verify>
</testcase>

View File

@ -256,6 +256,7 @@ my $has_cares; # set if built with c-ares
my $has_threadedres;# set if built with threaded resolver my $has_threadedres;# set if built with threaded resolver
my $has_psl; # set if libcurl is built with PSL support my $has_psl; # set if libcurl is built with PSL support
my $has_altsvc; # set if libcurl is built with alt-svc support my $has_altsvc; # set if libcurl is built with alt-svc support
my $has_hsts; # set if libcurl is built with HSTS support
my $has_ldpreload; # set if curl is built for systems supporting LD_PRELOAD my $has_ldpreload; # set if curl is built for systems supporting LD_PRELOAD
my $has_multissl; # set if curl is build with MultiSSL support my $has_multissl; # set if curl is build with MultiSSL support
my $has_manual; # set if curl is built with built-in manual my $has_manual; # set if curl is built with built-in manual
@ -2762,6 +2763,7 @@ sub compare {
sub setupfeatures { sub setupfeatures {
$feature{"alt-svc"} = $has_altsvc; $feature{"alt-svc"} = $has_altsvc;
$feature{"HSTS"} = $has_hsts;
$feature{"brotli"} = $has_brotli; $feature{"brotli"} = $has_brotli;
$feature{"crypto"} = $has_crypto; $feature{"crypto"} = $has_crypto;
$feature{"debug"} = $debug_build; $feature{"debug"} = $debug_build;
@ -3035,6 +3037,9 @@ sub checksystem {
# alt-svc enabled # alt-svc enabled
$has_altsvc=1; $has_altsvc=1;
} }
if($feat =~ /HSTS/i) {
$has_hsts=1;
}
if($feat =~ /AsynchDNS/i) { if($feat =~ /AsynchDNS/i) {
if(!$has_cares) { if(!$has_cares) {
# this means threaded resolver # this means threaded resolver

View File

@ -34,7 +34,8 @@ UNITPROGS = unit1300 unit1301 unit1302 unit1303 unit1304 unit1305 unit1307 \
unit1600 unit1601 unit1602 unit1603 unit1604 unit1605 unit1606 unit1607 \ unit1600 unit1601 unit1602 unit1603 unit1604 unit1605 unit1606 unit1607 \
unit1608 unit1609 unit1610 unit1611 unit1612 \ unit1608 unit1609 unit1610 unit1611 unit1612 \
unit1620 unit1621 \ unit1620 unit1621 \
unit1650 unit1651 unit1652 unit1653 unit1654 unit1655 unit1650 unit1651 unit1652 unit1653 unit1654 unit1655 \
unit1660
unit1300_SOURCES = unit1300.c $(UNITFILES) unit1300_SOURCES = unit1300.c $(UNITFILES)
unit1300_CPPFLAGS = $(AM_CPPFLAGS) unit1300_CPPFLAGS = $(AM_CPPFLAGS)
@ -154,3 +155,5 @@ unit1654_CPPFLAGS = $(AM_CPPFLAGS)
unit1655_SOURCES = unit1655.c $(UNITFILES) unit1655_SOURCES = unit1655.c $(UNITFILES)
unit1655_CPPFLAGS = $(AM_CPPFLAGS) unit1655_CPPFLAGS = $(AM_CPPFLAGS)
unit1660_SOURCES = unit1660.c $(UNITFILES)
unit1660_CPPFLAGS = $(AM_CPPFLAGS)

172
tests/unit/unit1660.c Normal file
View File

@ -0,0 +1,172 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.haxx.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
***************************************************************************/
#include "curlcheck.h"
#include "urldata.h"
#include "hsts.h"
static CURLcode
unit_setup(void)
{
return CURLE_OK;
}
static void
unit_stop(void)
{
curl_global_cleanup();
}
#if defined(CURL_DISABLE_HTTP) || !defined(USE_HSTS)
UNITTEST_START
{
return 0; /* nothing to do when HTTP or HSTS are disabled */
}
UNITTEST_STOP
#else
struct testit {
const char *host;
const char *chost; /* if non-NULL, use to lookup with */
const char *hdr; /* if NULL, just do the lookup */
const CURLcode result; /* parse result */
};
static const struct testit headers[] = {
/* two entries read from disk cache, verify first */
{ "-", "readfrom.example", NULL, CURLE_OK},
{ "-", "old.example", NULL, CURLE_OK},
/* delete the remaining one read from disk */
{ "readfrom.example", NULL, "max-age=\"0\"", CURLE_OK},
{ "example.com", NULL, "max-age=\"31536000\"\r\n", CURLE_OK },
{ "example.com", NULL, "max-age=\"21536000\"\r\n", CURLE_OK },
{ "example.com", NULL, "max-age=\"21536000\"; \r\n", CURLE_OK },
{ "example.com", NULL, "max-age=\"21536000\"; includeSubDomains\r\n",
CURLE_OK },
{ "example.org", NULL, "max-age=\"31536000\"\r\n", CURLE_OK },
{ "this.example", NULL, "max=\"31536\";", CURLE_BAD_FUNCTION_ARGUMENT },
{ "this.example", NULL, "max-age=\"31536", CURLE_BAD_FUNCTION_ARGUMENT },
{ "this.example", NULL, "max-age=31536\"", CURLE_OK },
/* max-age=0 removes the entry */
{ "this.example", NULL, "max-age=0", CURLE_OK },
{ "another.example", NULL, "includeSubDomains; ",
CURLE_BAD_FUNCTION_ARGUMENT },
/* Two max-age is illegal */
{ "example.com", NULL,
"max-age=\"21536000\"; includeSubDomains; max-age=\"3\";",
CURLE_BAD_FUNCTION_ARGUMENT },
/* Two includeSubDomains is illegal */
{ "2.example.com", NULL,
"max-age=\"21536000\"; includeSubDomains; includeSubDomains;",
CURLE_BAD_FUNCTION_ARGUMENT },
/* use a unknown directive "include" that should be ignored */
{ "3.example.com", NULL, "max-age=\"21536000\"; include; includeSubDomains;",
CURLE_OK },
/* remove the "3.example.com" one, should still match the example.com */
{ "3.example.com", NULL, "max-age=\"0\"; includeSubDomains;",
CURLE_OK },
{ "-", "foo.example.com", NULL, CURLE_OK},
{ "-", "foo.xample.com", NULL, CURLE_OK},
/* should not match */
{ "example.net", "forexample.net", "max-age=\"31536000\"\r\n", CURLE_OK },
/* should not match either, since forexample.net is not in the example.net
domain */
{ "example.net", "forexample.net",
"max-age=\"31536000\"; includeSubDomains\r\n", CURLE_OK },
/* remove example.net again */
{ "example.net", NULL, "max-age=\"0\"; includeSubDomains\r\n", CURLE_OK },
/* make this live for 7 seconds */
{ "expire.example", NULL, "max-age=\"7\"\r\n", CURLE_OK },
{ NULL, NULL, NULL, 0 }
};
static void showsts(struct stsentry *e, const char *chost)
{
if(!e)
printf("'%s' is not HSTS\n", chost);
else {
printf("%s [%s]: %" CURL_FORMAT_CURL_OFF_T "%s\n",
chost, e->host, e->expires,
e->includeSubDomains ? " includeSubDomains" : "");
}
}
UNITTEST_START
{
CURLcode result;
struct stsentry *e;
struct hsts *h = Curl_hsts_init();
int i;
const char *chost;
CURL *easy;
if(!h)
return 1;
Curl_hsts_load(h, "log/input1660");
for(i = 0; headers[i].host ; i++) {
if(headers[i].hdr) {
result = Curl_hsts_parse(h, headers[i].host, headers[i].hdr);
if(result != headers[i].result) {
fprintf(stderr, "Curl_hsts_parse(%s) failed: %d\n",
headers[i].hdr, result);
unitfail++;
continue;
}
else if(result) {
printf("Input %u: error %d\n", i, (int) result);
continue;
}
}
chost = headers[i].chost ? headers[i].chost : headers[i].host;
e = Curl_hsts(h, chost, TRUE);
showsts(e, chost);
}
printf("Number of entries: %d\n", h->list.size);
/* verify that it is exists for 7 seconds */
chost = "expire.example";
for(i = 100; i < 110; i++) {
e = Curl_hsts(h, chost, TRUE);
showsts(e, chost);
deltatime++; /* another second passed */
}
easy = curl_easy_init();
if(easy) {
(void)Curl_hsts_save(easy, h, "log/hsts1660");
curl_easy_cleanup(easy);
}
Curl_hsts_cleanup(&h);
return unitfail;
}
UNITTEST_STOP
#endif