#!/bin/bash # # Apache 2 Tools. # # For suggestion and bug reports, please contact # Pierre-Yves Landuré version=3.5.1 # History # ------- # # 3.5.1: # - Add fix for 503 error in HTTP/1.1 to HTTP/2.0 reverse proxy. # 3.5.0: # - Enable HTTP 2 in hosts when mod_http2.c is available. # 3.4.1: # - Fix acme.sh path detection bug and missing certificate paths. # 3.4.0: # - Enable custom reverse proxy settings with --proxy-settings= argument. # 3.3.0: # - Fix most of shellcheck warnings. # - Use new convention for variable names. # - Add X-SendFile support. # - Use _default_ as default SSL ip if possible. # 3.2.3: # - Fix null byte warning in basename. # 3.2.2: # - add test for version < 2.4 on allow/deny directives. # 3.2.1: # - bug fix for redirect template with acme. # 3.2.0: # - add support for Let's Encrypt via acme.sh. # - use cecho to add some colors in message output. # # Get the basename of a path (multi-plateform version) # Print the result on &1 if found. # # @param string $path A path. # # @return A return code.. function basename() { [[ ${#} -eq 0 ]] && exit 1 case "$(uname)" in 'Linux' ) command basename -z -- "${@}" \ | command tr -d '\0' ;; 'Darwin' | * ) command basename -- "${@}" ;; esac return ${?} } # basename() export -f 'basename' scriptName="$(basename "${0}")" # Print this script help. function usage { command echo "Apache 2 Tools v${version} This tool ease Apache 2 HTTP server administration from command line. Usage : ${scriptName} --template={ vhost | virtual-host } [ options ] domain.name /path/to/www Setup a named virtualhost using domain.name as ServerName and /path/to/www as DocumentRoot. ${scriptName} --template={ rproxy | reverse-proxy } [ options ] [ --proxy-settings='connectiontimeout=600 timeout=4000 Keepalive=On' ] domain.name http://server.local/ Setup a reverse proxy using domain.name as ServerName and http://server.local/ proxied server. ${scriptName} --template=redirect [ options ] domain.name http://domain.com/ Setup a permanent redirection from http://domain.name/ to http://domain.com/ as target url. ${scriptName} --template=custom [ options ] domain.name '# Custom Apache 2 configuration chunck.' Setup a customized virtual host using domain.name as ServerName. ${scriptName} --remove domain.name Remove all virtualhosts for the given domain. Available options are: * --help | -h Display this message. * --template= | -t { virtual-host | reverse-proxy | redirect | custom } Set the template to use to create the Apache 2 configuration. Available templates are: * vhost | virtual-host : A standard VirtualHost with a DocumentRoot. * rproxy | reverse-proxy : A reverse proxy VirtualHost. * redirect : A VirtualHost that performs a permanent redirection. * custom : A customized VirtualHost for specific configurations. If template option is not used, the 'virtual-host' template is used by default. * --remove | -d Remove all virtualhosts for the given domain. * --alias= | -a other-domain.name Set ServerAlias directive. You can provide a list of domain names separated by spaces. This option can be used more than once. * --overrides= | -o All Set AllowOverride directive (only for 'virtual-host' template). * --without-multiviews Disable MultiViews for VirtualHost. Can fix some rewriting problems. * --ssl= | -s { key_name | auto } Create a HTTP virtual host with mod_ssl. Require a key name or --private-key and --public-key If 'auto' is used instead of a key name, a Let's Encrypt certificate is created (require acme.sh installed for root user, see https://github.com/Neilpang/acme.sh ). Using the key_name option set the following defaults: - private-key : /etc/ssl/private/key_name.key - public-key : /etc/ssl/certificates/key_name.crt - root-ca : /etc/ssl/roots/key_name-root.ca - chain-ca : /etc/ssl/chains/key_name.ca * --private-key= | --key= | -k /etc/ssl/private/domain.com.key Set the SSL private key (mandatory with --ssl). * --public-key= | --cert= | -p /etc/ssl/certificates/domain.com.crt Set the SSL public key (mandatory with --ssl). * --root-ca= | --root= | -r /etc/ssl/roots/domain.com-root.ca Set the SSL root certificate (with --ssl only). * --chain-ca= | --chain= | -c /etc/ssl/chains/domain.com.ca Set the SSL intermediate certificate (with --ssl only). * --bind-ip= | --ip= | -i xx.xx.xx.xx Set the bind IP for a SSL virtual host (with --ssl only). * --xsendfile= | --xsf= /var/www/html/path Allow X-SendFile header for given path. This option can be used multiple times. * --proxy-settings= | --ps= 'connectiontimeout=600 timeout=4000 Keepalive=On' Replace default reverse proxy settings. * --custom-options= | --co= '# A additional Apache configuration snippet' Add custom Apache configuration snippets at the end of generated file. This option can be used multiple times. " [[ -n "${1}" ]] && exit "${1}" } # usage # Common Apache 2 Vhost template VHOST_TEMPLATE[0]=" # Uncomment this line and set it up with your actual webmaster email # or with your real email. #ServerAdmin webmaster@my-domain.com # Your actual domain name, on witch this virtual host is available. ServerName %_SITE_HOSTNAME_% # You may want your site to be available on other domain names, this is # what alias are for. # You can use the * wildcard caracter to match multiple sub-domains. #ServerAlias %_SERVER_ALIAS_% # Theses lines only apply of the rewrite module is enabled. # This is a security enhancement recommanded by the nessus tool. RewriteEngine on RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK) RewriteRule .* - [F] # These section tel Apache 2 that it can follow symbolic links (cf. ln -s) # on your system. This can avoid a lot of problems... as well at # it can be a security issue if the links points to /etc... # be carefull at what you link :) Options FollowSymLinks AllowOverride None " VHOST_TEMPLATE[1]=" " LOGS_TEMPLATE=" # The error log and access log. This can be used by awstats # Note : since we keed theses logs in /var/log/apache2, they are # automaticaly rotated by logrotate :D. ErrorLog \${APACHE_LOG_DIR}/%_SITE_HOSTNAME_%-error.log LogLevel warn CustomLog \${APACHE_LOG_DIR}/%_SITE_HOSTNAME_%-access.log combined " SSL_TEMPLATE=" # # SSL magic # # We enable the SSL engine. Without this line, we use HTTP, not HTTPS. SSLEngine On # Mitigating the BEAST attack on TLS # see https://community.qualys.com/blogs/securitylabs/2011/10/17/mitigating-the-beast-attack-on-tls SSLHonorCipherOrder On SSLCipherSuite EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4 # Allow RC4 for older browsers. # see https://www.digicert.com/ssl-support/ssl-enabling-perfect-forward-secrecy.htm#apache_forward_secrecy # SSLCipherSuite EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:+RC4:RC4 # We allow SSLv3 and TLSv1 only, we reject the old SSLv2. # TLSv1 is needed to support SNI (Server Name Indication). # SNI allow to have multiple SSL certificates on one IP address. SSLProtocol All +TLSv1 -SSLv2 -SSLv3 # Server public and private certificate files: SSLCertificateFile \"%_CERTIFICATE_FILE_%\" = 2.4> SSLCertificateFile \"%_CERTIFICATE_COMPLETE_FILE_%\" SSLCertificateKeyFile \"%_KEY_FILE_%\" # Server Certificate Chain: # Point SSLCertificateChainFile at a file containing the # concatenation of PEM encoded CA certificates which form the # certificate chain for the server certificate. Alternatively # the referenced file can be the same as SSLCertificateFile # when the CA certificates are directly appended to the server # certificate for convinience. SSLCertificateChainFile \"%_CHAIN_FILE_%\" # Certificate Authority (CA): # Set the CA certificate verification path where to find CA # certificates for client authentication or alternatively one # huge file containing all of them (file must be PEM encoded) # Note: Inside SSLCACertificatePath you need hash symlinks # to point to the certificate files. Use the provided # Makefile to update the hash symlinks after changes. # SSLCACertificatePath \"/etc/ssl/certs\" # SSLCACertificateFile \"%_ROOT_CA_FILE_%\" # Certificate Revocation Lists (CRL): # Set the CA revocation path where to find CA CRLs for client # authentication or alternatively one huge file containing all # of them (file must be PEM encoded) # Note: Inside SSLCARevocationPath you need hash symlinks # to point to the certificate files. Use the provided # Makefile to update the hash symlinks after changes. #SSLCARevocationPath /etc/apache2/ssl.crl/ #SSLCARevocationFile /etc/apache2/ssl.crl/ca-bundle.crl # Client Authentication (Type): # Client certificate verification type and depth. Types are # none, optional, require and optional_no_ca. Depth is a # number which specifies how deeply to verify the certificate # issuer chain before deciding the certificate is not valid. #SSLVerifyClient require #SSLVerifyDepth 10 # Access Control: # With SSLRequire you can do per-directory access control based # on arbitrary complex boolean expressions containing server # variable checks and other lookup directives. The syntax is a # mixture between C and Perl. See the mod_ssl documentation # for more details. # #SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ # and %{SSL_CLIENT_S_DN_O} eq \"Snake Oil, Ltd.\" \ # and %{SSL_CLIENT_S_DN_OU} in {\"Staff\", \"CA\", \"Dev\"} \ # and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ # and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ # or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ # # SSL Engine Options: # Set various options for the SSL engine. # o FakeBasicAuth: # Translate the client X.509 into a Basic Authorisation. This means that # the standard Auth/DBMAuth methods can be used for access control. The # user name is the 'one line' version of the client's X.509 certificate. # Note that no password is obtained from the user. Every entry in the user # file needs this password: 'xxj31ZMTZzkVA'. # o ExportCertData: # This exports two additional environment variables: SSL_CLIENT_CERT and # SSL_SERVER_CERT. These contain the PEM-encoded certificates of the # server (always existing) and the client (only existing when client # authentication is used). This can be used to import the certificates # into CGI scripts. # o StdEnvVars: # This exports the standard SSL/TLS related 'SSL_*' environment variables. # Per default this exportation is switched off for performance reasons, # because the extraction step is an expensive operation and is usually # useless for serving static content. So one usually enables the # exportation for CGI and SSI requests only. # o StrictRequire: # This denies access when 'SSLRequireSSL' or 'SSLRequire' applied even # under a 'Satisfy any' situation, i.e. when it applies access is denied # and no other module can change it. # o OptRenegotiate: # This enables optimized SSL connection renegotiation handling when SSL # directives are used in per-directory context. #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire SSLOptions +StdEnvVars SSLOptions +StdEnvVars # SSL Protocol Adjustments: # The safe and default but still SSL/TLS standard compliant shutdown # approach is that mod_ssl sends the close notify alert but doesn't wait for # the close notify alert from client. When you need a different shutdown # approach you can use one of the following variables: # o ssl-unclean-shutdown: # This forces an unclean shutdown when the connection is closed, i.e. no # SSL close notify alert is send or allowed to received. This violates # the SSL/TLS standard but is needed for some brain-dead browsers. Use # this when you receive I/O errors because of the standard approach where # mod_ssl sends the close notify alert. # o ssl-accurate-shutdown: # This forces an accurate shutdown when the connection is closed, i.e. a # SSL close notify alert is send and mod_ssl waits for the close notify # alert of the client. This is 100% SSL/TLS standard compliant, but in # practice often causes hanging connections with brain-dead browsers. Use # this only for browsers where you know that their SSL implementation # works correctly. # Notice: Most problems of broken clients are also related to the HTTP # keep-alive facility, so you usually additionally want to disable # keep-alive for those clients, too. Use variable \"nokeepalive\" for this. # Similarly, one has to force some clients to use HTTP/1.0 to workaround # their broken HTTP/1.1 implementation. Use variables \"downgrade-1.0\" and # \"force-response-1.0\" for this. BrowserMatch \"MSIE [2-6]\" \\ nokeepalive ssl-unclean-shutdown \\ downgrade-1.0 force-response-1.0 # MSIE 7 and newer should be able to use keepalive BrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown " CERTBOT_TEMPLATE=" # Certbot acme challenge. Alias \"/.well-known/acme-challenge\" \"%_CERTBOT_PATH_%\" = 2.4> Require all granted Order allow,deny Allow from all " # Apache 2 standard Virtual Host template. PATH_TEMPLATE=" # The root folder of this virtual host. DocumentRoot %_TARGET_VALUE_% # Some options for the root folder. # Read Apache 2 documentation to know exactly what is done. Options +Indexes +FollowSymLinks %_MULTI_VIEWS_% # If you want to enable overrides, you should read: # http://httpd.apache.org/docs/2.0/mod/core.html#allowoverride AllowOverride %_ALLOW_OVERRIDE_% = 2.4> Require all granted Order allow,deny Allow from all " REVERSE_PROXY_TEMPLATE=" # Do not ever never comment this line ! # This line prevent your web server to be used # as a proxy server by lurkers and other lamers. ProxyRequests Off # This little option pass the hostname to the proxyfied server. # This allow you to setup virtual hosts on the proxyfied server. # Yay ! This can be a life saver, so trust me, you want this option On. ProxyPreserveHost On # Fix IE problem (http error 408/409) BrowserMatch \"MSIE [2-6]\" proxy-nokeepalive 1 # Turn this on if the site behind the reverse proxy is on HTTPS. SSLProxyEngine %_SSL_PROXY_ENGINE_% # SSLProxyVerify none # SSLProxyCheckPeerCN off # SSLProxyCheckPeerName off SSLProxyCheckPeerExpire off # Declare the current request protocol. RequestHeader set X-Forwarded-Proto \"%_FORWARDED_PROTOCOL_%\" # Rise timeout values for failsafe on some applications (Horde ActiveSync or Plone, for example). #ProxySet %_PROXY_SETTINGS_% # Here is the magic that proxyfy the LAN server. # The first line is .... i don't remember what... # but trust me, it is usefull ;p. # The second line is a rewrite rule that do the proxy # magic. I was used to use a ProxyPass rule to do this work, but it # turned out that sometimes ProxyPass give you a 503 error when under # heavy load. The RewriteRule does not have this flaw. ProxyPassReverse / %_TARGET_VALUE_% # For HTTP2 Only: # Limit Proxy to h2 or h2c protocol when current protocol is HTTP/2.0. # For HTTP2 Only: RewriteCond %{THE_REQUEST} ^.*HTTP/2\.0$ # For HTTP2 Only: RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/.* # For HTTP2 Only: RewriteRule ^/(.*) %_HTTP2_TARGET_VALUE_%\$1 [P,L] # For HTTP2 Only: RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/.* RewriteRule ^/(.*) %_TARGET_VALUE_%\$1 [P,L] # This Location directives allow users to access to the proxyfied contents. # Do not remove this if you want your site to work. = 2.4> Require all granted Order deny,allow Allow from all " REDIRECT_TEMPLATE=" # Redirect every body to the HTTPS site. # This make sure that all users use secure version of the site. # Note the \"permanent\" : It is good for search engine optimization :D. Redirect permanent / %_TARGET_VALUE_% # This Location directives allow users to access the redirection. # Do not remove this if you want your site to work. = 2.4> Require all granted Order deny,allow Allow from all " XSENDFILE_TEMPLATE=" # Enable X-SendFile header support for path. XSendFile on %_XSENDFILE_OPTIONS_% " XSENDFILE_PATH_TEMPLATE=" XSendFilePath \"%_XSENDFILE_PATH_%\"" # h2 protocol needs https certificates. H2_SSL_TEMPLATE=" # Enable HTTP/2.0 Protocols h2 http/1.1 " # h2c stands for clear text h2. To use with http only VirtualHosts. H2_TEMPLATE=" # Enable HTTP/2.0 Protocols h2c http/1.1 " # Echo text in color. # # Colors definitions. # See http://mywiki.wooledge.org/BashFAQ/037 # # @param string $color Color and weight for text. (boldgreen for example). # @param string $text The text to echo (and echo options). function cecho() { if [[ ${#} -lt 2 ]]; then echo "${@}" return 0 fi local color="${1}" # remove color information from arguments. shift 1 # Check that the output is to a terminal. if [[ ! -t 1 ]]; then # Not outputing to a terminal, discaring colors. echo "${@}" return 0 fi # Bash 4 version with associative array. ## Color and weight definitions. #declare -A font #font['black']="$(tput 'setaf' 0)" #font['red']="$(tput 'setaf' 1)" #font['green']="$(tput 'setaf' 2)" #font['yellow']="$(tput 'setaf' 3)" #font['blue']="$(tput 'setaf' 4)" #font['magenta']="$(tput 'setaf' 5)" #font['cyan']="$(tput 'setaf' 6)" #font['white']="$(tput 'setaf' 7)" #font['bgBlack']="$(tput 'setab' 0)" #font['bgRed']="$(tput 'setab' 1)" #font['bgGreen']="$(tput 'setab' 2)" #font['bgYellow']="$(tput 'setab' 3)" #font['bgBlue']="$(tput 'setab' 4)" #font['bgMagenta']="$(tput 'setab' 5)" #font['bgCyan']="$(tput 'setab' 6)" #font['bgWhite']="$(tput 'setab' 7)" #font['bold']="$(tput 'bold')" #font['stout']="$(tput 'smso')" # Standout. #font['under']="$(tput 'smul')" # Underline. #font['blink']="$(tput 'blink')" # Blinking #font['italic']="$(tput 'sitm')" ## Parse the color string. #for key in "${!font[@]}"; do # [[ "${color}" = *"${key}"* ]] && echo -n "${font[${key}]}" #done declare -a fontIndex declare -a fontValue local index=0 fontIndex[$index]='black'; fontValue[$index]="$(tput 'setaf' 0)"; ((index++)) fontIndex[$index]='red'; fontValue[$index]="$(tput 'setaf' 1)"; ((index++)) fontIndex[$index]='green'; fontValue[$index]="$(tput 'setaf' 2)"; ((index++)) fontIndex[$index]='yellow'; fontValue[$index]="$(tput 'setaf' 3)"; ((index++)) fontIndex[$index]='blue'; fontValue[$index]="$(tput 'setaf' 4)"; ((index++)) fontIndex[$index]='magenta'; fontValue[$index]="$(tput 'setaf' 5)"; ((index++)) fontIndex[$index]='cyan'; fontValue[$index]="$(tput 'setaf' 6)"; ((index++)) fontIndex[$index]='white'; fontValue[$index]="$(tput 'setaf' 7)"; ((index++)) fontIndex[$index]='bgBlack'; fontValue[$index]="$(tput 'setab' 0)"; ((index++)) fontIndex[$index]='bgRed'; fontValue[$index]="$(tput 'setab' 1)"; ((index++)) fontIndex[$index]='bgGreen'; fontValue[$index]="$(tput 'setab' 2)"; ((index++)) fontIndex[$index]='bgYellow'; fontValue[$index]="$(tput 'setab' 3)"; ((index++)) fontIndex[$index]='bgBlue'; fontValue[$index]="$(tput 'setab' 4)"; ((index++)) fontIndex[$index]='bgMagenta'; fontValue[$index]="$(tput 'setab' 5)"; ((index++)) fontIndex[$index]='bgCyan'; fontValue[$index]="$(tput 'setab' 6)"; ((index++)) fontIndex[$index]='bgWhite'; fontValue[$index]="$(tput 'setab' 7)"; ((index++)) fontIndex[$index]='bold'; fontValue[$index]="$(tput 'bold')"; ((index++)) fontIndex[$index]='stout'; fontValue[$index]="$(tput 'smso')"; ((index++)) # Standout. fontIndex[$index]='under'; fontValue[$index]="$(tput 'smul')"; ((index++)) # Underline. fontIndex[$index]='blink'; fontValue[$index]="$(tput 'blink')"; ((index++)) # Blinking. fontIndex[$index]='italic'; fontValue[$index]="$(tput 'sitm')"; ((index++)) for key in "${!fontIndex[@]}"; do [[ "${color}" = *"${fontIndex[${key}]}"* ]] && echo -n "${fontValue[${key}]}" done # Output the text. echo "${@}" # Reset all attributes. tput 'sgr0' return 0 } # cecho() export -f 'cecho' # Get the absolute path for a file or directory. # Does not resolve symbolic links. # Print the result on &1 if found. # # @param string $path A relative path. # # @return A code. function absolutepath() { if [ -d "${1}" ]; then # Path point to a directory. echo "$(cd "${1}" 2>'/dev/null' || exit 1; pwd)" elif [ -f "${1}" ]; then # Path point to a file. if [[ "${1}" = '/'* ]]; then echo "${1}" elif [[ "${1}" == *'/'* ]]; then echo "$(cd "${1%/*}" 2>'/dev/null' || exit 1; pwd)/${1##*/}" else echo "$(pwd)/${1}" fi fi } # absolutepath # Get the absolute real path for a file or directory. # Resolve symbolic links. # Print the result on &1 if found. # # @param string $path A relative path. # # @return A code. function realpath { command test ${#} -ne 1 && exit 1 command readlink -f "${1}" return ${?} } # realpath # Get the absolute real path for a file or directory and check the file existance. # Resolve symbolic links. # Print the result on &1 if found. # If the file does not exists, display an error message and exit the script. # # @param string $path A relative path. # # @return A code. function realpath_check { command test ${#} -ne 1 && exit 1 realpath="$(realpath "${1}")" if [[ -n "${realpath}" && ! -e "${realpath}" ]]; then realpath="" fi if [[ -z "${realpath}" ]]; then cecho "red" "Error : File '${1}' does not exists." >&2 exit 1 fi echo "${realpath}" return 0 } # realpath_check # Check if a binary is present. Print its path on &1 if found. # # @param string $binary The binaries to check, separated by ;. # @param string $package The package the binary come from. # # @return Exit with error if the binary is missing. function check_binary() { [[ ${#} -ne 2 ]] && exit 1 local primary local binaries local binary primary="${1%%;*}" binaries=() read -d ';' -r -a binaries <<< "${1}" # Test the binary presence. for binary in "${binaries[@]}"; do if type "${binary}" &>'/dev/null'; then command -v "${binary}" return 0 fi done cecho 'redbold' "Error: '${primary}' is missing. Please install package '${2}'." >&2 exit 1 } # check_binary() # Read the eth0 IP. # # @return A IP address. function eth0_ip { command ifconfig eth0 \ | command grep "inet " \ | command sed -e 's/.*inet [^:]*:\([^ ]*\).*/\1/' } # eth0_ip ####################################################################################### ####################################################################################### ####################################################################################### # Include from /usr/share/doc/bash-doc/examples/functions/getoptx.bash of package bash-doc. ####################################################################################### ####################################################################################### ####################################################################################### function getoptex() { let $# || return 1 local optionList optionList="${1#;}" let optionIndex || optionIndex=1 [[ ${optionIndex} -lt $# ]] || return 1 shift ${optionIndex} if [[ "${1}" != "-" && "${1}" != "${1#-}" ]]; then optionIndex=$((optionIndex + 1)); if [[ "${1}" != "--" ]]; then local o o="-${1#-${optionOfs}}" for opt in ${optionList#;} do optionName="${opt%[;.:]}" unset optionArgument local optionType="${opt##*[^;:.]}" [[ -z "${optionType}" ]] && optionType=";" if [[ ${#optionName} -gt 1 ]]; then # long-named option case "$o" in "--${optionName}") if [[ "${optionType}" != ":" ]]; then return 0 fi optionArgument="$2" if [[ -z "${optionArgument}" ]]; then # error: must have an agrument let optionError && echo "$0: error: ${optionName} must have an argument" >&2 optionArgument="${optionName}"; optionName="?" return 1; fi optionIndex=$((optionIndex + 1)) # skip option's argument return 0 ;; "--${optionName}="*) if [[ "${optionType}" = ";" ]]; then # error: must not have arguments let optionError && echo "$0: error: ${optionName} must not have arguments" >&2 optionArgument="${optionName}" optionName="?" return 1 fi optionArgument=${o#--${optionName}=} return 0 ;; esac else # short-named option case "$o" in "-${optionName}") unset optionOfs [[ "${optionType}" != ":" ]] && return 0 optionArgument="$2" if [[ -z "${optionArgument}" ]]; then echo "$0: error: -${optionName} must have an argument" >&2 optionArgument="${optionName}" optionName="?" return 1 fi optionIndex=$(( optionIndex + 1 )) # skip option's argument return 0 ;; "-${optionName}"*) if [ ${optionType} = ";" ] then # an option with no argument is in a chain of options optionOfs="${optionOfs}?" # move to the next option in the chain optionIndex=$(( optionIndex - 1 )) # the chain still has other options return 0 else unset optionOfs optionArgument="${o#-${optionName}}" return 0 fi ;; esac fi done echo "Error : invalid option : '${o}'." >&2 usage 1 fi; fi optionName="?" unset optionArgument return 1 } function optionListex { local l="${1}" local m # mask local r # to store result while [[ ${#m} -lt $((${#l}-1)) ]]; do m="$m?"; done # create a "???..." mask while [[ -n "$l" ]] do r="${r:+"$r "}${l%$m}" # append the first character of $l to $r l="${l#?}" # cut the first charecter from $l m="${m#?}" # cut one "?" sign from m if [[ -n "${l%%[^:.;]*}" ]] then # a special character (";", ".", or ":") was found r="$r${l%$m}" # append it to $r l="${l#?}" # cut the special character from l m="${m#?}" # cut one more "?" sign fi done echo "${r}" } function getopt() { local optionList optionList="$(optionListex "${1}")" shift getoptex "${optionList}" "${@}" return ${?} } ####################################################################################### ####################################################################################### ####################################################################################### # Reload Apache 2 configuration only if valid. # # @return void function apache2_reload { if command apache2ctl -t > /dev/null 2>&1; then if [ -n "$(command which 'service')" ]; then command service 'apache2' 'reload' else /etc/init.d/apache2 'reload' fi else cecho "red" "Error in Apache 2 configuration : reload cancelled." exit 1 fi } # apache2_reload # Force Apache 2 configuration reload only if valid. # # @return void function apache2_force_reload { if command apache2ctl -t > /dev/null 2>&1; then if [ -n "$(command which 'service')" ]; then command service 'apache2' 'force-reload' else /etc/init.d/apache2 'force-reload' fi else cecho "red" "Error in Apache 2 configuration : forced reload cancelled." exit 1 fi } # apache2_force_reload # Check for binaries presence check_binary 'basename' 'coreutils' > '/dev/null' check_binary 'dirname' 'coreutils' > '/dev/null' check_binary 'tar' 'tar' > '/dev/null' check_binary 'mktemp' 'mktemp' > '/dev/null' check_binary 'sed' 'sed' > '/dev/null' check_binary 'apache2ctl' 'apache2' > '/dev/null' #check_binary 'wget' 'wget' > '/dev/null' #check_binary 'unzip' 'unzip' > '/dev/null' #check_binary 'apg' 'apg' > '/dev/null' #check_binary 'mysql' 'mysql-client' > '/dev/null' # Setup defaults mainTemplate="${PATH_TEMPLATE}" customOptions='' targetValueIsPath='1' serverAlias='' sslEnabled='0' sslCertbot='0' sslCertbotBasePath='/var/lib/a2tools' sslBindIp='' allowOverride='None' isRemoval='0' multiViews='+MultiViews' proxySettings='connectiontimeout=600 timeout=4000 Keepalive=On' sslAcmeSh="${HOME}/.acme.sh/acme.sh" if [[ ! -e "${sslAcmeSh}" ]]; then sslAcmeSh="$(eval 'echo ~root')/.acme.sh/acme.sh" fi declare -a xsendfilePaths xsendfilePaths=() typePrefix="" protocol="http" apacheModules=( "rewrite" "http2" ) # Parse options using getoptex from /usr/share/doc/bash-doc/examples/functions/getoptx.bash while getoptex "help h template: t: ssl. s. alias: a: private-key: key: k: public-key: crt: p: root-ca: root: r: chain-ca: chain: c: bind-ip: ip: i: overrides: o: without-multiviews remove d xsendfile: xsf: custom-options: co: proxy-settings: ps:" "${@}"; do # Options debuging. # echo "Option <${optionName}> ${optionArgument:+has an arg <${optionArgument}>}" case "${optionName}" in 'template' | 't' ) case "${optionArgument}" in vhost | virtual-host ) mainTemplate="${PATH_TEMPLATE}" targetValueIsPath="1" ;; rproxy | reverse-proxy ) mainTemplate="${REVERSE_PROXY_TEMPLATE}" targetValueIsPath="0" apacheModules+=( "proxy_http" "proxy_http2" "headers" ) ;; redirect ) mainTemplate="${REDIRECT_TEMPLATE}" targetValueIsPath="0" typePrefix="redirect-" ;; custom ) mainTemplate="CUSTOM" targetValueIsPath="0" ;; esac ;; 'ssl' | 's' ) sslEnabled="1" protocol="https" apacheModules+=( "ssl" ) if [[ -n "${optionArgument}" ]]; then if [[ "${optionArgument}" = "auto" && -e "${sslAcmeSh}" ]]; then sslCertbot="1" fi if [[ -z "${sslPrivateKey}" \ && -e "/etc/ssl/private/${optionArgument}.key" ]]; then sslPrivateKey="/etc/ssl/private/${optionArgument}.key" fi if [[ -z "${sslCompletePublicKey}" \ && -e "/etc/ssl/certificates/${optionArgument}.crt+chain+root" ]]; then sslCompletePublicKey="/etc/ssl/certificates/${optionArgument}.crt+chain+root" fi if [[ -z "${sslPublicKey}" \ && -e "/etc/ssl/certificates/${optionArgument}.crt" ]]; then sslPublicKey="/etc/ssl/certificates/${optionArgument}.crt" if [[ -z "${sslCompletePublicKey}" ]]; then sslCompletePublicKey="${sslPublicKey}" fi fi if [[ -z "${sslRootCa}" \ && -e "/etc/ssl/roots/${optionArgument}-root.ca" ]]; then sslRootCa="/etc/ssl/roots/${optionArgument}-root.ca" fi if [[ -z "${sslChainCa}" \ && -e "/etc/ssl/chains/${optionArgument}.ca" ]]; then sslChainCa="/etc/ssl/chains/${optionArgument}.ca" fi fi ;; 'alias' | 'a' ) if [ -n "${optionArgument}" ]; then serverAlias="${serverAlias} ${optionArgument}" fi ;; 'private-key' | 'key' | 'k' ) sslPrivateKey="$(realpath_check "${optionArgument}")" ;; 'public-key' | 'crt' | 'p' ) sslPublicKey="$(realpath_check "${optionArgument}")" sslCompletePublicKey="${sslPublicKey}" ;; 'root-ca' | 'root' | 'r' ) sslRootCa="$(realpath_check "${optionArgument}")" ;; 'chain-ca' | 'chain' | 'c' ) sslChainCa="$(realpath_check "${optionArgument}")" ;; 'bind-ip' | 'ip' | 'i' ) if [ -n "${optionArgument}" ]; then sslBindIp="${optionArgument}" fi ;; 'overrides' | 'o' ) if [ -n "${optionArgument}" ]; then allowOverride="${optionArgument}" fi ;; 'without-multiviews' ) multiViews="-MultiViews" ;; 'remove' | 'd' ) isRemoval="1" ;; 'xsendfile' | 'xsf' ) xsendfilePath="$(absolutepath "${optionArgument}")" xsendfilePaths+=("${xsendfilePath}") ;; 'proxy-settings' | 'ps' ) proxySettings="${optionArgument}" ;; 'custom-options' | 'co' ) customOptions="${customOptions} ${optionArgument}" ;; 'help' | 'h' | '*' ) usage 0 ;; esac done # Discard processed options. shift $((optionIndex - 1)) # Fetch script path. scriptPath="$(realpath "${0}")" # Process retro-compatibility. case "${1}" in 'add-virtual-host' | 'add-vhost' ) ${scriptPath} --template="virtual-host" \ --overrides="${4}" \ "${2}" "${3}" exit 0 ;; 'add-reverse-proxy' | 'add-reverse' ) ${scriptPath} --template="reverse-proxy" \ "${2}" "${3}" exit 0 ;; 'add-ssl-virtual-host' | 'add-ssl-vhost' ) ${scriptPath} --template="virtual-host" --ssl \ --private-key="${4}" --public-key="${5}" \ --bind-ip="${6}" --overrides="${7}" \ "${2}" "${3}" exit 0 ;; 'add-ssl-reverse-proxy' ) ${scriptPath} --template="reverse-proxy" --ssl \ --private-key="${4}" --public-key="${5}" \ --bind-ip="${6}" \ "${2}" "${3}" exit 0 ;; 'add-redirect' ) ${scriptPath} --template="redirect" \ "${2}" "${3}" exit 0 ;; 'add-ssl-redirect' ) ${scriptPath} --template="redirect" --ssl \ --private-key="${4}" --public-key="${5}" \ --bind-ip="${6}" \ "${2}" "${3}" exit 0 ;; 'add-custom' ) ${scriptPath} --template="custom" \ "${2}" "${3}" exit 0 ;; 'add-ssl-custom' ) ${scriptPath} --template="custom" --ssl \ --private-key="${4}" --public-key="${5}" \ --bind-ip="${6}" \ "${2}" "${3}" exit 0 ;; 'remove' ) ${scriptPath} --remove "${2}" exit 0 ;; esac siteHostname=${1} if [ -z "${siteHostname}" ]; then cecho "red" "Error : no domain name has been provided." >&2 usage 1 fi if [ "${isRemoval}" = "1" ]; then # The --remove option has been given. # Process the virtualhosts removal. declare -a configFiles configFiles[0]="http-${siteHostname}.conf" configFiles[1]="https-${siteHostname}.conf" configFiles[2]="redirect-http-${siteHostname}.conf" configFiles[3]="redirect-https-${siteHostname}.conf" configFiles[4]="http-${siteHostname}" configFiles[5]="https-${siteHostname}" configFiles[6]="redirect-http-${siteHostname}" configFiles[7]="redirect-https-${siteHostname}" for configFile in "${configFiles[@]}"; do if [ -e "/etc/apache2/sites-available/${configFile}" ]; then command a2dissite "${configFile}" command rm "/etc/apache2/sites-available/${configFile}" fi done if command apache2ctl -t; then apache2_reload exit 0 fi else targetValue="${2}" # Compute the VirtualHost filename. vhostFile="${typePrefix}${protocol}-${siteHostname}.conf" # Setup defaults. sslProxyEngine="Off" # Setup certbot defaults. sslCertbotPath="${sslCertbotBasePath}/${siteHostname}" command mkdir -p "${sslCertbotPath}" if [ "${sslCertbot}" = "1" ]; then sslPrivateKey="/etc/ssl/private/${siteHostname}.key" sslCompletePublicKey="/etc/ssl/certificates/${siteHostname}.crt+chain+root" sslPublicKey="/etc/ssl/certificates/${siteHostname}.crt" sslChainCa="/etc/ssl/chains/${siteHostname}.ca" fi # Process X-SendFile options: if [[ ${#xsendfilePaths[@]} -gt 0 ]]; then xsendfileOptions='' for xsendfilePath in "${xsendfilePaths[@]}"; do xsendfileOptions="${xsendfileOptions} ${XSENDFILE_PATH_TEMPLATE//%_XSENDFILE_PATH_%/${xsendfilePath}}" done customOptions="${customOptions} ${XSENDFILE_TEMPLATE//%_XSENDFILE_OPTIONS_%/${xsendfileOptions}}" apacheModules+=( "xsendfile" ) fi # For custom template, the target value is used as template. if [ "${mainTemplate}" = "CUSTOM" ]; then mainTemplate="${targetValue}" targetValue="" else # Check for target value existence if it is supposed to be a path. if [[ "${targetValueIsPath}" = "1" ]]; then targetValue="$(realpath_check "${targetValue}")" else targetValue=$(command echo "${targetValue}/" \ | command sed -e 's|//$|/|') if [[ "${targetValue}" == "https:"* || "${targetValue}" == "h2:"* ]]; then sslProxyEngine="On" fi fi fi http2Template="${H2_SSL_TEMPLATE}" # Disable SSL template if not needed if [[ "${sslEnabled}" = "0" ]]; then SSL_TEMPLATE="" http2Template="${H2_TEMPLATE}" else if [[ "${sslCertbot}" = "0" && ( -z "${sslPrivateKey}" || -z "${sslPublicKey}" ) ]]; then cecho "red" "Error : --private-key and --public-key options are mandatory when --ssl is used." >&2 usage 1 fi # Detect IP for SSL vHost, if not provided. if [[ -z "${sslBindIp}" ]]; then # Try to detect existing SSL bind ip. sslBindIp="$(command grep '443' "/etc/apache2/sites-enabled/"* \ | command head -n 1 \ | command sed -e 's/^.*VirtualHost[\t ]*\([^:]*\):443.*$/\1/')" # If no SSL host is found, use "_default_" as bind IP. if [[ -z "${sslBindIp}" ]]; then sslBindIp="_default_" fi fi fi cecho "green" "Creating /etc/apache2/sites-available/${vhostFile}..." vhostContent="${VHOST_TEMPLATE[0]} ${LOGS_TEMPLATE} ${http2Template} ${SSL_TEMPLATE} ${CERTBOT_TEMPLATE} ${mainTemplate} ${customOptions} ${VHOST_TEMPLATE[1]}" # HTTP/2.0 preprocessing. if [[ "${targetValue}" == "h2c:"* || "${targetValue}" == "h2:"* ]]; then # Preverse HTTP2 proxy Url. http2TargetValue="${targetValue}" # Create a safe HTTP(S) Url for HTTP/1.1 connexions. targetValue="${targetValue//h2:/https:}" targetValue="${targetValue//h2c:/http:}" # Activate HTTP2 proxy rules lines, since target protocol is HTTP 2.0. vhostContent="$(echo "${vhostContent}" \ | command sed -e 's|# For HTTP2 Only: ||g')" else # Delete HTTP2 proxy rules lines, since target protocol is not HTTP 2.0. vhostContent="$(echo "${vhostContent}" \ | command sed -e '/# For HTTP2 Only: /d')" fi vhostContent="$(echo "${vhostContent}" \ | command sed -e "s|%_SITE_HOSTNAME_%|${siteHostname}|g" \ -e "s|%_SERVER_ALIAS_%|${serverAlias}|g" \ -e "s|%_TARGET_VALUE_%|${targetValue}|g" \ -e "s|%_HTTP2_TARGET_VALUE_%|${http2TargetValue}|g" \ -e "s|%_MULTI_VIEWS_%|${multiViews}|g" \ -e "s|%_ALLOW_OVERRIDE_%|${allowOverride}|g" \ -e "s|%_PROXY_SETTINGS_%|${proxySettings}|g" \ -e "s|%_SSL_PROXY_ENGINE_%|${sslProxyEngine}|" \ -e "s|%_FORWARDED_PROTOCOL_%|${protocol}|" \ -e "s|%_CERTBOT_PATH_%|${sslCertbotPath}/.well-known/acme-challenge/|g")" # Setup sed rules to enable ProxySet if needed. if [[ -n "${proxySettings}" ]]; then vhostContent="$(echo "${vhostContent}" \ | command sed -e 's|#ProxySet|ProxySet|')" fi # Setup sed rules to enable ServerAlias if needed. if [[ -n "${serverAlias}" ]]; then vhostContent="$(echo "${vhostContent}" \ | command sed -e 's|#ServerAlias|ServerAlias|')" fi # Apply SSL specific transforms. if [[ "${sslEnabled}" = "1" ]]; then # If no existing http configuration exists; if [[ ! ( -e "/etc/apache2/sites-enabled/http-${siteHostname}.conf" \ || -e "/etc/apache2/sites-enabled/redirect-http-${siteHostname}.conf" ) ]]; then # Create HTTP redirect VirtualHost with Certbot Webroot path included (for certificate validation). command "${scriptName}" -t redirect "${siteHostname}" "https://${siteHostname}/" fi if [[ "${sslCertbot}" = "1" ]]; then # Create directories for keys. command mkdir -p "$(dirname "${sslPrivateKey}")" command mkdir -p "$(dirname "${sslPublicKey}")" command mkdir -p "$(dirname "${sslChainCa}")" command mkdir -p "$(dirname "${sslCompletePublicKey}")" # Use acme.sh to create a Let's Encrypt certificate for the domain. command bash "${sslAcmeSh}" --issue --domain "${siteHostname}" --webroot "${sslCertbotPath}" # Install the certificate on the system. command bash "${sslAcmeSh}" --installcert -d "${siteHostname}" \ --keypath "${sslPrivateKey}" \ --certpath "${sslPublicKey}" \ --capath "${sslChainCa}" \ --fullchainpath "${sslCompletePublicKey}" \ --reloadcmd "service apache2 reload" fi vhostContent="$(echo "${vhostContent}" \ | command sed -e "s|*:80|${sslBindIp}:443|g" \ -e "s|%_CERTIFICATE_FILE_%|${sslPublicKey}|g" \ -e "s|%_CERTIFICATE_COMPLETE_FILE_%|${sslCompletePublicKey}|g" \ -e "s|%_KEY_FILE_%|${sslPrivateKey}|g")" if [ -n "${sslRootCa}" ]; then vhostContent="$(echo "${vhostContent}" \ | command sed -e "s|%_ROOT_CA_FILE_%|${sslRootCa}|g" \ -e "s|# SSLCACertificateFile|SSLCACertificateFile|g")" fi if [ -n "${sslChainCa}" ]; then vhostContent="$(echo "${vhostContent}" \ | command sed -e "s|%_CHAIN_FILE_%|${sslChainCa}|g" \ -e "s|# SSLCertificateChainFile|SSLCertificateChainFile|g")" fi fi echo "${vhostContent}" > "/etc/apache2/sites-available/${vhostFile}" command a2enmod "${apacheModules[@]}" > /dev/null command a2ensite "${vhostFile}" # Disable the site if there is a configuration error. if command apache2ctl -t; then apache2_reload exit 0 else command a2dissite "${vhostFile}" fi fi exit 0