|
|
#!/bin/bash
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# create-getssl-config - Create a config file interactively to obtain an SSL certificate using getssl
|
|
|
|
|
|
# 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 3 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 at <http://www.gnu.org/licenses/> for
|
|
|
# more details.
|
|
|
|
|
|
# Usage: create-getssl-config [-h|--help] [-d|--debug]
|
|
|
|
|
|
# Revision history:
|
|
|
# 2016-02-04 Created (v0.1)
|
|
|
# 2016-02-05 Updated to include more variables. Still not full operational. (v0.2)
|
|
|
# 2016-05-04 Corrected typo on DNS_DEL_COMMAND (v0.3)
|
|
|
# 2016-05-23 Added option to provide a blank response, for things like SANS. (0.4)
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
PROGNAME=${0##*/}
|
|
|
VERSION="0.4"
|
|
|
|
|
|
# defaults
|
|
|
CA="https://acme-staging.api.letsencrypt.org"
|
|
|
AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
|
|
|
ACCOUNT_KEY_LENGTH=4096
|
|
|
WORKING_DIR=~/.getssl
|
|
|
DOMAIN_KEY_LENGTH=4096
|
|
|
SSLCONF="$(openssl version -d | cut -d\" -f2)/openssl.cnf"
|
|
|
VALIDATE_VIA_DNS="false"
|
|
|
RELOAD_CMD=""
|
|
|
RENEW_ALLOW="30"
|
|
|
PRIVATE_KEY_ALG="rsa"
|
|
|
SERVER_TYPE="https"
|
|
|
CHECK_REMOTE="true"
|
|
|
DNS_EXTRA_WAIT=0
|
|
|
|
|
|
clean_up() { # Perform pre-exit housekeeping
|
|
|
return
|
|
|
}
|
|
|
|
|
|
error_exit() {
|
|
|
echo -e "${PROGNAME}: ${1:-"Unknown Error"}" >&2
|
|
|
clean_up
|
|
|
exit 1
|
|
|
}
|
|
|
|
|
|
graceful_exit() {
|
|
|
clean_up
|
|
|
exit
|
|
|
}
|
|
|
|
|
|
signal_exit() { # Handle trapped signals
|
|
|
case $1 in
|
|
|
INT)
|
|
|
error_exit "Program interrupted by user" ;;
|
|
|
TERM)
|
|
|
echo -e "\n$PROGNAME: Program terminated" >&2
|
|
|
graceful_exit ;;
|
|
|
*)
|
|
|
error_exit "$PROGNAME: Terminating on unknown signal" ;;
|
|
|
esac
|
|
|
}
|
|
|
|
|
|
usage() {
|
|
|
echo -e "Usage: $PROGNAME [-h|--help] [-d|--debug]"
|
|
|
}
|
|
|
|
|
|
log() {
|
|
|
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*" >> "${PROGNAME}.log"
|
|
|
}
|
|
|
|
|
|
debug() {
|
|
|
if [[ "${_USE_DEBUG:-"0"}" -eq 1 ]]; then
|
|
|
echo "$@"
|
|
|
fi
|
|
|
}
|
|
|
|
|
|
info() {
|
|
|
echo "$@"
|
|
|
}
|
|
|
|
|
|
get_user_input() {
|
|
|
prompt=$1
|
|
|
DefVal=$2
|
|
|
HelpInfo=$3
|
|
|
response=""
|
|
|
echo ""
|
|
|
validresponse="false"
|
|
|
while [[ "$validresponse" == "false" ]]; do
|
|
|
read -p "${prompt} (${DefVal}) : " response
|
|
|
if [[ -z $response ]]; then
|
|
|
debug "response blank - used default - $DefVal"
|
|
|
res=$DefVal
|
|
|
validresponse="true"
|
|
|
elif [[ "$response" == "h" ]]; then
|
|
|
echo ""
|
|
|
echo "$HelpInfo"
|
|
|
echo ""
|
|
|
elif [[ "$response" == "-" ]]; then
|
|
|
res=""
|
|
|
validresponse="true"
|
|
|
else
|
|
|
res=$response
|
|
|
validresponse="true"
|
|
|
fi
|
|
|
done
|
|
|
}
|
|
|
|
|
|
write_getssl_template() { # write out the main template file
|
|
|
cat > "$1" <<- _EOF_getssl_
|
|
|
# Uncomment and modify any variables you need
|
|
|
# The staging server is best for testing (hence set as default)
|
|
|
#CA="https://acme-staging.api.letsencrypt.org"
|
|
|
# This server issues full certificates, however has rate limits
|
|
|
#CA="https://acme-v01.api.letsencrypt.org"
|
|
|
CA="$CA"
|
|
|
|
|
|
AGREEMENT="$AGREEMENT"
|
|
|
|
|
|
# Set an email address associated with your account - generally set at account level rather than domain.
|
|
|
#ACCOUNT_EMAIL="me@example.com"
|
|
|
ACCOUNT_KEY_LENGTH=4096
|
|
|
ACCOUNT_KEY="$WORKING_DIR/account.key"
|
|
|
PRIVATE_KEY_ALG="rsa"
|
|
|
|
|
|
# The command needed to reload apache / nginx or whatever you use
|
|
|
#RELOAD_CMD=""
|
|
|
# The time period within which you want to allow renewal of a certificate
|
|
|
# this prevents hitting some of the rate limits.
|
|
|
RENEW_ALLOW="30"
|
|
|
|
|
|
# Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp,
|
|
|
# smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which
|
|
|
# will be checked for certificate expiry and also will be checked after
|
|
|
# an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true
|
|
|
SERVER_TYPE="https"
|
|
|
CHECK_REMOTE="true"
|
|
|
|
|
|
# openssl config file. The default should work in most cases.
|
|
|
SSLCONF="$SSLCONF"
|
|
|
|
|
|
# Use the following 3 variables if you want to validate via DNS
|
|
|
#VALIDATE_VIA_DNS="true"
|
|
|
#DNS_ADD_COMMAND=
|
|
|
#DNS_DEL_COMMAND=
|
|
|
# If your DNS-server needs extra time to make sure your DNS changes are readable by the ACME-server (time in seconds)
|
|
|
#DNS_EXTRA_WAIT=60
|
|
|
_EOF_getssl_
|
|
|
}
|
|
|
|
|
|
write_domain_template() { # write out a template file for a domain.
|
|
|
cat > "$1" <<- _EOF_domain_
|
|
|
# Uncomment and modify any variables you need
|
|
|
# The staging server is best for testing
|
|
|
#CA="https://acme-staging.api.letsencrypt.org"
|
|
|
# This server issues full certificates, however has rate limits
|
|
|
#CA="https://acme-v01.api.letsencrypt.org"
|
|
|
CA="$CA"
|
|
|
|
|
|
AGREEMENT="$AGREEMENT"
|
|
|
|
|
|
ACCOUNT_EMAIL="$ACCOUNT_EMAIL"
|
|
|
ACCOUNT_KEY_LENGTH=$ACCOUNT_KEY_LENGTH
|
|
|
ACCOUNT_KEY="$ACCOUNT_KEY"
|
|
|
PRIVATE_KEY_ALG="$PRIVATE_KEY_ALG"
|
|
|
|
|
|
# Additional domains - this could be multiple domains / subdomains in a comma separated list
|
|
|
SANS=${SANS}
|
|
|
|
|
|
# Acme Challenge Location. The first entry for the domain, the following ones for each additional domain.
|
|
|
# If these start with ssh: then the next variable is assumed to be the hostname and the rest the location.
|
|
|
# An ssh key will be needed to provide you with access to the remote server.
|
|
|
# If these start with ftp: or sftp: then the next variables are userid:password:servername:ACL_location
|
|
|
#ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge'
|
|
|
# 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge'
|
|
|
# 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge')
|
|
|
ACL=(${ACL[*]})
|
|
|
|
|
|
# Location for all your certs, these can either be on the server (so full path name) or using ssh as for the ACL
|
|
|
DOMAIN_CERT_LOCATION="$DOMAIN_CERT_LOCATION"
|
|
|
DOMAIN_KEY_LOCATION="$DOMAIN_KEY_LOCATION"
|
|
|
CA_CERT_LOCATION="$CA_CERT_LOCATION"
|
|
|
DOMAIN_CHAIN_LOCATION=""
|
|
|
DOMAIN_PEM_LOCATION=""
|
|
|
|
|
|
# The command needed to reload apache / nginx or whatever you use
|
|
|
RELOAD_CMD="$RELOAD_CMD"
|
|
|
# The time period within which you want to allow renewal of a certificate
|
|
|
# this prevents hitting some of the rate limits.
|
|
|
RENEW_ALLOW="$RENEW_ALLOW"
|
|
|
|
|
|
# Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp,
|
|
|
# smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which
|
|
|
# will be checked for certificate expiry and also will be checked after
|
|
|
# an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true
|
|
|
SERVER_TYPE="$SERVER_TYPE"
|
|
|
CHECK_REMOTE="$CHECK_REMOTE"
|
|
|
|
|
|
# Use the following 3 variables if you want to validate via DNS
|
|
|
VALIDATE_VIA_DNS="$VALIDATE_VIA_DNS"
|
|
|
DNS_ADD_COMMAND="$DNS_ADD_COMMAND"
|
|
|
DNS_DEL_COMMAND="$DNS_DEL_COMMAND"
|
|
|
# If your DNS-server needs extra time to make sure your DNS changes are readable by the ACME-server (time in seconds)
|
|
|
DNS_EXTRA_WAIT=$DNS_EXTRA_WAIT
|
|
|
_EOF_domain_
|
|
|
}
|
|
|
|
|
|
help_message() {
|
|
|
cat <<- _EOF_
|
|
|
$PROGNAME ver. $VERSION
|
|
|
Create a config file interactively to obtain an SSL certificate using getssl
|
|
|
|
|
|
$(usage)
|
|
|
|
|
|
Options:
|
|
|
-h, --help Display this help message and exit.
|
|
|
-d, --debug outputs debug information
|
|
|
|
|
|
_EOF_
|
|
|
return
|
|
|
}
|
|
|
|
|
|
# Trap signals
|
|
|
trap "signal_exit TERM" TERM HUP
|
|
|
trap "signal_exit INT" INT
|
|
|
|
|
|
# Parse command-line
|
|
|
while [[ -n $1 ]]; do
|
|
|
case $1 in
|
|
|
-h | --help)
|
|
|
help_message; graceful_exit ;;
|
|
|
-d | --debug)
|
|
|
_USE_DEBUG=1 ;;
|
|
|
-w)
|
|
|
shift; WORKING_DIR="$1" ;;
|
|
|
-* | --*)
|
|
|
usage
|
|
|
error_exit "Unknown option $1" ;;
|
|
|
*)
|
|
|
DOMAIN="$1" ;;
|
|
|
esac
|
|
|
shift
|
|
|
done
|
|
|
|
|
|
# Main logic
|
|
|
|
|
|
info ""
|
|
|
info "This is an interactive script to create a config file for getssl, please answer the following questions"
|
|
|
info "Just press return for using the default"
|
|
|
info "enter the letter 'h' for help / more information"
|
|
|
info "enter the minus character '-' to provide a completely empty response."
|
|
|
info ""
|
|
|
if [ -f "$WORKING_DIR/getssl.cfg" ]; then
|
|
|
debug "reading main config from existing $WORKING_DIR/getssl.cfg"
|
|
|
. "$WORKING_DIR/getssl.cfg"
|
|
|
fi
|
|
|
|
|
|
get_user_input "What is the working directory for getssl" "$WORKING_DIR" \
|
|
|
"The working directory is where getssl saves all the config and certifcates"
|
|
|
WORKING_DIR=$res
|
|
|
|
|
|
# if the "working directory" doesn't exist, then create it.
|
|
|
if [ ! -d "$WORKING_DIR" ]; then
|
|
|
debug "Making working directory - $WORKING_DIR"
|
|
|
mkdir -p "$WORKING_DIR"
|
|
|
fi
|
|
|
|
|
|
# if main config file exists, read it, else write default.
|
|
|
if [ -f "$WORKING_DIR/getssl.cfg" ]; then
|
|
|
debug "reading main config from existing $WORKING_DIR/getssl.cfg"
|
|
|
. "$WORKING_DIR/getssl.cfg"
|
|
|
else
|
|
|
write_getssl_template "$WORKING_DIR/getssl.cfg"
|
|
|
fi
|
|
|
|
|
|
get_user_input "Domain name" "${DOMAIN}" \
|
|
|
"This should be the primary domain name you want on your SSL certificate"
|
|
|
DOMAIN=$res
|
|
|
|
|
|
# need to check if domain is valid
|
|
|
|
|
|
DOMAIN_DIR="$WORKING_DIR/$DOMAIN"
|
|
|
|
|
|
if [ ! -d "$DOMAIN_DIR" ]; then
|
|
|
info "Making domain directory - $DOMAIN_DIR"
|
|
|
mkdir -p "$DOMAIN_DIR"
|
|
|
fi
|
|
|
|
|
|
#if domain config file exists, read it.
|
|
|
if [ -f "$DOMAIN_DIR/getssl.cfg" ]; then
|
|
|
debug "reading config from $DOMAIN_DIR/getssl.cfg"
|
|
|
. "$DOMAIN_DIR/getssl.cfg"
|
|
|
fi
|
|
|
|
|
|
#prompt to use staging server .... best for testing
|
|
|
#CA="https://acme-staging.api.letsencrypt.org"
|
|
|
# This server issues full certificates, however has rate limits
|
|
|
#CA="https://acme-v01.api.letsencrypt.org"
|
|
|
|
|
|
#prompt for agreement
|
|
|
get_user_input "Agreement" "${AGREEMENT}" \
|
|
|
"This is the agreement with LetsEncrypt, and shouldn't generally be changed"
|
|
|
AGREEMENT=$res
|
|
|
|
|
|
# Set an email address associated with your account - generally set at account level rather than domain.
|
|
|
get_user_input "Account email address" "${ACCOUNT_EMAIL}" \
|
|
|
"The email address that will be used by LetsEncrypt to notify you when your certificate is due for renewal"
|
|
|
ACCOUNT_EMAIL=$res
|
|
|
|
|
|
get_user_input "Account key location" "${ACCOUNT_KEY}" \
|
|
|
"The location of the account key. "
|
|
|
ACCOUNT_KEY=$res
|
|
|
|
|
|
get_user_input "Account key length" "${ACCOUNT_KEY_LENGTH}" \
|
|
|
"Account key length - the default is typically the best option"
|
|
|
ACCOUNT_KEY_LENGTH=$res
|
|
|
|
|
|
get_user_input "Domain private key algorithm" "${PRIVATE_KEY_ALG}" \
|
|
|
"Domain private key algorithm - the default is typically the best option"
|
|
|
PRIVATE_KEY_ALG=$res
|
|
|
|
|
|
get_user_input "Check server for certificate validity" "$CHECK_REMOTE" \
|
|
|
"If true, getssl will check the live server for certificate validity rather than using the local certs
|
|
|
getssl also checks after installation, that the new valid certificate is in place"
|
|
|
CHECK_REMOTE=$res
|
|
|
|
|
|
if [[ "$CHECK_REMOTE" == "true" ]]; then
|
|
|
get_user_input "server type" "$SERVER_TYPE" \
|
|
|
"This can be 'https', 'ftp', 'ftpi', 'imap', 'imaps', 'pop3', 'pop3s', 'smtp', 'smtps_deprecated', 'smtps', 'smtp_submission', 'xmpp', 'xmpps', 'ldaps' or a port number that getssl will use for certifcate checks"
|
|
|
SERVER_TYPE=$res
|
|
|
else
|
|
|
SERVER_TYPE=""
|
|
|
fi
|
|
|
|
|
|
if [[ ${SERVER_TYPE} == "https" ]] || [[ ${SERVER_TYPE} == "webserver" ]]; then
|
|
|
REMOTE_PORT=443
|
|
|
elif [[ ${SERVER_TYPE} == "ftp" ]]; then
|
|
|
REMOTE_PORT=21
|
|
|
REMOTE_EXTRA="-starttls ftp"
|
|
|
elif [[ ${SERVER_TYPE} == "ftpi" ]]; then
|
|
|
REMOTE_PORT=990
|
|
|
elif [[ ${SERVER_TYPE} == "imap" ]]; then
|
|
|
REMOTE_PORT=143
|
|
|
REMOTE_EXTRA="-starttls imap"
|
|
|
elif [[ ${SERVER_TYPE} == "imaps" ]]; then
|
|
|
REMOTE_PORT=993
|
|
|
elif [[ ${SERVER_TYPE} == "pop3" ]]; then
|
|
|
REMOTE_PORT=110
|
|
|
REMOTE_EXTRA="-starttls pop3"
|
|
|
elif [[ ${SERVER_TYPE} == "pop3s" ]]; then
|
|
|
REMOTE_PORT=995
|
|
|
elif [[ ${SERVER_TYPE} == "smtp" ]]; then
|
|
|
REMOTE_PORT=25
|
|
|
REMOTE_EXTRA="-starttls smtp"
|
|
|
elif [[ ${SERVER_TYPE} == "smtps_deprecated" ]]; then
|
|
|
REMOTE_PORT=465
|
|
|
elif [[ ${SERVER_TYPE} == "smtps" ]] || [[ ${SERVER_TYPE} == "smtp_submission" ]]; then
|
|
|
REMOTE_PORT=587
|
|
|
REMOTE_EXTRA="-starttls smtp"
|
|
|
elif [[ ${SERVER_TYPE} == "xmpp" ]]; then
|
|
|
REMOTE_PORT=5222
|
|
|
REMOTE_EXTRA="-starttls xmpp"
|
|
|
elif [[ ${SERVER_TYPE} == "xmpps" ]]; then
|
|
|
REMOTE_PORT=5269
|
|
|
elif [[ ${SERVER_TYPE} == "ldaps" ]]; then
|
|
|
REMOTE_PORT=636
|
|
|
elif [[ ${SERVER_TYPE} =~ ^[0-9]+$ ]]; then
|
|
|
REMOTE_PORT=${SERVER_TYPE}
|
|
|
else
|
|
|
error_exit "unknown server type"
|
|
|
fi
|
|
|
|
|
|
SANS="www.${DOMAIN}"
|
|
|
if [[ ! -z ${REMOTE_PORT} ]]; then
|
|
|
# Additional domains - this could be multiple domains / subdomains in a comma separated list
|
|
|
EX_CERT=$(echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null | openssl x509 2>/dev/null)
|
|
|
if [ ! -z "${EX_CERT}" ]; then
|
|
|
SANS=$(echo "$EX_CERT" | openssl x509 -noout -text 2>/dev/null| grep "Subject Alternative Name" -A2 \
|
|
|
| grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-)
|
|
|
SANS=${SANS//$'\n'/','}
|
|
|
fi
|
|
|
fi
|
|
|
|
|
|
get_user_input "Additional domain names" "${SANS}" \
|
|
|
"this could be multiple domains / subdomains in a comma separated list" \
|
|
|
"use the minus sign - if you don't want any SANS"
|
|
|
SANS=$res
|
|
|
|
|
|
get_user_input "Validate via DNS" "${VALIDATE_VIA_DNS}" \
|
|
|
"If true, getssl will use DNS to validate the domain, if false then http / https will be used"
|
|
|
VALIDATE_VIA_DNS=$res
|
|
|
|
|
|
if [[ $VALIDATE_VIA_DNS == "true" ]]; then
|
|
|
get_user_input "DNS add command" "${DNS_ADD_COMMAND}" \
|
|
|
"location/name of script which will add the token message to DNS"
|
|
|
DNS_ADD_COMMAND=$res
|
|
|
get_user_input "DNS del command" "${DNS_DEL_COMMAND}" \
|
|
|
"location/name of script which will delete the token message from DNS"
|
|
|
DNS_DEL_COMMAND=$res
|
|
|
get_user_input "DNS extra wait time" "${DNS_EXTRA_WAIT}" \
|
|
|
"delay time, to wait for DNS to propagate once changed."
|
|
|
DNS_EXTRA_WAIT=$res
|
|
|
else
|
|
|
# find IP of this server
|
|
|
LocalIP=$(dig +short @208.67.222.222 myip.opendns.com)
|
|
|
|
|
|
#loop over all domains
|
|
|
alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")
|
|
|
dn=0
|
|
|
for d in $alldomains; do
|
|
|
# find IP of domain
|
|
|
DomainIP=$(dig +short ${d})
|
|
|
# if domain is local, try and find location of files
|
|
|
if [[ "${DomainIP}" == "${LocalIP}" ]]; then
|
|
|
if [[ ! -d "/var/www/${d}/web" ]]; then
|
|
|
ACL[$dn]="/var/www/${DOMAIN}/web/.well-known/acme-challenge"
|
|
|
elif [[ ! -d "/var/www/${d}" ]]; then
|
|
|
ACL[$dn]="/var/www/${d}/.well-known/acme-challenge"
|
|
|
else
|
|
|
ACL[$dn]="/var/www/.well-known/acme-challenge"
|
|
|
fi
|
|
|
else #domain is remote
|
|
|
ACL[$dn]="ssh:${d}:/var/www/.well-known/acme-challenge"
|
|
|
fi
|
|
|
|
|
|
get_user_input "ACL for $d" "${ACL[$dn]}" \
|
|
|
"The Acme challenge location for domaind ${d}. This should be your web root plus .well-known/acme-challenge"
|
|
|
ACL[$dn]=$res
|
|
|
|
|
|
((dn++))
|
|
|
done
|
|
|
fi
|
|
|
|
|
|
# Location for all your certs, these can either be on the server (so full path name) or using ssh as for the ACL
|
|
|
#DOMAIN_CERT_LOCATION="ssh:server5:/etc/ssl/domain.crt"
|
|
|
#DOMAIN_KEY_LOCATION="ssh:server5:/etc/ssl/domain.key"
|
|
|
#CA_CERT_LOCATION="/etc/ssl/chain.crt"
|
|
|
#DOMAIN_CHAIN_LOCATION="" this is the domain cert and CA cert
|
|
|
#DOMAIN_PEM_LOCATION="" this is the domain_key. domain cert and CA cert
|
|
|
|
|
|
# The command needed to reload apache / nginx or whatever you use
|
|
|
#RELOAD_CMD=""
|
|
|
# The time period within which you want to allow renewal of a certificate
|
|
|
# this prevents hitting some of the rate limits.
|
|
|
# RENEW_ALLOW="30"
|
|
|
|
|
|
# create domain directory if it doesn't exist
|
|
|
if [ ! -d "$DOMAIN_DIR" ]; then
|
|
|
info "Making domain directory - $DOMAIN_DIR"
|
|
|
mkdir -p "$DOMAIN_DIR"
|
|
|
fi
|
|
|
|
|
|
#Write out domain config
|
|
|
write_domain_template "$DOMAIN_DIR/getssl.cfg"
|
|
|
|
|
|
# Is it worth, with this create script, setting it to run once on the "happy hacker" CA to test, before
|
|
|
# it would then change the CA ( if all OK ) and running on the live LE server ?
|
|
|
|
|
|
graceful_exit
|