functions.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. #!/bin/bash
  2. # shellcheck disable=SC2155
  3. [[ -z "${VHOST_DIR:-}" ]] && \
  4. declare -r VHOST_DIR=/etc/nginx/vhost.d
  5. [[ -z "${START_HEADER:-}" ]] && \
  6. declare -r START_HEADER='## Start of configuration add by letsencrypt container'
  7. [[ -z "${END_HEADER:-}" ]] && \
  8. declare -r END_HEADER='## End of configuration add by letsencrypt container'
  9. function check_nginx_proxy_container_run {
  10. local _nginx_proxy_container=$(get_nginx_proxy_container)
  11. if [[ -n "$_nginx_proxy_container" ]]; then
  12. if [[ $(docker_api "/containers/${_nginx_proxy_container}/json" | jq -r '.State.Status') = "running" ]];then
  13. return 0
  14. else
  15. echo "$(date "+%Y/%m/%d %T") Error: nginx-proxy container ${_nginx_proxy_container} isn't running." >&2
  16. return 1
  17. fi
  18. else
  19. echo "$(date "+%Y/%m/%d %T") Error: could not get a nginx-proxy container ID." >&2
  20. return 1
  21. fi
  22. }
  23. function ascending_wildcard_locations {
  24. # Given foo.bar.baz.example.com as argument, will output:
  25. # - *.bar.baz.example.com
  26. # - *.baz.example.com
  27. # - *.example.com
  28. local domain="${1:?}"
  29. local first_label
  30. regex="^[[:alnum:]_\-]+(\.[[:alpha:]]+)?$"
  31. until [[ "$domain" =~ $regex ]]; do
  32. first_label="${domain%%.*}"
  33. domain="${domain/${first_label}./}"
  34. echo "*.${domain}"
  35. done
  36. }
  37. function descending_wildcard_locations {
  38. # Given foo.bar.baz.example.com as argument, will output:
  39. # - foo.bar.baz.example.*
  40. # - foo.bar.baz.*
  41. # - foo.bar.*
  42. # - foo.*
  43. local domain="${1:?}"
  44. local last_label
  45. regex="^[[:alnum:]_\-]+$"
  46. until [[ "$domain" =~ $regex ]]; do
  47. last_label="${domain##*.}"
  48. domain="${domain/.${last_label}/}"
  49. echo "${domain}.*"
  50. done
  51. }
  52. function enumerate_wildcard_locations {
  53. # Goes through ascending then descending wildcard locations for a given FQDN
  54. local domain="${1:?}"
  55. ascending_wildcard_locations "$domain"
  56. descending_wildcard_locations "$domain"
  57. }
  58. function add_location_configuration {
  59. local domain="${1:-}"
  60. local wildcard_domain
  61. # If no domain was passed use default instead
  62. [[ -z "$domain" ]] && domain='default'
  63. # If the domain does not have an exact matching location file, test the possible
  64. # wildcard locations files. Use default is no location file is present at all.
  65. if [[ ! -f "${VHOST_DIR}/${domain}" ]]; then
  66. for wildcard_domain in $(enumerate_wildcard_locations "$domain"); do
  67. if [[ -f "${VHOST_DIR}/${wildcard_domain}" ]]; then
  68. domain="$wildcard_domain"
  69. break
  70. fi
  71. domain='default'
  72. done
  73. fi
  74. if [[ -f "${VHOST_DIR}/${domain}" && -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "${VHOST_DIR}/${domain}") ]]; then
  75. # If the config file exist and already have the location configuration, end with exit code 0
  76. return 0
  77. else
  78. # Else write the location configuration to a temp file ...
  79. echo "$START_HEADER" > "${VHOST_DIR}/${domain}".new
  80. cat /app/nginx_location.conf >> "${VHOST_DIR}/${domain}".new
  81. echo "$END_HEADER" >> "${VHOST_DIR}/${domain}".new
  82. # ... append the existing file content to the temp one ...
  83. [[ -f "${VHOST_DIR}/${domain}" ]] && cat "${VHOST_DIR}/${domain}" >> "${VHOST_DIR}/${domain}".new
  84. # ... and copy the temp file to the old one (if the destination file is bind mounted, you can't change
  85. # its inode from within the container, so mv won't work and cp has to be used), then remove the temp file.
  86. cp -f "${VHOST_DIR}/${domain}".new "${VHOST_DIR}/${domain}" && rm -f "${VHOST_DIR}/${domain}".new
  87. return 1
  88. fi
  89. }
  90. function remove_all_location_configurations {
  91. for file in "${VHOST_DIR}"/*; do
  92. [[ -e "$file" ]] || continue
  93. if [[ -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "$file") ]]; then
  94. sed "/$START_HEADER/,/$END_HEADER/d" "$file" > "$file".new
  95. cp -f "$file".new "$file" && rm -f "$file".new
  96. fi
  97. done
  98. }
  99. function check_cert_min_validity {
  100. # Check if a certificate ($1) is still valid for a given amount of time in seconds ($2).
  101. # Returns 0 if the certificate is still valid for this amount of time, 1 otherwise.
  102. local cert_path="$1"
  103. local min_validity="$(( $(date "+%s") + $2 ))"
  104. local cert_expiration
  105. cert_expiration="$(openssl x509 -noout -enddate -in "$cert_path" | cut -d "=" -f 2)"
  106. cert_expiration="$(date --utc --date "${cert_expiration% GMT}" "+%s")"
  107. [[ $cert_expiration -gt $min_validity ]] || return 1
  108. }
  109. function get_self_cid {
  110. local self_cid=""
  111. # Try the /proc files methods first then resort to the Docker API.
  112. if [[ -f /proc/1/cpuset ]]; then
  113. self_cid="$(grep -Eo '[[:alnum:]]{64}' /proc/1/cpuset)"
  114. fi
  115. if [[ ( ${#self_cid} != 64 ) && ( -f /proc/self/cgroup ) ]]; then
  116. self_cid="$(grep -Eo -m 1 '[[:alnum:]]{64}' /proc/self/cgroup)"
  117. fi
  118. if [[ ( ${#self_cid} != 64 ) ]]; then
  119. self_cid="$(docker_api "/containers/$(hostname)/json" | jq -r '.Id')"
  120. fi
  121. # If it's not 64 characters long, then it's probably not a container ID.
  122. if [[ ${#self_cid} == 64 ]]; then
  123. echo "$self_cid"
  124. else
  125. echo "$(date "+%Y/%m/%d %T"), Error: can't get my container ID !" >&2
  126. return 1
  127. fi
  128. }
  129. ## Docker API
  130. function docker_api {
  131. local scheme
  132. local curl_opts=(-s)
  133. local method=${2:-GET}
  134. # data to POST
  135. if [[ -n "${3:-}" ]]; then
  136. curl_opts+=(-d "$3")
  137. fi
  138. if [[ -z "$DOCKER_HOST" ]];then
  139. echo "Error DOCKER_HOST variable not set" >&2
  140. return 1
  141. fi
  142. if [[ $DOCKER_HOST == unix://* ]]; then
  143. curl_opts+=(--unix-socket ${DOCKER_HOST#unix://})
  144. scheme='http://localhost'
  145. else
  146. scheme="http://${DOCKER_HOST#*://}"
  147. fi
  148. [[ $method = "POST" ]] && curl_opts+=(-H 'Content-Type: application/json')
  149. curl "${curl_opts[@]}" -X${method} ${scheme}$1
  150. }
  151. function docker_exec {
  152. local id="${1?missing id}"
  153. local cmd="${2?missing command}"
  154. local data=$(printf '{ "AttachStdin": false, "AttachStdout": true, "AttachStderr": true, "Tty":false,"Cmd": %s }' "$cmd")
  155. exec_id=$(docker_api "/containers/$id/exec" "POST" "$data" | jq -r .Id)
  156. if [[ -n "$exec_id" && "$exec_id" != "null" ]]; then
  157. docker_api /exec/$exec_id/start "POST" '{"Detach": false, "Tty":false}'
  158. else
  159. echo "$(date "+%Y/%m/%d %T"), Error: can't exec command ${cmd} in container ${id}. Check if the container is running." >&2
  160. return 1
  161. fi
  162. }
  163. function docker_restart {
  164. local id="${1?missing id}"
  165. docker_api "/containers/$id/restart" "POST"
  166. }
  167. function docker_kill {
  168. local id="${1?missing id}"
  169. local signal="${2?missing signal}"
  170. docker_api "/containers/$id/kill?signal=$signal" "POST"
  171. }
  172. function labeled_cid {
  173. docker_api "/containers/json" | jq -r '.[] | select(.Labels["'$1'"])|.Id'
  174. }
  175. function is_docker_gen_container {
  176. local id="${1?missing id}"
  177. if [[ $(docker_api "/containers/$id/json" | jq -r '.Config.Env[]' | egrep -c '^DOCKER_GEN_VERSION=') = "1" ]]; then
  178. return 0
  179. else
  180. return 1
  181. fi
  182. }
  183. function get_docker_gen_container {
  184. # First try to get the docker-gen container ID from the container label.
  185. local docker_gen_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen)"
  186. # If the labeled_cid function dit not return anything and the env var is set, use it.
  187. if [[ -z "$docker_gen_cid" ]] && [[ -n "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
  188. docker_gen_cid="$NGINX_DOCKER_GEN_CONTAINER"
  189. fi
  190. # If a container ID was found, output it. The function will return 1 otherwise.
  191. [[ -n "$docker_gen_cid" ]] && echo "$docker_gen_cid"
  192. }
  193. function get_nginx_proxy_container {
  194. local volumes_from
  195. # First try to get the nginx container ID from the container label.
  196. local nginx_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy)"
  197. # If the labeled_cid function dit not return anything ...
  198. if [[ -z "${nginx_cid}" ]]; then
  199. # ... and the env var is set, use it ...
  200. if [[ -n "${NGINX_PROXY_CONTAINER:-}" ]]; then
  201. nginx_cid="$NGINX_PROXY_CONTAINER"
  202. # ... else try to get the container ID with the volumes_from method.
  203. elif [[ $(get_self_cid) ]]; then
  204. volumes_from=$(docker_api "/containers/$(get_self_cid)/json" | jq -r '.HostConfig.VolumesFrom[]' 2>/dev/null)
  205. for cid in $volumes_from; do
  206. cid="${cid%:*}" # Remove leading :ro or :rw set by remote docker-compose (thx anoopr)
  207. if [[ $(docker_api "/containers/$cid/json" | jq -r '.Config.Env[]' | egrep -c '^NGINX_VERSION=') = "1" ]];then
  208. nginx_cid="$cid"
  209. break
  210. fi
  211. done
  212. fi
  213. fi
  214. # If a container ID was found, output it. The function will return 1 otherwise.
  215. [[ -n "$nginx_cid" ]] && echo "$nginx_cid"
  216. }
  217. ## Nginx
  218. function reload_nginx {
  219. local _docker_gen_container=$(get_docker_gen_container)
  220. local _nginx_proxy_container=$(get_nginx_proxy_container)
  221. if [[ -n "${_docker_gen_container:-}" ]]; then
  222. # Using docker-gen and nginx in separate container
  223. echo "Reloading nginx docker-gen (using separate container ${_docker_gen_container})..."
  224. docker_kill "${_docker_gen_container}" SIGHUP
  225. if [[ -n "${_nginx_proxy_container:-}" ]]; then
  226. # Reloading nginx in case only certificates had been renewed
  227. echo "Reloading nginx (using separate container ${_nginx_proxy_container})..."
  228. docker_kill "${_nginx_proxy_container}" SIGHUP
  229. fi
  230. else
  231. if [[ -n "${_nginx_proxy_container:-}" ]]; then
  232. echo "Reloading nginx proxy (${_nginx_proxy_container})..."
  233. docker_exec "${_nginx_proxy_container}" \
  234. '[ "sh", "-c", "/app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload" ]' \
  235. | sed -rn 's/^.*([0-9]{4}\/[0-9]{2}\/[0-9]{2}.*$)/\1/p'
  236. [[ ${PIPESTATUS[0]} -eq 1 ]] && echo "$(date "+%Y/%m/%d %T"), Error: can't reload nginx-proxy." >&2
  237. fi
  238. fi
  239. }
  240. function set_ownership_and_permissions {
  241. local path="${1:?}"
  242. # The default ownership is root:root, with 755 permissions for folders and 644 for files.
  243. local user="${FILES_UID:-root}"
  244. local group="${FILES_GID:-$user}"
  245. local f_perms="${FILES_PERMS:-644}"
  246. local d_perms="${FOLDERS_PERMS:-755}"
  247. if [[ ! "$f_perms" =~ ^[0-7]{3,4}$ ]]; then
  248. echo "Warning : the provided files permission octal ($f_perms) is incorrect. Skipping ownership and permissions check."
  249. return 1
  250. fi
  251. if [[ ! "$d_perms" =~ ^[0-7]{3,4}$ ]]; then
  252. echo "Warning : the provided folders permission octal ($d_perms) is incorrect. Skipping ownership and permissions check."
  253. return 1
  254. fi
  255. [[ "$(lc $DEBUG)" == true ]] && echo "Debug: checking $path ownership and permissions."
  256. # Find the user numeric ID if the FILES_UID environment variable isn't numeric.
  257. if [[ "$user" =~ ^[0-9]+$ ]]; then
  258. user_num="$user"
  259. # Check if this user exist inside the container
  260. elif id -u "$user" > /dev/null 2>&1; then
  261. # Convert the user name to numeric ID
  262. local user_num="$(id -u "$user")"
  263. [[ "$(lc $DEBUG)" == true ]] && echo "Debug: numeric ID of user $user is $user_num."
  264. else
  265. echo "Warning: user $user not found in the container, please use a numeric user ID instead of a user name. Skipping ownership and permissions check."
  266. return 1
  267. fi
  268. # Find the group numeric ID if the FILES_GID environment variable isn't numeric.
  269. if [[ "$group" =~ ^[0-9]+$ ]]; then
  270. group_num="$group"
  271. # Check if this group exist inside the container
  272. elif getent group "$group" > /dev/null 2>&1; then
  273. # Convert the group name to numeric ID
  274. local group_num="$(getent group "$group" | awk -F ':' '{print $3}')"
  275. [[ "$(lc $DEBUG)" == true ]] && echo "Debug: numeric ID of group $group is $group_num."
  276. else
  277. echo "Warning: group $group not found in the container, please use a numeric group ID instead of a group name. Skipping ownership and permissions check."
  278. return 1
  279. fi
  280. # Check and modify ownership if required.
  281. if [[ -e "$path" ]]; then
  282. if [[ "$(stat -c %u:%g "$path" )" != "$user_num:$group_num" ]]; then
  283. [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path ownership to $user:$group."
  284. if [[ -L "$path" ]]; then
  285. chown -h "$user_num:$group_num" "$path"
  286. else
  287. chown "$user_num:$group_num" "$path"
  288. fi
  289. fi
  290. # If the path is a folder, check and modify permissions if required.
  291. if [[ -d "$path" ]]; then
  292. if [[ "$(stat -c %a "$path")" != "$d_perms" ]]; then
  293. [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path permissions to $d_perms."
  294. chmod "$d_perms" "$path"
  295. fi
  296. # If the path is a file, check and modify permissions if required.
  297. elif [[ -f "$path" ]]; then
  298. # Use different permissions for private files (private keys and ACME account files) ...
  299. if [[ "$path" =~ ^.*(default\.key|key\.pem|\.json)$ ]]; then
  300. if [[ "$(stat -c %a "$path")" != "$f_perms" ]]; then
  301. [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path permissions to $f_perms."
  302. chmod "$f_perms" "$path"
  303. fi
  304. # ... and for public files (certificates, chains, fullchains, DH parameters).
  305. else
  306. if [[ "$(stat -c %a "$path")" != "644" ]]; then
  307. [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path permissions to 644."
  308. chmod "644" "$path"
  309. fi
  310. fi
  311. fi
  312. else
  313. echo "Warning: $path does not exist. Skipping ownership and permissions check."
  314. return 1
  315. fi
  316. }
  317. # Convert argument to lowercase (bash 4 only)
  318. function lc {
  319. echo "${@,,}"
  320. }