diff --git a/README.md b/README.md index aec8f94..5992f77 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ the linker to perform LTO and dead code elimination. ## How to Use Add the module to the nginx configuration and configure as described below. +Note that this module requires a [resolver] configuration in the `http` block. + +[resolver]: https://nginx.org/en/docs/http/ngx_http_core_module.html#resolver ## Example Configuration @@ -54,8 +57,9 @@ resolver 127.0.0.1:53; acme_issuer example { uri https://acme.example.com/directory; - contact mailto:admin@example.test; + contact admin@example.test; state_path /var/lib/nginx/acme-example; + accept_terms_of_service; } acme_shared_zone zone=acme_shared:1M; @@ -133,39 +137,11 @@ restart unless [](#state_path) is configured. **Context:** acme_issuer An array of URLs that the ACME server can use to contact the client for issues -related to this account. +related to this account. The `mailto:` scheme will be assumed unless specified +explicitly. Can be specified multiple times. -### resolver - -**Syntax:** resolver `address` ... [ `valid` = `time` ] [ `ipv4` = `on` | `off` ] [ `ipv6` = `on` | `off` ] [ `status_zone` = `zone` ] - -**Default:** - - -**Context:** acme_issuer - -Configures name servers used to resolve names of upstream servers into -addresses. -See [resolver](https://nginx.org/en/docs/http/ngx_http_core_module.html#resolver) -for the parameter reference. - -Required, but can be inherited from the `http` block. -### resolver_timeout - -**Syntax:** resolver_timeout `time` - -**Default:** 30s - -**Context:** acme_issuer - -Sets a timeout for name resolution, for example: - -```nginx -resolver_timeout 5s; - -``` - ### ssl_trusted_certificate **Syntax:** ssl_trusted_certificate `file` @@ -203,6 +179,20 @@ help with rate-limiting ACME servers. The directory, if configured, will contain sensitive content: the account key, the issued certificates and private keys. +### accept_terms_of_service + +**Syntax:** accept_terms_of_service + +**Default:** - + +**Context:** acme_issuer + +Agree to the terms under which the ACME server is to be used. + +Some servers require the user to agree with the terms of service before +registering an account. The text is usually available on the ACME server's +website and the URL will be printed to the error log if necessary. + ### acme_shared_zone **Syntax:** acme_shared_zone `zone` = `name:size` diff --git a/src/conf.rs b/src/conf.rs index 19deab4..c0f092a 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -3,8 +3,9 @@ use core::{mem, ptr}; use nginx_sys::{ ngx_command_t, ngx_conf_parse, ngx_conf_t, ngx_http_core_srv_conf_t, ngx_str_t, ngx_uint_t, - NGX_CONF_1MORE, NGX_CONF_BLOCK, NGX_CONF_FLAG, NGX_CONF_TAKE1, NGX_HTTP_MAIN_CONF, - NGX_HTTP_MAIN_CONF_OFFSET, NGX_HTTP_SRV_CONF, NGX_HTTP_SRV_CONF_OFFSET, NGX_LOG_EMERG, + NGX_CONF_1MORE, NGX_CONF_BLOCK, NGX_CONF_FLAG, NGX_CONF_NOARGS, NGX_CONF_TAKE1, + NGX_HTTP_MAIN_CONF, NGX_HTTP_MAIN_CONF_OFFSET, NGX_HTTP_SRV_CONF, NGX_HTTP_SRV_CONF_OFFSET, + NGX_LOG_EMERG, }; use ngx::collections::Vec; use ngx::core::{Pool, Status, NGX_CONF_ERROR, NGX_CONF_OK}; @@ -71,7 +72,7 @@ pub static mut NGX_HTTP_ACME_COMMANDS: [ngx_command_t; 4] = [ ngx_command_t::empty(), ]; -static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 9] = [ +static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 8] = [ ngx_command_t { name: ngx_string!("uri"), type_: NGX_CONF_TAKE1 as ngx_uint_t, @@ -96,22 +97,6 @@ static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 9] = [ offset: 0, post: ptr::null_mut(), }, - ngx_command_t { - name: ngx_string!("resolver"), - type_: NGX_CONF_TAKE1 as ngx_uint_t, - set: Some(cmd_issuer_set_resolver), - conf: 0, - offset: 0, - post: ptr::null_mut(), - }, - ngx_command_t { - name: ngx_string!("resolver_timeout"), - type_: NGX_CONF_TAKE1 as ngx_uint_t, - set: Some(nginx_sys::ngx_conf_set_msec_slot), - conf: 0, - offset: mem::offset_of!(Issuer, resolver_timeout), - post: ptr::null_mut(), - }, ngx_command_t { name: ngx_string!("ssl_trusted_certificate"), type_: NGX_CONF_TAKE1 as ngx_uint_t, @@ -136,6 +121,14 @@ static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 9] = [ offset: mem::offset_of!(Issuer, state_path), post: ptr::null_mut(), }, + ngx_command_t { + name: ngx_string!("accept_terms_of_service"), + type_: NGX_CONF_NOARGS as ngx_uint_t, + set: Some(cmd_issuer_set_accept_tos), + conf: 0, + offset: 0, + post: ptr::null_mut(), + }, ngx_command_t::empty(), ]; @@ -308,6 +301,24 @@ extern "C" fn cmd_issuer_add_contact( _cmd: *mut ngx_command_t, conf: *mut c_void, ) -> *mut c_char { + const MAILTO: &[u8] = b"mailto:"; + + fn has_scheme(val: &[u8]) -> bool { + // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + if !val[0].is_ascii_alphabetic() { + return false; + } + + for c in val { + if c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.') { + continue; + } + return *c == b':'; + } + + false + } + let cf = unsafe { cf.as_mut().expect("cf") }; let issuer = unsafe { conf.cast::().as_mut().expect("issuer conf") }; @@ -318,11 +329,25 @@ extern "C" fn cmd_issuer_add_contact( // NGX_CONF_TAKE1 ensures that args contains 2 elements let args = cf.args(); - if core::str::from_utf8(args[1].as_bytes()).is_err() { - return c"contains invalid UTF-8 sequence".as_ptr().cast_mut(); + if args[1].is_empty() || core::str::from_utf8(args[1].as_bytes()).is_err() { + return c"invalid value".as_ptr().cast_mut(); }; - issuer.contacts.push(args[1]); + if has_scheme(args[1].as_ref()) { + issuer.contacts.push(args[1]); + } else { + let mut value = ngx_str_t::empty(); + value.len = MAILTO.len() + args[1].len; + value.data = cf.pool().alloc_unaligned(value.len).cast(); + if value.data.is_null() { + return NGX_CONF_ERROR; + } + + value.as_bytes_mut()[..MAILTO.len()].copy_from_slice(MAILTO); + value.as_bytes_mut()[MAILTO.len()..].copy_from_slice(args[1].as_ref()); + + issuer.contacts.push(value); + } NGX_CONF_OK } @@ -350,32 +375,6 @@ extern "C" fn cmd_issuer_set_account_key( NGX_CONF_OK } -extern "C" fn cmd_issuer_set_resolver( - cf: *mut ngx_conf_t, - _cmd: *mut ngx_command_t, - conf: *mut c_void, -) -> *mut c_char { - let cf = unsafe { cf.as_mut().expect("cf") }; - let issuer = unsafe { conf.cast::().as_mut().expect("issuer conf") }; - - if issuer.resolver.is_some() { - return NGX_CONF_DUPLICATE; - } - - let args = unsafe { &mut *cf.args }; - let value: *mut ngx_str_t = args.elts.cast(); - - issuer.resolver = ptr::NonNull::new(unsafe { - nginx_sys::ngx_resolver_create(cf, value.add(1), args.nelts - 1) - }); - - if issuer.resolver.is_none() { - return NGX_CONF_ERROR; - } - - NGX_CONF_OK -} - extern "C" fn cmd_issuer_set_uri( cf: *mut ngx_conf_t, _cmd: *mut ngx_command_t, @@ -400,6 +399,22 @@ extern "C" fn cmd_issuer_set_uri( NGX_CONF_OK } +extern "C" fn cmd_issuer_set_accept_tos( + _cf: *mut ngx_conf_t, + _cmd: *mut ngx_command_t, + conf: *mut c_void, +) -> *mut c_char { + let issuer = unsafe { conf.cast::().as_mut().expect("issuer conf") }; + + if issuer.accept_tos.is_some() { + return NGX_CONF_DUPLICATE; + } + + issuer.accept_tos = Some(true); + + NGX_CONF_OK +} + /* Methods and trait implementations */ impl AcmeMainConfig { diff --git a/src/conf/issuer.rs b/src/conf/issuer.rs index 8b06c5d..2f9b4fc 100644 --- a/src/conf/issuer.rs +++ b/src/conf/issuer.rs @@ -38,6 +38,7 @@ pub struct Issuer { pub ssl_trusted_certificate: ngx_str_t, pub ssl_verify: ngx_flag_t, pub state_path: *mut ngx_path_t, + pub accept_tos: Option, // Generated fields // ngx_ssl_t stores a pointer to itself in SSL_CTX ex_data. pub ssl: Box, @@ -51,7 +52,7 @@ pub enum IssuerError { AccountKey(super::ssl::CertificateFetchError), #[error("cannot generate account key: {0}")] AccountKeyGen(#[from] super::pkey::PKeyGenError), - #[error("resolver is not configured")] + #[error("\"resolver\" is not configured")] Resolver, #[error("memory allocation failed")] Alloc(#[from] AllocError), @@ -80,6 +81,7 @@ impl Issuer { ssl_trusted_certificate: ngx_str_t::empty(), ssl_verify: NGX_CONF_UNSET_FLAG, state_path: ptr::null_mut(), + accept_tos: None, ssl, pkey: None, orders: RbTreeMap::try_new_in(alloc)?, diff --git a/t/acme_conf_certificate.t b/t/acme_conf_certificate.t index 3a23688..152b591 100644 --- a/t/acme_conf_certificate.t +++ b/t/acme_conf_certificate.t @@ -54,9 +54,10 @@ http { acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; - resolver 127.0.0.1:%%PORT_8980_UDP%%; - ssl_verify off; + ssl_verify off; } + + resolver 127.0.0.1:%%PORT_8980_UDP%%; } EOF diff --git a/t/acme_conf_issuer.t b/t/acme_conf_issuer.t index bbb681c..283659d 100644 --- a/t/acme_conf_issuer.t +++ b/t/acme_conf_issuer.t @@ -24,7 +24,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http http_ssl/)->plan(8); +my $t = Test::Nginx->new()->has(qw/http http_ssl/)->plan(7); use constant TEMPLATE_CONF => <<'EOF'; @@ -67,29 +67,19 @@ acme_shared_zone zone=ngx_acme_shared:1M; acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; account_key ecdsa:256; - contact mailto:admin@example.test; - resolver 127.0.0.1:%%PORT_8980_UDP%%; - resolver_timeout 5s; + contact admin@example.test; ssl_verify off; state_path %%TESTDIR%%; -} - -EOF - - -is(check($t, <<'EOF' ), undef, 'valid - resolver in server'); - -acme_issuer example { - uri https://localhost:%%PORT_9000%%/dir; - ssl_verify off; + accept_terms_of_service; } resolver 127.0.0.1:%%PORT_8980_UDP%%; +resolver_timeout 5s; EOF -like(check($t, <<'EOF' ), qr/\[emerg].*resolver is not/, 'no resolver'); +like(check($t, <<'EOF' ), qr/\[emerg].*"resolver" is not/, 'no resolver'); acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; @@ -105,10 +95,11 @@ acme_shared_zone bad-value; acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; - resolver 127.0.0.1:%%PORT_8980_UDP%%; ssl_verify off; } +resolver 127.0.0.1:%%PORT_8980_UDP%%; + EOF @@ -118,10 +109,11 @@ acme_shared_zone zone=test:bad-size; acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; - resolver 127.0.0.1:%%PORT_8980_UDP%%; ssl_verify off; } +resolver 127.0.0.1:%%PORT_8980_UDP%%; + EOF @@ -130,10 +122,11 @@ like(check($t, <<'EOF' ), qr/\[emerg].*cannot load/, 'bad key file'); acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; account_key no-such-file.key; - resolver 127.0.0.1:%%PORT_8980_UDP%%; ssl_verify off; } +resolver 127.0.0.1:%%PORT_8980_UDP%%; + EOF @@ -142,10 +135,11 @@ like(check($t, <<'EOF' ), qr/\[emerg].*unsupported curve/, 'bad key curve'); acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; account_key ecdsa:234; - resolver 127.0.0.1:%%PORT_8980_UDP%%; ssl_verify off; } +resolver 127.0.0.1:%%PORT_8980_UDP%%; + EOF @@ -154,10 +148,11 @@ like(check($t, <<'EOF' ), qr/\[emerg].*unsupported key size/, 'bad key size'); acme_issuer example { uri https://localhost:%%PORT_9000%%/dir; account_key rsa:1024; - resolver 127.0.0.1:%%PORT_8980_UDP%%; ssl_verify off; } +resolver 127.0.0.1:%%PORT_8980_UDP%%; + EOF # stop and clear the log to avoid triggering sanitizer checks @@ -189,13 +184,13 @@ sub try_run { open STDERR, ">&", \*OLDERR; }; - return unless $@; - my $log = $t->read_file('error.log'); if ($ENV{TEST_NGINX_VERBOSE}) { map { Test::Nginx::log_core($_) } split(/^/m, $log); } + return unless $@; + return $log; }