diff --git a/Dockerfiles/Dockerfile.alpine b/Dockerfiles/Dockerfile.alpine index b2bb5ae..0fddac5 100644 --- a/Dockerfiles/Dockerfile.alpine +++ b/Dockerfiles/Dockerfile.alpine @@ -18,7 +18,7 @@ LABEL \ ### ### Build arguments ### -ARG VHOST_GEN_GIT_REF=1.0.8 +ARG VHOST_GEN_GIT_REF=1.0.9 ARG WATCHERD_GIT_REF=v1.0.7 ARG CERT_GEN_GIT_REF=0.10 ARG ARCH=linux/amd64 @@ -66,7 +66,7 @@ RUN set -eux \ && rm -rf mod-proxy-fcgi* \ \ # Install vhost-gen - && wget --no-check-certificate -O vhost-gen.tar.gz "https://github.com/devilbox/vhost-gen/archive/refs/tags/${VHOST_GEN_GIT_REF}.tar.gz" \ + && wget --no-check-certificate -O vhost-gen.tar.gz "https://github.com/devilbox/vhost-gen/archive/${VHOST_GEN_GIT_REF}.tar.gz" \ && tar xvfz vhost-gen.tar.gz \ && cd "vhost-gen-${VHOST_GEN_GIT_REF}" \ && make install \ @@ -124,7 +124,7 @@ RUN set -eux \ ENV MY_USER=daemon ENV MY_GROUP=daemon ENV HTTPD_START="httpd-foreground" -ENV HTTPD_RELOAD="/usr/local/apache2/bin/httpd -k stop" +ENV HTTPD_RELOAD="/usr/local/apache2/bin/httpd -k restart" ENV HTTPD_VERSION="httpd -V 2>&1 | head -1 | awk '{print \$3}'" ENV VHOSTGEN_HTTPD_SERVER="apache22" diff --git a/Dockerfiles/Dockerfile.debian b/Dockerfiles/Dockerfile.debian index 6b584f1..5a40295 100644 --- a/Dockerfiles/Dockerfile.debian +++ b/Dockerfiles/Dockerfile.debian @@ -12,7 +12,7 @@ LABEL \ ### ### Build arguments ### -ARG VHOST_GEN_GIT_REF=1.0.8 +ARG VHOST_GEN_GIT_REF=1.0.9 ARG WATCHERD_GIT_REF=v1.0.7 ARG CERT_GEN_GIT_REF=0.10 ARG ARCH=linux/amd64 @@ -61,7 +61,7 @@ RUN set -eux \ && rm -rf mod-proxy-fcgi* \ \ # Install vhost-gen - && wget --no-check-certificate -O vhost-gen.tar.gz "https://github.com/devilbox/vhost-gen/archive/refs/tags/${VHOST_GEN_GIT_REF}.tar.gz" \ + && wget --no-check-certificate -O vhost-gen.tar.gz "https://github.com/devilbox/vhost-gen/archive/${VHOST_GEN_GIT_REF}.tar.gz" \ && tar xvfz vhost-gen.tar.gz \ && cd "vhost-gen-${VHOST_GEN_GIT_REF}" \ && make install \ @@ -120,7 +120,7 @@ RUN set -eux \ ENV MY_USER=daemon ENV MY_GROUP=daemon ENV HTTPD_START="httpd-foreground" -ENV HTTPD_RELOAD="/usr/local/apache2/bin/httpd -k stop" +ENV HTTPD_RELOAD="/usr/local/apache2/bin/httpd -k restart" ENV HTTPD_VERSION="httpd -V 2>&1 | head -1 | awk '{print \$3}'" ENV VHOSTGEN_HTTPD_SERVER="apache22" diff --git a/Dockerfiles/data/docker-entrypoint.d/.httpd/func-backend.sh b/Dockerfiles/data/docker-entrypoint.d/.httpd/func-backend.sh index aad0dd8..73c37c1 100755 --- a/Dockerfiles/data/docker-entrypoint.d/.httpd/func-backend.sh +++ b/Dockerfiles/data/docker-entrypoint.d/.httpd/func-backend.sh @@ -22,6 +22,8 @@ set -o pipefail ### conf:phpfpm:tcp:: # Remote PHP-FPM server at : ### conf:rproxy:http:: # Reverse Proxy server at http://: ### conf:rproxy:https:: # Reverse Proxy server at https://: +### conf:rproxy:ws:: # Reverse Proxy (websocket) at ws://: +### conf:rproxy:wss:: # Reverse Proxy (websocket) at wss://: ### ### ### Format-2: file: @@ -40,6 +42,8 @@ set -o pipefail ### Examples: ### conf:rproxy:http:10.0.0.1:3000 ### conf:rproxy:https:mydomain.com:8080 +### conf:rproxy:ws:10.0.0.1:3000 +### conf:rproxy:wss:10.0.0.1:3000 ### ### Note: If no file is found, a warning will be logged and no Reverse proxy will be created. ### @@ -90,7 +94,13 @@ backend_conf_is_valid() { fi # 3. Protocol: 'tcp', 'http' or 'https' if ! backend_is_valid_conf_prot "${1}"; then - echo "Invalid backend conf: in: '${1}'. It must be 'tcp', 'http' or 'https'" + # Apache 2.2 does not have websocket support + if [ "${VHOSTGEN_HTTPD_SERVER}" = "apache22" ]; then + echo "Invalid backend conf: in: '${1}'. It must be 'tcp', 'http' or 'https'." + # All other webserver have websocket support + else + echo "Invalid backend conf: in: '${1}'. It must be 'tcp', 'http', 'https', 'ws' or 'wss'." + fi return 1 fi # 4. Host @@ -116,9 +126,22 @@ backend_conf_is_valid() { fi # 7. Validate conf rproxy == http(s)? if [ "${backend_conf_type}" = "rproxy" ]; then - if [ "${backend_conf_prot}" != "http" ] && [ "${backend_conf_prot}" != "https" ]; then - echo "Invalid backend conf: in: '${1}'. 'rproxy' only supports 'http' or 'https'" - return 1 + # Apache 2.2 does not have websocket support + if [ "${VHOSTGEN_HTTPD_SERVER}" = "apache22" ]; then + if [ "${backend_conf_prot}" != "http" ] \ + && [ "${backend_conf_prot}" != "https" ]; then + echo "Invalid backend conf: in: '${1}'. 'rproxy' only supports 'http' and 'https'" + return 1 + fi + # All other webserver have websocket support + else + if [ "${backend_conf_prot}" != "http" ] \ + && [ "${backend_conf_prot}" != "https" ] \ + && [ "${backend_conf_prot}" != "ws" ] \ + && [ "${backend_conf_prot}" != "wss" ]; then + echo "Invalid backend conf: in: '${1}'. 'rproxy' only supports 'http', 'https', 'ws' and 'wss'" + return 1 + fi fi fi } @@ -181,8 +204,14 @@ backend_is_valid_conf_prot() { local value value="$( get_backend_conf_prot "${1}" )" - if [ "${value}" != "tcp" ] && [ "${value}" != "http" ] && [ "${value}" != "https" ]; then - return 1 + if [ "${VHOSTGEN_HTTPD_SERVER}" = "apache22" ];then + if [ "${value}" != "tcp" ] && [ "${value}" != "http" ] && [ "${value}" != "https" ]; then + return 1 + fi + else + if [ "${value}" != "tcp" ] && [ "${value}" != "http" ] && [ "${value}" != "https" ] && [ "${value}" != "ws" ] && [ "${value}" != "wss" ]; then + return 1 + fi fi return 0 } diff --git a/Dockerfiles/data/docker-entrypoint.d/02-env-vars-validate.sh b/Dockerfiles/data/docker-entrypoint.d/02-env-vars-validate.sh index 02e8f5c..e16b03d 100755 --- a/Dockerfiles/data/docker-entrypoint.d/02-env-vars-validate.sh +++ b/Dockerfiles/data/docker-entrypoint.d/02-env-vars-validate.sh @@ -647,7 +647,12 @@ _validate_vhost_backend() { # 5. Validate conf if ! backend_is_valid_conf_prot "${value}"; then _log_env_valid "invalid" "${name}" "${value}" "Invalid format" - _log_env_valid "invalid" "${name}" "${backend_conf_prot}" " is invalid. Must be: " "'tcp', 'http' or 'https'" + # Apache 2.2 does not have websocket support + if [ "${VHOSTGEN_HTTPD_SERVER}" = "apache22" ];then + _log_env_valid "invalid" "${name}" "${backend_conf_prot}" " is invalid. Must be: " "'tcp', 'http' or 'https'" + else + _log_env_valid "invalid" "${name}" "${backend_conf_prot}" " is invalid. Must be: " "'tcp', 'http', 'https', 'ws' or 'wss'" + fi _log_backend_examples "conf" exit 1 fi @@ -662,11 +667,21 @@ _validate_vhost_backend() { fi # 7. Validate conf rproxy == http(s)? if [ "${backend_conf_type}" = "rproxy" ]; then - if [ "${backend_conf_prot}" != "http" ] && [ "${backend_conf_prot}" != "https" ]; then - _log_env_valid "invalid" "${name}" "${value}" "Invalid format" - _log_env_valid "invalid" "${name}" "${backend_conf_prot}" "rproxy only supports protocol " "'http' or 'https'" - _log_backend_examples "conf" - exit 1 + # Apache 2.2 does not have websocket support + if [ "${VHOSTGEN_HTTPD_SERVER}" = "apache22" ];then + if [ "${backend_conf_prot}" != "http" ] && [ "${backend_conf_prot}" != "https" ]; then + _log_env_valid "invalid" "${name}" "${value}" "Invalid format" + _log_env_valid "invalid" "${name}" "${backend_conf_prot}" "rproxy only supports protocol " "'http' and 'https'" + _log_backend_examples "conf" + exit 1 + fi + else + if [ "${backend_conf_prot}" != "http" ] && [ "${backend_conf_prot}" != "https" ] && [ "${backend_conf_prot}" != "ws" ] && [ "${backend_conf_prot}" != "wss" ]; then + _log_env_valid "invalid" "${name}" "${value}" "Invalid format" + _log_env_valid "invalid" "${name}" "${backend_conf_prot}" "rproxy only supports protocol " "'http', 'https', 'ws' and 'wss'" + _log_backend_examples "conf" + exit 1 + fi fi fi # 8. Validate conf @@ -1062,11 +1077,13 @@ _log_backend_examples() { log "err" "Example: conf:phpfpm:tcp:10.0.0.100:9000" log "err" "Example: conf:phpfpm:tcp:domain.com:9000" log "err" "" - log "err" "Example: conf:rproxy:http:10.0.0.100:3000" - log "err" "Example: conf:rproxy:http:domain.com:443" - log "err" "" - log "err" "Example: conf:rproxy:https:10.0.0.100:8080" + log "err" "Example: conf:rproxy:http:10.0.0.100:8080" log "err" "Example: conf:rproxy:https:domain.com:8443" + if [ "${VHOSTGEN_HTTPD_SERVER}" != "apache22" ]; then + log "err" "" + log "err" "Example: conf:rproxy:ws:10.0.0.100:8080" + log "err" "Example: conf:rproxy:wss:domain.com:8443" + fi fi if [ "${show}" = "all" ] || [ "${show}" = "file" ]; then log "err" "" diff --git a/Dockerfiles/data/nginx/nginx.conf b/Dockerfiles/data/nginx/nginx.conf index 720c677..682ed3a 100644 --- a/Dockerfiles/data/nginx/nginx.conf +++ b/Dockerfiles/data/nginx/nginx.conf @@ -36,7 +36,7 @@ http { # [emerg] could not build server_names_hash, you should increase server_names_hash_bucket_size: 32 # https://stackoverflow.com/questions/26357487/ - server_names_hash_bucket_size 64; + server_names_hash_bucket_size 128; # ------------------------------------------------------------------------------- diff --git a/Dockerfiles/data/vhost-gen/templates-main/nginx.yml b/Dockerfiles/data/vhost-gen/templates-main/nginx.yml index 8d5f50e..92b0acc 100644 --- a/Dockerfiles/data/vhost-gen/templates-main/nginx.yml +++ b/Dockerfiles/data/vhost-gen/templates-main/nginx.yml @@ -84,12 +84,31 @@ vhost_type: root "__DOCUMENT_ROOT__"; index __INDEX__; - # Reverse Proxy (-r) + # Reverse Proxy (-r http(s)://ADDR:PORT) rproxy: | - # Define the vhost to reverse proxy + # Define Reverse Proxy location __LOCATION__ { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; + # https://stackoverflow.com/a/72586833 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # Proxy connection + proxy_pass __PROXY_PROTO__://__PROXY_ADDR__:__PROXY_PORT__; + } + + # Reverse Proxy with websocket support (-r ws(s)://ADDR:PORT) + rproxy_ws: | + # Define Reverse Proxy with Websock support + location __LOCATION__ { + # https://stackoverflow.com/a/72586833 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # Websocket settings + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + # Proxy connection proxy_pass __PROXY_PROTO__://__PROXY_ADDR__:__PROXY_PORT__; } diff --git a/README.md b/README.md index cd80729..46688f6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This image is based on the official **[Apache 2.2](https://hub.docker.com/_/httpd)** Docker image and extends it with the ability to have **virtual hosts created automatically**, as well as **adding SSL certificates** when creating new directories. For that to work, it integrates two tools that will take care about the whole process: **[watcherd](https://github.com/devilbox/watcherd)** and **[vhost-gen](https://github.com/devilbox/vhost-gen)**. -From a users perspective, you mount your local project directory into the container under `/shared/httpd`. Any directory then created in your local project directory wil spawn a new virtual host by the same name. Each virtual host optionally supports a generic or custom backend configuration (**static files**, **PHP-FPM** or **reverse proxy**). +From a users perspective, you mount your local project directory into the container under `/shared/httpd`. Any directory then created in your local project directory wil spawn a new virtual host by the same name. Each virtual host optionally supports a generic or custom backend configuration: **static files**, **PHP-FPM** or **reverse proxy**. For convenience the entrypoint script during `docker run` provides a pretty decent **validation and documentation** about wrong user input and suggests steps to fix it. diff --git a/tests/130-main-vhost__websocket.sh b/tests/130-main-vhost__websocket.sh new file mode 100755 index 0000000..72cbeb3 --- /dev/null +++ b/tests/130-main-vhost__websocket.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +CWD="$(cd -P -- "$(dirname -- "$0")" && pwd -P)" + +IMAGE="${1}" +TAG="${2}" +ARCH="${3}" + +if [ "${IMAGE}" = "devilbox/apache-2.2" ]; then + echo "Skipping websocket check for Apache 2.2 - not supported." + exit 0 +fi + + +### +### Load Library +### +# shellcheck disable=SC1090,SC1091 +. "${CWD}/.lib.sh" + + +### +### Universal ports +### +# shellcheck disable=SC2034 +HOST_PORT_HTTP="8093" +# shellcheck disable=SC2034 +HOST_PORT_HTTPS="8493" + +### +### Universal container names +### +# shellcheck disable=SC2034 +NAME_HTTPD="$( get_random_name )" +# shellcheck disable=SC2034 +NAME_PHPFPM="$( get_random_name )" +# shellcheck disable=SC2034 +NAME_RPROXY="$( get_random_name )" +# shellcheck disable=SC2034 +NAME_WORKER="$( get_random_name )" + + + +#--------------------------------------------------------------------------------------------------- +# DEFINES +#--------------------------------------------------------------------------------------------------- + +### +### GLOBALS +### +#DOCROOT="htdocs" +MOUNT_CONT="/var/www/default" +MOUNT_HOST="$( tmp_dir )" + + + +#--------------------------------------------------------------------------------------------------- +# APPS +#--------------------------------------------------------------------------------------------------- + +### +### Application 1 +### +APP1_URL="http://localhost:${HOST_PORT_HTTP}" +#APP1_EXT="nodejs" +#APP1_HDR="" +APP1_TXT="hello you are now connected to a websocket" +#create_app "${MOUNT_HOST}" "${DOCROOT}" "" "index.${APP1_EXT}" " "${MOUNT_HOST}/index.js" +const WebSocket = require("ws"); +const wss = new WebSocket.Server({ port: 3000 }); +wss.on("connection", (ws) => { + ws.send("${APP1_TXT}"); + ws.on("message", (message) => { + console.log("New message from client: %s", message); + }); +}); +console.log("WebSocket server ready at localhost:3000"); +EOF +# Create package.json +cat << EOF > "${MOUNT_HOST}/package.json" +{ + "name": "node-websocket-example", + "version": "1.0.0", + "main": "index.js", + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "ws": "^7.5.1" + } +} +EOF +# Create start script +cat << EOF > "${MOUNT_HOST}/start.sh" +#!/bin/sh +npm install +node index.js +EOF +# Create script for worker +cat << EOF > "${MOUNT_HOST}/worker.sh" +#!/bin/sh +npm install -g wscat +sleep 90000 +EOF + + + + +#--------------------------------------------------------------------------------------------------- +# START +#--------------------------------------------------------------------------------------------------- + +### +### Start NodeJS Container +### +run "docker run -d --name ${NAME_RPROXY} \ +-v ${MOUNT_HOST}:${MOUNT_CONT} -w ${MOUNT_CONT} \ +node:19-alpine sh start.sh >/dev/null" + + +### +### Start HTTPD Container +### +run "docker run -d --platform ${ARCH} --name ${NAME_HTTPD} \ +-v ${MOUNT_HOST}:${MOUNT_CONT} \ +-p 127.0.0.1:${HOST_PORT_HTTP}:80 \ +-p 127.0.0.1:${HOST_PORT_HTTPS}:443 \ +-e DEBUG_ENTRYPOINT=3 \ +-e DEBUG_RUNTIME=2 \ +-e MAIN_VHOST_BACKEND=conf:rproxy:ws:${NAME_RPROXY}:3000 \ +--link ${NAME_RPROXY} \ +${IMAGE}:${TAG} >/dev/null" + +### +### Start Worker Container +### +run "docker run -d --name ${NAME_WORKER} \ +-v ${MOUNT_HOST}:${MOUNT_CONT} -w ${MOUNT_CONT} \ +--link ${NAME_HTTPD} \ +node:19-alpine sh worker.sh >/dev/null" + + +#--------------------------------------------------------------------------------------------------- +# TESTS +#--------------------------------------------------------------------------------------------------- + +### +### Test: APP1 +### +count=0 +retry=30 +while ! run "docker exec -t ${NAME_WORKER} wscat --no-color --connect ${NAME_HTTPD} -x quit | grep '${APP1_TXT}'"; do + if [ "${count}" = "${retry}" ]; then + docker_logs "${NAME_WORKER}" + docker_logs "${NAME_RPROXY}" + docker_logs "${NAME_HTTPD}" + + docker_stop "${NAME_WORKER}" + docker_stop "${NAME_RPROXY}" + docker_stop "${NAME_HTTPD}" + log "fail" "'${APP1_TXT}' not found in ${APP1_URL}" + exit 1 + fi + count=$(( count + 1 )) + sleep 1 +done +log "ok" "Resp: '${APP1_TXT}'" + + + +#--------------------------------------------------------------------------------------------------- +# GENERIC +#--------------------------------------------------------------------------------------------------- + +### +### Test: Errors +### +if ! test_docker_logs_err "${NAME_HTTPD}"; then + docker_logs "${NAME_WORKER}" + docker_logs "${NAME_RPROXY}" + docker_logs "${NAME_HTTPD}" + + docker_stop "${NAME_WORKER}" + docker_stop "${NAME_RPROXY}" + docker_stop "${NAME_HTTPD}" + log "fail" "Found errors in docker logs" + exit 1 +fi + + +### +### Cleanup +### +docker_stop "${NAME_WORKER}" +docker_stop "${NAME_RPROXY}" +docker_stop "${NAME_HTTPD}" +log "ok" "Test succeeded"