@ -66,46 +66,49 @@ zyxel_gs1900_deploy() {
_debug _cfullchain " $_cfullchain "
_getdeployconf DEPLOY_ZYXEL_SWITCH
_getdeployconf DEPLOY_ZYXEL_SWITCH_USER
_getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD
_getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT
if [ -z " $DEPLOY_ZYXEL_SWITCH " ] ; then
_zyxel_switch_host = " $_cdomain "
else
_zyxel_switch_host = " $DEPLOY_ZYXEL_SWITCH "
_savedeployconf DEPLOY_ZYXEL_SWITCH " $DEPLOY_ZYXEL_SWITCH "
DEPLOY_ZYXEL_SWITCH = " $_cdomain "
fi
_debug2 DEPLOY_ZYXEL_SWITCH " $_zyxel_switch_host "
_getdeployconf DEPLOY_ZYXEL_SWITCH_USER
if [ -z " $DEPLOY_ZYXEL_SWITCH_USER " ] ; then
_zyxel_switch_user = "admin"
else
_zyxel_switch_user = " $DEPLOY_ZYXEL_SWITCH_USER "
_savedeployconf DEPLOY_ZYXEL_SWITCH_USER " $DEPLOY_ZYXEL_SWITCH_USER "
DEPLOY_ZYXEL_SWITCH_USER = "admin"
fi
_debug2 DEPLOY_ZYXEL_SWITCH_USER " $_zyxel_switch_user "
_getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD
if [ -z " $DEPLOY_ZYXEL_SWITCH_PASSWORD " ] ; then
_zyxel_switch_password = "1234"
else
_zyxel_switch_password = " $DEPLOY_ZYXEL_SWITCH_PASSWORD "
_savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD " $DEPLOY_ZYXEL_SWITCH_PASSWORD "
DEPLOY_ZYXEL_SWITCH_PASSWORD = "1234"
fi
_secure_debug2 DEPLOY_ZYXEL_SWITCH_PASSWORD " $_zyxel_switch_password "
_getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT
if [ -z " $DEPLOY_ZYXEL_SWITCH_REBOOT " ] ; then
_zyxel_switch_reboot = "0"
else
_zyxel_switch_reboot = " $DEPLOY_ZYXEL_SWITCH_REBOOT "
_savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT " $DEPLOY_ZYXEL_SWITCH_REBOOT "
DEPLOY_ZYXEL_SWITCH_REBOOT = "0"
fi
_debug2 DEPLOY_ZYXEL_SWITCH_REBOOT " $_zyxel_switch_reboot "
_zyxel_switch_base_uri = " https:// ${ _zyxel_switch_host } "
_savedeployconf DEPLOY_ZYXEL_SWITCH " $DEPLOY_ZYXEL_SWITCH "
_savedeployconf DEPLOY_ZYXEL_SWITCH_USER " $DEPLOY_ZYXEL_SWITCH_USER "
_savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD " $DEPLOY_ZYXEL_SWITCH_PASSWORD "
_savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT " $DEPLOY_ZYXEL_SWITCH_REBOOT "
_debug DEPLOY_ZYXEL_SWITCH " $DEPLOY_ZYXEL_SWITCH "
_debug DEPLOY_ZYXEL_SWITCH_USER " $DEPLOY_ZYXEL_SWITCH_USER "
_secure_debug DEPLOY_ZYXEL_SWITCH_PASSWORD " $DEPLOY_ZYXEL_SWITCH_PASSWORD "
_debug DEPLOY_ZYXEL_SWITCH_REBOOT " $DEPLOY_ZYXEL_SWITCH_REBOOT "
_zyxel_switch_base_uri = " https:// ${ DEPLOY_ZYXEL_SWITCH } "
_info " Beginning to deploy to a Zyxel GS1900 series switch at ${ _zyxel_switch_base_uri } . "
_zyxel_gs1900_deployment_precheck || return $?
_zyxel_gs1900_should_update
if [ " $? " != "0" ] ; then
_info "The switch already has our certificate installed. No update required."
return 0
else
_info "The switch does not yet have our certificate installed."
fi
_info "Logging into the switch web interface."
_zyxel_gs1900_login || return $?
@ -115,7 +118,7 @@ zyxel_gs1900_deploy() {
_info "Uploading the certificate."
_zyxel_gs1900_upload_certificate || return $?
if [ " $ _zyxel_switch_reboot " = "1" ] ; then
if [ " $ DEPLOY_ZYXEL_SWITCH_REBOOT " = "1" ] ; then
_info "Rebooting the switch."
_zyxel_gs1900_trigger_reboot || return $?
fi
@ -139,21 +142,19 @@ _zyxel_gs1900_deployment_precheck() {
# Check the server for some common failure modes prior to authentication and certificate upload in order to avoid
# sending a certificate when we may not want to.
_post "username=test&password=test&login=true;" " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi " '' "POST" "application/x-www-form-urlencoded" >/dev/null 2>& 1
test_login_response = $( _post "username=test&password=test&login=true;" " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi ?cmd=0.html " '' "POST" "application/x-www-form-urlencoded" 2>& 1)
test_login_page_exitcode = " $? "
_debug3 " Test Login Response: ${ test_login_response } "
if [ " $test_login_page_exitcode " -ne "0" ] ; then
if [ " ${ ACME_USE_WGET :- 0 } " = "0" ] && [ " $test_login_page_exitcode " = "56" ] ; then
_info "Warning: curl is returning exit code 56. Please re-run with --debug for more information."
_debug "If the above curl trace contains the error 'SSL routines::unexpected eof while reading, errno 0'"
_debug "please ensure you are running the latest versions of curl and openssl. For more information"
_debug "see: https://github.com/openssl/openssl/issues/18866#issuecomment-1194219601"
elif { [ " ${ ACME_USE_WGET :- 0 } " = "0" ] && [ " $test_login_page_exitcode " = "60" ] ; } || { [ " ${ ACME_USE_WGET :- 0 } " = "1" ] && [ " $test_login_page_exitcode " = "5" ] ; } ; then
if { [ " ${ ACME_USE_WGET :- 0 } " = "0" ] && [ " $test_login_page_exitcode " = "60" ] ; } || { [ " ${ ACME_USE_WGET :- 0 } " = "1" ] && [ " $test_login_page_exitcode " = "5" ] ; } ; then
_err " The SSL certificate at $_zyxel_switch_base_uri could not be validated. "
_err "Please double check your hostname, port, and that you are actually connecting to your switch."
_err "If the problem persists then please ensure that the certificate is not self-signed, has not"
_err "expired, and matches the switch hostname. If you expect validation to fail then you can disable"
_err "certificate validation by running with --insecure."
return 1
elif [ " ${ ACME_USE_WGET :- 0 } " = "0" ] && [ " $test_login_page_exitcode " = "56" ] ; then
_debug3 "Intentionally ignore curl exit code 56 in our precheck"
else
_err " Failed to submit the initial login attempt to $_zyxel_switch_base_uri . "
return 1
@ -163,11 +164,11 @@ _zyxel_gs1900_deployment_precheck() {
_zyxel_gs1900_login( ) {
# Login to the switch and set the appropriate auth cookie in _H1
username_encoded = $( printf "%s" " $ _zyxel_switch_user " | _url_encode)
password_encoded = $( _zyxel_gs1900_password_obfuscate " $ _zyxel_switch_password " | _url_encode)
username_encoded = $( printf "%s" " $ DEPLOY_ZYXEL_SWITCH_USER " | _url_encode)
password_encoded = $( _zyxel_gs1900_password_obfuscate " $ DEPLOY_ZYXEL_SWITCH_PASSWORD " | _url_encode)
login_response = $( _post " username= ${ username_encoded } &password= ${ password_encoded } &login=true; " " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi " '' "POST" "application/x-www-form-urlencoded" | tr -d '\n' )
auth_response = $( _post " authId= ${ login_response } &login_chk=true " " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi " '' "POST" "application/x-www-form-urlencoded" | tr -d '\n' )
login_response = $( _post " username= ${ username_encoded } &password= ${ password_encoded } &login=true; " " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi ?cmd=0.html " '' "POST" "application/x-www-form-urlencoded" | tr -d '\n' )
auth_response = $( _post " authId= ${ login_response } &login_chk=true " " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi ?cmd=0.html " '' "POST" "application/x-www-form-urlencoded" | tr -d '\n' )
if [ " $auth_response " != "OK" ] ; then
_err "Login failed due to invalid credentials."
_err "Please double check the configured username and password and try again."
@ -235,6 +236,21 @@ _zyxel_gs1900_validate_device_compatibility() {
return $?
}
_zyxel_gs1900_should_update( ) {
# Get the remote certificate serial number
_remote_cert = $( ${ ACME_OPENSSL_BIN :- openssl } s_client -showcerts -connect " ${ DEPLOY_ZYXEL_SWITCH } :443 " 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' )
_debug3 "_remote_cert" " $_remote_cert "
_remote_cert_serial = $( printf "%s" " ${ _remote_cert } " | ${ ACME_OPENSSL_BIN :- openssl } x509 -noout -serial)
_debug2 "_remote_cert_serial" " $_remote_cert_serial "
# Get our certificate serial number
_our_cert_serial = $( cat $_ccert | ${ ACME_OPENSSL_BIN :- openssl } x509 -noout -serial)
_debug2 "_our_cert_serial" " $_our_cert_serial "
[ " ${ _remote_cert_serial } " != " ${ _our_cert_serial } " ]
}
_zyxel_gs1900_upload_certificate( ) {
# Generate a PKCS12 certificate with a temporary password since the web interface
# requires a password be present. Then upload that certificate.
@ -254,17 +270,11 @@ _zyxel_gs1900_upload_certificate() {
return $?
fi
# Load the upload page
upload_page_html = $( _get " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi?cmd=5914 " | tr -d '\n' )
# Get the two validity dates by looking for their date format in the page (i.e. Mar 5 05:48:48 2024 GMT)
existing_validity = $( _zyxel_html_extract_dates " $upload_page_html " )
_debug2 "existing_validity" " $existing_validity "
form_xss_value = $( printf "%s" " $upload_page_html " | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g' )
_secure_debug2 "form_xss_value" " $form_xss_value "
# If a certificate exists on the switch already there will be two XSS keys - we want the first one
form_xss_value = $( printf "%s" " $form_xss_value " | head -n 1)
# Get the first instance of XSSID from the upload page
form_xss_value = $( printf "%s" " $upload_page_html " | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g' | head -n 1)
_secure_debug2 "form_xss_value" " $form_xss_value "
_info "Generating the certificate upload request"
@ -294,7 +304,8 @@ _zyxel_gs1900_upload_certificate() {
# to return a consistent body return - so we cannot inspect the result of this
# upload to determine success. We will need to re-query the certificates page
# and compare the validity dates to try and identify if they have changed.
_post " ${ upload_post_request } " " ${ _zyxel_switch_base_uri } /cgi-bin/httpuploadcert.cgi " '' "POST" " multipart/form-data; boundary= ${ upload_post_boundary } " '1' >/dev/null 2>& 1
upload_response = $( _zyxel_upload_pkcs12 " ${ upload_post_request } " " ${ upload_post_boundary } " 2>& 1)
_debug3 " Upload response: ${ upload_response } "
rm " ${ upload_post_request } "
# Pause for a few seconds to give the switch a chance to process the certificate
@ -302,18 +313,15 @@ _zyxel_gs1900_upload_certificate() {
_debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate."
sleep "4"
_debug2 "Checking to see if the certificate updated properly"
upload_page_html = $( _get " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi?cmd=5914 " | tr -d '\n' )
new_validity = $( _zyxel_html_extract_dates " $upload_page_html " )
_debug2 "new_validity" " $existing_validity "
# Check to see whether or not our update was successful
_ret = 0
if [ " $existing_validity " != " $new_validity " ] ; then
_debug2 "The certificate validity has changed. The upload must have succeeded."
_zyxel_gs1900_should_update
if [ " $? " != "0" ] ; then
_info "The certificate was updated successfully"
else
_ret = 1
_err "The certificate upload does not appear to have worked."
_err " Either the certificate provided has not changed, or the switch is returning an unexpected error ."
_err " The remote certificate does not match the certificate we tried to upload ."
_err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug."
fi
@ -323,6 +331,83 @@ _zyxel_gs1900_upload_certificate() {
return $_ret
}
# make the certificate upload request using either
# --data binary with @ for file access in CURL
# or using --post-file for wget to ensure we upload
# the pkcs12 without getting tripped up on null bytes
#
# Usage _zyxel_upload_pkcs12 [body file name] [post boundary marker]
_zyxel_upload_pkcs12( ) {
bodyfilename = " $1 "
multipartformmarker = " $2 "
_post_url = " ${ _zyxel_switch_base_uri } /cgi-bin/httpuploadcert.cgi "
httpmethod = "POST"
_postContentType = " multipart/form-data; boundary= ${ multipartformmarker } "
if [ -z " $httpmethod " ] ; then
httpmethod = "POST"
fi
_debug $httpmethod
_debug "_post_url" " $_post_url "
_debug2 "body" " $body "
_debug2 "_postContentType" " $_postContentType "
_debug2 "readBodyFromFile" " $readBodyFromFile "
_inithttp
if [ " $_ACME_CURL " ] && [ " ${ ACME_USE_WGET :- 0 } " = "0" ] ; then
_CURL = " $_ACME_CURL "
if [ " $HTTPS_INSECURE " ] ; then
_CURL = " $_CURL --insecure "
fi
if [ " $httpmethod " = "HEAD" ] ; then
_CURL = " $_CURL -I "
fi
_debug "_CURL" " $_CURL "
response = " $( $_CURL --user-agent " $USER_AGENT " -X $httpmethod -H " $_H1 " -H " $_H2 " -H " $_H3 " -H " $_H4 " -H " $_H5 " --data-binary " @ ${ bodyfilename } " " $_post_url " ) "
_ret = " $? "
if [ " $_ret " != "0" ] ; then
_err " Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret "
if [ " $DEBUG " ] && [ " $DEBUG " -ge "2" ] ; then
_err "Here is the curl dump log:"
_err " $( cat " $_CURL_DUMP " ) "
fi
fi
elif [ " $_ACME_WGET " ] ; then
_WGET = " $_ACME_WGET "
if [ " $HTTPS_INSECURE " ] ; then
_WGET = " $_WGET --no-check-certificate "
fi
_debug "_WGET" " $_WGET "
response = " $( $_WGET -S -O - --user-agent= " $USER_AGENT " --header " $_H5 " --header " $_H4 " --header " $_H3 " --header " $_H2 " --header " $_H1 " --post-file= " ${ bodyfilename } " " $_post_url " 2>" $HTTP_HEADER " ) "
_ret = " $? "
if [ " $_ret " = "8" ] ; then
_ret = 0
_debug "wget returned 8 as the server returned a 'Bad Request' response. Let's process the response later."
fi
if [ " $_ret " != "0" ] ; then
_err " Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret "
fi
if _contains " $_WGET " " -d " ; then
# Demultiplex wget debug output
cat " $HTTP_HEADER " >& 2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' " $HTTP_HEADER "
fi
# remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' " $HTTP_HEADER "
else
_ret = " $? "
_err " Neither curl nor wget have been found, cannot make $httpmethod request. "
fi
_debug "_ret" " $_ret "
printf "%s" " $response "
return $_ret
}
_zyxel_gs1900_trigger_reboot( ) {
# Trigger a reboot via the management reboot page in the web ui
reboot_page_html = $( _get " ${ _zyxel_switch_base_uri } /cgi-bin/dispatcher.cgi?cmd=5888 " | tr -d '\n' )
@ -340,12 +425,45 @@ _zyxel_gs1900_trigger_reboot() {
return 0
}
# html
_zyxel_html_extract_dates( ) {
html = " $1 "
# Extract all dates in the html which match the format "Mar 5 05:48:48 2024 GMT"
# Note that the number of spaces between the format sections may differ for some reason
printf "%s" " $html " | _egrep_o '[A-Za-z]{3}\s+[0-9]+\s+[0-9]+:[0-9]+:[0-9]+\s+[0-9]+\s+[A-Za-z]+'
# password
_zyxel_gs1900_password_obfuscate( ) {
# Return the password obfuscated via the same method used by the
# switch's web UI login process
echo " $1 " | awk ' {
encoded = "" ;
password = $1 ;
allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ;
len = length( $1 ) ;
pwi = length( $1 ) ;
for ( i = 1; i <= ( 321 - pwi) ; i++)
{
if ( 0 = = i % 5 && pwi > 0)
{
encoded = ( encoded) ( substr( password, pwi--, 1) ) ;
}
else if ( i = = 123)
{
if ( len < 10)
{
encoded = ( encoded) ( 0) ;
}
else
{
encoded = ( encoded) ( int( len / 10) ) ;
}
}
else if ( i = = 289)
{
encoded = ( encoded) ( len % 10)
}
else
{
encoded = ( encoded) ( substr( allowed, int( rand( ) * length( allowed) ) , 1) )
}
}
printf( "%s" , encoded) ;
} '
}
# html label
@ -359,66 +477,26 @@ _zyxel_html_table_lookup() {
return 0
}
# password
_zyxel_gs1900_password_obfuscate( ) {
# Return the password obfuscated via the same method used by the
# Zyxel Web UI login process
login_allowed_chrs = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
login_pw_arg = " $1 "
login_pw_len = " ${# login_pw_arg } "
login_pw_index = " ${# login_pw_arg } "
login_pw_obfuscated = ""
i = 1
while [ " $i " -le " $( _math "321" - " $login_pw_index " ) " ] ; do
append_chr = "0"
if [ " $(( i % 5 )) " -eq 0 ] && [ " $login_pw_index " -gt 0 ] ; then
login_pw_index = $( _math " $login_pw_index " - 1)
append_chr = $( echo " $login_pw_arg " | awk -v var = " $login_pw_index " '{ str=substr($0,var+1,1); print str }' )
elif [ " $i " -eq 123 ] ; then
if [ " ${ login_pw_len } " -lt 10 ] ; then
# The 123rd character must be 0 if the login_pw_arg is less than 10 characters
append_chr = "0"
else
# Or the login_pw_arg divided by 10 rounded down if greater than or equal to 10
append_chr = $( _math " $login_pw_len " / 10)
fi
elif [ $i -eq 289 ] ; then
# the 289th character must be the len % 10
append_chr = $( _math " $login_pw_len " % 10)
else
# add random characters for the sake of obfuscation...
rand = $( head -q /dev/urandom | tr -cd '0-9' | head -c5 | sed 's/^0\{1,\}//' )
rand = $( printf "%5d" " $rand " )
rand_idx = $( _math " $rand " % " ${# login_allowed_chrs } " )
append_chr = $( echo " $login_allowed_chrs " | awk -v var = " $rand_idx " '{ str=substr($0,var+1,1); print str }' )
fi
login_pw_obfuscated = " ${ login_pw_obfuscated } ${ append_chr } "
i = $( _math " $i " + 1)
done
printf "%s" " $login_pw_obfuscated "
}
# html
_zyxel_gs1900_get_model( ) {
html = " $1 "
model_name = $( _zyxel_html_table_lookup " $html " "Model Name:" )
printf "%s" " $model_name "
}
# html
_zyxel_gs1900_get_firmware_version( ) {
html = " $1 "
firmware_version = $( _zyxel_html_table_lookup " $html " "Firmware Version:" | _egrep_o "V[^.]+.[^(]+" )
printf "%s" " $firmware_version "
}
# version_number
_zyxel_gs1900_parse_major_version( ) {
printf "%s" " $1 " | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi'
}
# version_number
_zyxel_gs1900_parse_minor_version( ) {
printf "%s" " $1 " | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi'
}