diff --git a/.github/workflows/Dockerfile.alpine-php-test-nts b/.github/workflows/Dockerfile.alpine-php-test-nts new file mode 100644 index 00000000..fc58681d --- /dev/null +++ b/.github/workflows/Dockerfile.alpine-php-test-nts @@ -0,0 +1,149 @@ +# syntax=docker/dockerfile:1.7 +# Alpine test image with PHP built from source in NTS mode +# Used for testing the extension on musl-based systems + +ARG PHP_VERSION=8.3 +ARG PHP_SRC_REF=PHP-${PHP_VERSION} + +FROM alpine:3.16 AS base +SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + +ENV TZ=Etc/UTC \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ + PHP_VERSION=${PHP_VERSION} + +RUN apk add --no-cache \ + bash curl git wget make gcc g++ autoconf bison re2c pkgconf \ + libxml2-dev sqlite-dev curl-dev openssl-dev \ + libzip-dev oniguruma-dev libjpeg-turbo-dev libpng-dev libwebp-dev \ + icu-dev readline-dev libxslt-dev mariadb-connector-c-dev \ + linux-headers xz tar ca-certificates \ + nginx mariadb mariadb-client \ + python3 py3-pip py3-flask py3-requests py3-psutil \ + procps + +FROM base AS php-build +ARG PHP_SRC_REF +WORKDIR /usr/src +RUN git clone --depth 1 --branch "${PHP_SRC_REF}" https://github.com/php/php-src.git +WORKDIR /usr/src/php-src + +RUN sed -i 's/\[\([0-9]\+\.[0-9]\+\.[0-9]\+\)-dev\]/[\1]/' configure.ac +RUN ./buildconf --force + +RUN if [ -f ext/openssl/openssl.c ] && grep -q 'REGISTER_LONG_CONSTANT("OPENSSL_SSLV23_PADDING"' ext/openssl/openssl.c; then \ + awk '/REGISTER_LONG_CONSTANT\("OPENSSL_SSLV23_PADDING"/ { \ + indent = $0; \ + gsub(/[^[:space:]].*/, "", indent); \ + print indent "#ifdef RSA_SSLV23_PADDING"; \ + gsub(/^[[:space:]]*/, indent " "); \ + print; \ + print indent "#endif"; \ + next \ + } \ + { print }' ext/openssl/openssl.c > ext/openssl/openssl.c.new && \ + mv ext/openssl/openssl.c.new ext/openssl/openssl.c; \ + fi || true + +RUN mkdir -p /usr/local/etc/php/conf.d + +RUN ./configure \ + --prefix=/usr/local \ + --with-config-file-path=/usr/local/lib \ + --with-config-file-scan-dir=/usr/local/etc/php/conf.d \ + --enable-fpm \ + --enable-mbstring \ + --enable-pcntl \ + --enable-cgi \ + --with-extra-version="" \ + --with-curl \ + --with-mysqli \ + --with-openssl \ + --with-zlib \ + --with-zip \ + --disable-zts \ + && make -j"$(nproc)" \ + && make install +RUN strip /usr/local/bin/php /usr/local/sbin/php-fpm || true + +FROM base AS final +COPY --from=php-build /usr/local /usr/local + +RUN EXTENSION_DIR=$(php -i | grep "^extension_dir" | awk '{print $3}') && \ + if [ -z "$EXTENSION_DIR" ]; then \ + echo "Error: Could not determine extension_dir"; \ + exit 1; \ + fi && \ + mkdir -p "$EXTENSION_DIR" + +RUN php -v | grep -v "ZTS" >/dev/null || (echo "ERROR: ZTS is enabled but should be NTS!" && exit 1) && \ + php -m | grep -E 'curl|mysqli' >/dev/null + +ENV PATH="/usr/local/bin:${PATH}" + +RUN ln -sf /usr/local/bin/php /usr/bin/php && \ + ln -sf /usr/local/sbin/php-fpm /usr/sbin/php-fpm || true && \ + ln -sf /usr/local/bin/php-cgi /usr/bin/php-cgi + +# Create /etc/httpd so the test harness uses the CentOS-style php-fpm config path +RUN mkdir -p /etc/httpd && \ + mkdir -p /etc/php-fpm.d && \ + mkdir -p /run/php-fpm && \ + mkdir -p /var/run && \ + mkdir -p /var/log/php-fpm && \ + mkdir -p /usr/local/etc/php-fpm.d && \ + mkdir -p /usr/local/etc/php/conf.d && \ + ln -sf /usr/local/etc/php/conf.d /etc/php.d || true && \ + echo "[global]" > /usr/local/etc/php-fpm.conf && \ + echo "pid = /run/php-fpm/php-fpm.pid" >> /usr/local/etc/php-fpm.conf && \ + echo "error_log = /var/log/php-fpm/error.log" >> /usr/local/etc/php-fpm.conf && \ + echo "daemonize = yes" >> /usr/local/etc/php-fpm.conf && \ + echo "include=/usr/local/etc/php-fpm.d/*.conf" >> /usr/local/etc/php-fpm.conf && \ + echo "include=/etc/php-fpm.d/*.conf" >> /usr/local/etc/php-fpm.conf && \ + echo "[www]" > /usr/local/etc/php-fpm.d/www.conf && \ + echo "user = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "group = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "listen = 127.0.0.1:9000" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "listen.owner = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "listen.group = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm = dynamic" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.max_children = 5" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.start_servers = 2" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.min_spare_servers = 1" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.max_spare_servers = 3" >> /usr/local/etc/php-fpm.d/www.conf && \ + php-fpm -t -y /usr/local/etc/php-fpm.conf 2>&1 | grep -v "Nothing matches the include pattern" || true && \ + php-fpm -t -y /usr/local/etc/php-fpm.conf >/dev/null 2>&1 || \ + (echo "PHP-FPM config test failed" && exit 1) && \ + ln -sf /usr/local/etc/php-fpm.conf /etc/php-fpm.conf + +# Configure nginx to include /etc/nginx/conf.d/ (expected by test harness) +RUN mkdir -p /etc/nginx/conf.d && \ + awk '/include.*http\.d/{print; print " include /etc/nginx/conf.d/*.conf;"; next}1' \ + /etc/nginx/nginx.conf > /tmp/nginx.conf && \ + mv /tmp/nginx.conf /etc/nginx/nginx.conf + +# MySQL socket path for mysqli +RUN echo "mysqli.default_socket = /var/lib/mysql/mysql.sock" > /usr/local/etc/php/conf.d/mysql-socket.ini + +# MariaDB helper script +RUN mkdir -p /usr/local/bin /var/lib/mysql /run/mysqld && \ + printf '%s\n' \ + '#!/bin/sh' \ + 'set -e' \ + 'mkdir -p /var/lib/mysql /run/mysqld' \ + 'chown -R mysql:mysql /var/lib/mysql /run/mysqld' \ + 'if [ ! -d /var/lib/mysql/mysql ]; then' \ + ' mysql_install_db --user=mysql --datadir=/var/lib/mysql' \ + 'fi' \ + 'mysqld --user=mysql --datadir=/var/lib/mysql --socket=/var/lib/mysql/mysql.sock &' \ + 'pid=$!' \ + 'for i in $(seq 1 30); do mysqladmin --socket=/var/lib/mysql/mysql.sock ping --silent 2>/dev/null && break; sleep 1; done' \ + 'mysql --socket=/var/lib/mysql/mysql.sock -u root -e "CREATE DATABASE IF NOT EXISTS db;" || true' \ + 'mysql --socket=/var/lib/mysql/mysql.sock -u root -e "ALTER USER '"'"'root'"'"'@'"'"'localhost'"'"' IDENTIFIED BY '"'"'pwd'"'"'; FLUSH PRIVILEGES;" || true' \ + 'wait $pid' \ + > /usr/local/bin/start-mariadb && \ + chmod +x /usr/local/bin/start-mariadb + +WORKDIR /work +CMD ["bash"] diff --git a/.github/workflows/Dockerfile.alpine-php-test-zts b/.github/workflows/Dockerfile.alpine-php-test-zts new file mode 100644 index 00000000..0083d5e3 --- /dev/null +++ b/.github/workflows/Dockerfile.alpine-php-test-zts @@ -0,0 +1,152 @@ +# syntax=docker/dockerfile:1.7 +# Alpine test image with PHP built from source in ZTS mode +# Used for testing the extension on musl-based systems with thread safety + +ARG PHP_VERSION=8.3 +ARG PHP_SRC_REF=PHP-${PHP_VERSION} + +FROM alpine:3.16 AS base +SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + +ENV TZ=Etc/UTC \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 \ + PHP_VERSION=${PHP_VERSION} + +RUN apk add --no-cache \ + bash curl git wget make gcc g++ autoconf bison re2c pkgconf \ + libxml2-dev sqlite-dev curl-dev openssl-dev \ + libzip-dev oniguruma-dev libjpeg-turbo-dev libpng-dev libwebp-dev \ + icu-dev readline-dev libxslt-dev mariadb-connector-c-dev \ + linux-headers xz tar ca-certificates \ + nginx mariadb mariadb-client \ + python3 py3-pip py3-flask py3-requests py3-psutil \ + procps + +FROM base AS php-build +ARG PHP_SRC_REF +WORKDIR /usr/src +RUN git clone --depth 1 --branch "${PHP_SRC_REF}" https://github.com/php/php-src.git +WORKDIR /usr/src/php-src + +RUN sed -i 's/\[\([0-9]\+\.[0-9]\+\.[0-9]\+\)-dev\]/[\1]/' configure.ac +RUN ./buildconf --force + +RUN if [ -f ext/openssl/openssl.c ] && grep -q 'REGISTER_LONG_CONSTANT("OPENSSL_SSLV23_PADDING"' ext/openssl/openssl.c; then \ + awk '/REGISTER_LONG_CONSTANT\("OPENSSL_SSLV23_PADDING"/ { \ + indent = $0; \ + gsub(/[^[:space:]].*/, "", indent); \ + print indent "#ifdef RSA_SSLV23_PADDING"; \ + gsub(/^[[:space:]]*/, indent " "); \ + print; \ + print indent "#endif"; \ + next \ + } \ + { print }' ext/openssl/openssl.c > ext/openssl/openssl.c.new && \ + mv ext/openssl/openssl.c.new ext/openssl/openssl.c; \ + fi || true + +RUN mkdir -p /usr/local/etc/php/conf.d + +RUN ./configure \ + --prefix=/usr/local \ + --with-config-file-path=/usr/local/lib \ + --with-config-file-scan-dir=/usr/local/etc/php/conf.d \ + --enable-zts \ + --enable-maintainer-zts \ + --enable-fpm \ + --enable-mbstring \ + --enable-pcntl \ + --enable-cgi \ + --with-extra-version="" \ + --with-curl \ + --with-mysqli \ + --with-openssl \ + --with-zlib \ + --with-zip \ + --disable-zend-signals \ + --enable-zend-max-execution-timers \ + && make -j"$(nproc)" \ + && make install +RUN strip /usr/local/bin/php /usr/local/sbin/php-fpm || true + +FROM base AS final +COPY --from=php-build /usr/local /usr/local + +RUN EXTENSION_DIR=$(php -i | grep "^extension_dir" | awk '{print $3}') && \ + if [ -z "$EXTENSION_DIR" ]; then \ + echo "Error: Could not determine extension_dir"; \ + exit 1; \ + fi && \ + mkdir -p "$EXTENSION_DIR" + +RUN php -v | grep -q "ZTS" || (echo "ERROR: ZTS not enabled!" && exit 1) && \ + php -m | grep -E 'curl|mysqli' >/dev/null + +ENV PATH="/usr/local/bin:${PATH}" + +RUN ln -sf /usr/local/bin/php /usr/bin/php && \ + ln -sf /usr/local/sbin/php-fpm /usr/sbin/php-fpm || true && \ + ln -sf /usr/local/bin/php-cgi /usr/bin/php-cgi + +# Create /etc/httpd so the test harness uses the CentOS-style php-fpm config path +RUN mkdir -p /etc/httpd && \ + mkdir -p /etc/php-fpm.d && \ + mkdir -p /run/php-fpm && \ + mkdir -p /var/run && \ + mkdir -p /var/log/php-fpm && \ + mkdir -p /usr/local/etc/php-fpm.d && \ + mkdir -p /usr/local/etc/php/conf.d && \ + ln -sf /usr/local/etc/php/conf.d /etc/php.d || true && \ + echo "[global]" > /usr/local/etc/php-fpm.conf && \ + echo "pid = /run/php-fpm/php-fpm.pid" >> /usr/local/etc/php-fpm.conf && \ + echo "error_log = /var/log/php-fpm/error.log" >> /usr/local/etc/php-fpm.conf && \ + echo "daemonize = yes" >> /usr/local/etc/php-fpm.conf && \ + echo "include=/usr/local/etc/php-fpm.d/*.conf" >> /usr/local/etc/php-fpm.conf && \ + echo "include=/etc/php-fpm.d/*.conf" >> /usr/local/etc/php-fpm.conf && \ + echo "[www]" > /usr/local/etc/php-fpm.d/www.conf && \ + echo "user = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "group = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "listen = 127.0.0.1:9000" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "listen.owner = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "listen.group = root" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm = dynamic" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.max_children = 5" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.start_servers = 2" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.min_spare_servers = 1" >> /usr/local/etc/php-fpm.d/www.conf && \ + echo "pm.max_spare_servers = 3" >> /usr/local/etc/php-fpm.d/www.conf && \ + php-fpm -t -y /usr/local/etc/php-fpm.conf 2>&1 | grep -v "Nothing matches the include pattern" || true && \ + php-fpm -t -y /usr/local/etc/php-fpm.conf >/dev/null 2>&1 || \ + (echo "PHP-FPM config test failed" && exit 1) && \ + ln -sf /usr/local/etc/php-fpm.conf /etc/php-fpm.conf + +# Configure nginx to include /etc/nginx/conf.d/ (expected by test harness) +RUN mkdir -p /etc/nginx/conf.d && \ + awk '/include.*http\.d/{print; print " include /etc/nginx/conf.d/*.conf;"; next}1' \ + /etc/nginx/nginx.conf > /tmp/nginx.conf && \ + mv /tmp/nginx.conf /etc/nginx/nginx.conf + +# MySQL socket path for mysqli +RUN echo "mysqli.default_socket = /var/lib/mysql/mysql.sock" > /usr/local/etc/php/conf.d/mysql-socket.ini + +# MariaDB helper script +RUN mkdir -p /usr/local/bin /var/lib/mysql /run/mysqld && \ + printf '%s\n' \ + '#!/bin/sh' \ + 'set -e' \ + 'mkdir -p /var/lib/mysql /run/mysqld' \ + 'chown -R mysql:mysql /var/lib/mysql /run/mysqld' \ + 'if [ ! -d /var/lib/mysql/mysql ]; then' \ + ' mysql_install_db --user=mysql --datadir=/var/lib/mysql' \ + 'fi' \ + 'mysqld --user=mysql --datadir=/var/lib/mysql --socket=/var/lib/mysql/mysql.sock &' \ + 'pid=$!' \ + 'for i in $(seq 1 30); do mysqladmin --socket=/var/lib/mysql/mysql.sock ping --silent 2>/dev/null && break; sleep 1; done' \ + 'mysql --socket=/var/lib/mysql/mysql.sock -u root -e "CREATE DATABASE IF NOT EXISTS db;" || true' \ + 'mysql --socket=/var/lib/mysql/mysql.sock -u root -e "ALTER USER '"'"'root'"'"'@'"'"'localhost'"'"' IDENTIFIED BY '"'"'pwd'"'"'; FLUSH PRIVILEGES;" || true' \ + 'wait $pid' \ + > /usr/local/bin/start-mariadb && \ + chmod +x /usr/local/bin/start-mariadb + +WORKDIR /work +CMD ["bash"] diff --git a/.github/workflows/Dockerfile.build-extension-alpine-nts b/.github/workflows/Dockerfile.build-extension-alpine-nts new file mode 100644 index 00000000..a1edee29 --- /dev/null +++ b/.github/workflows/Dockerfile.build-extension-alpine-nts @@ -0,0 +1,67 @@ +# syntax=docker/dockerfile:1.7 +# Alpine image with PHP built from source in NTS mode +# Used for building the PHP extension on musl-based systems + +ARG PHP_VERSION=8.3 +ARG PHP_SRC_REF=PHP-${PHP_VERSION} + +FROM alpine:3.16 AS base +SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + +ENV TZ=Etc/UTC \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 + +RUN apk add --no-cache \ + bash curl git wget make gcc g++ autoconf bison re2c pkgconf \ + libxml2-dev sqlite-dev curl-dev openssl-dev \ + libzip-dev oniguruma-dev libjpeg-turbo-dev libpng-dev libwebp-dev \ + icu-dev readline-dev libxslt-dev mariadb-connector-c-dev \ + linux-headers xz tar ca-certificates + +FROM base AS php-src +ARG PHP_SRC_REF +WORKDIR /usr/src +RUN git clone --depth 1 --branch "${PHP_SRC_REF}" https://github.com/php/php-src.git +WORKDIR /usr/src/php-src +RUN ./buildconf --force + +RUN if [ -f ext/openssl/openssl.c ] && grep -q 'REGISTER_LONG_CONSTANT("OPENSSL_SSLV23_PADDING"' ext/openssl/openssl.c; then \ + awk '/REGISTER_LONG_CONSTANT\("OPENSSL_SSLV23_PADDING"/ { \ + indent = $0; \ + gsub(/[^[:space:]].*/, "", indent); \ + print indent "#ifdef RSA_SSLV23_PADDING"; \ + gsub(/^[[:space:]]*/, indent " "); \ + print; \ + print indent "#endif"; \ + next \ + } \ + { print }' ext/openssl/openssl.c > ext/openssl/openssl.c.new && \ + mv ext/openssl/openssl.c.new ext/openssl/openssl.c; \ + fi || true + +FROM php-src AS php-build +RUN ./configure \ + --prefix=/usr/local \ + --with-config-file-path=/usr/local/lib \ + --with-config-file-scan-dir=/usr/local/etc/php/conf.d \ + --enable-mbstring \ + --enable-pcntl \ + --enable-intl \ + --with-curl \ + --with-mysqli \ + --with-openssl \ + --with-zlib \ + --with-zip \ + && make -j"$(nproc)" \ + && make install +RUN strip /usr/local/bin/php || true + +FROM base AS dev +COPY --from=php-build /usr/local /usr/local +RUN php -v && php -m | grep -E 'curl|mysqli' >/dev/null +ENV PATH="/usr/local/bin:${PATH}" + +RUN mkdir -p /usr/local/etc/php/conf.d +WORKDIR /work +CMD ["php", "-v"] diff --git a/.github/workflows/Dockerfile.build-extension-alpine-zts b/.github/workflows/Dockerfile.build-extension-alpine-zts new file mode 100644 index 00000000..bb7c2284 --- /dev/null +++ b/.github/workflows/Dockerfile.build-extension-alpine-zts @@ -0,0 +1,72 @@ +# syntax=docker/dockerfile:1.7 +# Alpine image with PHP built from source in ZTS mode +# Used for building the PHP extension on musl-based systems with thread safety + +ARG PHP_VERSION=8.3 +ARG PHP_SRC_REF=PHP-${PHP_VERSION} + +FROM alpine:3.16 AS base +SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + +ENV TZ=Etc/UTC \ + LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 + +RUN apk add --no-cache \ + bash curl git wget make gcc g++ autoconf bison re2c pkgconf \ + libxml2-dev sqlite-dev curl-dev openssl-dev \ + libzip-dev oniguruma-dev libjpeg-turbo-dev libpng-dev libwebp-dev \ + icu-dev readline-dev libxslt-dev mariadb-connector-c-dev \ + linux-headers xz tar ca-certificates + +FROM base AS php-src +ARG PHP_SRC_REF +WORKDIR /usr/src +RUN git clone --depth 1 --branch "${PHP_SRC_REF}" https://github.com/php/php-src.git +WORKDIR /usr/src/php-src + +RUN sed -i 's/\[\([0-9]\+\.[0-9]\+\.[0-9]\+\)-dev\]/[\1]/' configure.ac +RUN ./buildconf --force + +RUN if [ -f ext/openssl/openssl.c ] && grep -q 'REGISTER_LONG_CONSTANT("OPENSSL_SSLV23_PADDING"' ext/openssl/openssl.c; then \ + awk '/REGISTER_LONG_CONSTANT\("OPENSSL_SSLV23_PADDING"/ { \ + indent = $0; \ + gsub(/[^[:space:]].*/, "", indent); \ + print indent "#ifdef RSA_SSLV23_PADDING"; \ + gsub(/^[[:space:]]*/, indent " "); \ + print; \ + print indent "#endif"; \ + next \ + } \ + { print }' ext/openssl/openssl.c > ext/openssl/openssl.c.new && \ + mv ext/openssl/openssl.c.new ext/openssl/openssl.c; \ + fi || true + +FROM php-src AS php-build +RUN ./configure \ + --prefix=/usr/local \ + --with-config-file-path=/usr/local/lib \ + --with-config-file-scan-dir=/usr/local/etc/php/conf.d \ + --enable-zts \ + --enable-maintainer-zts \ + --enable-mbstring \ + --enable-pcntl \ + --with-extra-version="" \ + --with-curl \ + --with-mysqli \ + --with-openssl \ + --with-zlib \ + --with-zip \ + && make -j"$(nproc)" \ + && make install +RUN strip /usr/local/bin/php || true + +FROM base AS dev +COPY --from=php-build /usr/local /usr/local +RUN php -v | grep -q "ZTS" || (echo "ERROR: ZTS not enabled!" && exit 1) && \ + php -m | grep -E 'curl|mysqli' >/dev/null +ENV PATH="/usr/local/bin:${PATH}" + +RUN mkdir -p /usr/local/etc/php/conf.d +WORKDIR /work +CMD ["php", "-v"] diff --git a/.github/workflows/Dockerfile.build-libs-alpine b/.github/workflows/Dockerfile.build-libs-alpine new file mode 100644 index 00000000..0744192f --- /dev/null +++ b/.github/workflows/Dockerfile.build-libs-alpine @@ -0,0 +1,29 @@ +# Alpine-based image for building Go shared libraries with custom Go fork +# that supports c-shared buildmode on musl-based systems. +FROM alpine:3.16 + +ARG GO_RELEASE_TAG=devel-go1.27-tlsdesc.1 + +RUN apk add --no-cache \ + bash curl git make gcc g++ musl-dev \ + protobuf protobuf-dev + +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "x86_64" ]; then GO_ARCH="amd64"; \ + elif [ "$ARCH" = "aarch64" ]; then GO_ARCH="arm64"; fi && \ + curl -fsSL -o /tmp/go.tar.gz \ + "https://github.com/AikidoSec/go/releases/download/${GO_RELEASE_TAG}/${GO_RELEASE_TAG}.linux-${GO_ARCH}.tar.gz" && \ + tar -C /usr/local -xzf /tmp/go.tar.gz && \ + rm /tmp/go.tar.gz + +ENV PATH="/usr/local/go/bin:${PATH}" +ENV GOPATH="/go" +ENV GOBIN="/go/bin" +ENV PATH="${GOBIN}:${PATH}" + +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \ + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +WORKDIR /workspace + +CMD ["/bin/bash"] diff --git a/.github/workflows/build-alpine-php-test-images-nts.yml b/.github/workflows/build-alpine-php-test-images-nts.yml new file mode 100644 index 00000000..86e32872 --- /dev/null +++ b/.github/workflows/build-alpine-php-test-images-nts.yml @@ -0,0 +1,40 @@ +name: Build Alpine PHP test images (NTS) + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.alpine-php-test-nts + - .github/workflows/build-alpine-php-test-images-nts.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-test-alpine-nts + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + strategy: + matrix: + php_version: ['7.4','8.0','8.1','8.2','8.3','8.4','8.5'] + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.alpine-php-test-nts + platforms: linux/amd64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-${{ env.VERSION }} diff --git a/.github/workflows/build-alpine-php-test-images-zts.yml b/.github/workflows/build-alpine-php-test-images-zts.yml new file mode 100644 index 00000000..2e55daf6 --- /dev/null +++ b/.github/workflows/build-alpine-php-test-images-zts.yml @@ -0,0 +1,40 @@ +name: Build Alpine PHP test images (ZTS) + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.alpine-php-test-zts + - .github/workflows/build-alpine-php-test-images-zts.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-test-alpine-zts + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + strategy: + matrix: + php_version: ['7.4','8.0','8.1','8.2','8.3','8.4','8.5'] + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.alpine-php-test-zts + platforms: linux/amd64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-${{ env.VERSION }} diff --git a/.github/workflows/build-extension-images-alpine-nts.yml b/.github/workflows/build-extension-images-alpine-nts.yml new file mode 100644 index 00000000..4492a3dc --- /dev/null +++ b/.github/workflows/build-extension-images-alpine-nts.yml @@ -0,0 +1,40 @@ +name: Build Alpine PHP extension images (NTS) + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.build-extension-alpine-nts + - .github/workflows/build-extension-images-alpine-nts.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-build-extension-alpine-nts + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + strategy: + matrix: + php_version: ['7.4','8.0','8.1','8.2','8.3','8.4','8.5'] + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.build-extension-alpine-nts + platforms: linux/amd64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-${{ env.VERSION }} diff --git a/.github/workflows/build-extension-images-alpine-zts.yml b/.github/workflows/build-extension-images-alpine-zts.yml new file mode 100644 index 00000000..ce759026 --- /dev/null +++ b/.github/workflows/build-extension-images-alpine-zts.yml @@ -0,0 +1,40 @@ +name: Build Alpine PHP extension images (ZTS) + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.build-extension-alpine-zts + - .github/workflows/build-extension-images-alpine-zts.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-build-extension-alpine-zts + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + strategy: + matrix: + php_version: ['7.4','8.0','8.1','8.2','8.3','8.4','8.5'] + fail-fast: false + permissions: { contents: read, packages: write } + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.build-extension-alpine-zts + platforms: linux/amd64 + push: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php_version }}-${{ env.VERSION }} diff --git a/.github/workflows/build-libs-alpine-image.yml b/.github/workflows/build-libs-alpine-image.yml new file mode 100644 index 00000000..a81cf7c6 --- /dev/null +++ b/.github/workflows/build-libs-alpine-image.yml @@ -0,0 +1,40 @@ +name: Build Alpine libs toolchain image + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/Dockerfile.build-libs-alpine + - .github/workflows/build-libs-alpine-image.yml + +env: + REGISTRY: ghcr.io + IMAGE_NAME: aikidosec/firewall-php-build-libs-alpine + VERSION: v1 + +jobs: + build-amd64: + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & push (amd64) + uses: docker/build-push-action@v6 + with: + context: . + file: .github/workflows/Dockerfile.build-libs-alpine + platforms: linux/amd64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ env.VERSION }}-amd64 + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ env.VERSION }}-amd64,mode=max diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72b131b9..a7bc6763 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,6 +78,68 @@ jobs: path: | ${{ github.workspace }}/build/aikido-request-processor.so + build_libs_alpine: + name: Build Go libs (Alpine/musl) x86_64 + runs-on: ubuntu-24.04 + container: + image: ghcr.io/aikidosec/firewall-php-build-libs-alpine:v1 + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Get Arch + run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV + + - name: Get Aikido version + run: | + AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') + echo $AIKIDO_VERSION + echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV + + - name: Build Aikido Agent + run: | + cd lib + protoc --go_out=agent --go-grpc_out=agent ipc.proto + cd agent + go get main/ipc/protos + go get google.golang.org/grpc + go get github.com/stretchr/testify/assert + go test ./... + go build -buildvcs=false -ldflags "-s -w" -o ../../build/aikido-agent + ls -l ../../build + + - name: Build Aikido Request Processor + run: | + cd lib + protoc --go_out=request-processor --go-grpc_out=request-processor ipc.proto + cd request-processor + go mod tidy + go get google.golang.org/grpc + go get github.com/stretchr/testify/assert + go get main/ipc/protos + go test ./... + go build -buildvcs=false -ldflags "-s -w" -buildmode=c-shared -o ../../build/aikido-request-processor.so + ls -l ../../build + + - name: Archive agent + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + if: always() + with: + name: aikido-agent-alpine-${{ env.ARCH }} + if-no-files-found: error + path: | + ${{ github.workspace }}/build/aikido-agent + + - name: Archive request processor + uses: actions/upload-artifact@v4 + if: always() + with: + name: aikido-request-processor-alpine-${{ env.ARCH }} + if-no-files-found: error + path: | + ${{ github.workspace }}/build/aikido-request-processor.so + build_php_extension_nts: name: Build php ${{ matrix.php_version }} extension NTS ${{ matrix.arch == '' && 'x86_64' || 'arm' }} runs-on: ubuntu-24.04${{ matrix.arch }} @@ -185,6 +247,110 @@ jobs: ${{ github.workspace }}/build/modules/${{ env.AIKIDO_ARTIFACT }}-zts.so ${{ github.workspace }}/tests/*.diff + build_php_extension_alpine_nts: + name: Build php ${{ matrix.php_version }} extension Alpine NTS x86_64 + runs-on: ubuntu-24.04 + container: ghcr.io/aikidosec/firewall-php-build-extension-alpine-nts:${{ matrix.php_version }}-v1 + strategy: + matrix: + php_version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get Arch + run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV + + - name: Get Aikido version + run: | + AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') + echo $AIKIDO_VERSION + echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV + echo "AIKIDO_ARTIFACT=aikido-extension-alpine-php-${{ matrix.php_version }}" >> $GITHUB_ENV + + - name: Check PHP setup + run: | + php -v | grep -v "ZTS" > /dev/null || (echo "ERROR: PHP is ZTS, expected NTS!" && php -v && exit 1) + + - name: Build extension + run: | + rm -rf build + mkdir build + cd lib/php-extension + phpize + cd ../../build + CXX=g++ CXXFLAGS="-fPIC -g -O2 -I../lib/php-extension/include" LDFLAGS="-lstdc++" ../lib/php-extension/configure + make -j"$(nproc)" + + - name: Version Aikido extension + run: | + cd ./build/modules + mv aikido.so ${{ env.AIKIDO_ARTIFACT }}-nts.so + + - name: Archive build artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ env.AIKIDO_ARTIFACT }}-nts-${{ env.ARCH }} + if-no-files-found: error + path: | + ${{ github.workspace }}/build/modules/${{ env.AIKIDO_ARTIFACT }}-nts.so + ${{ github.workspace }}/tests/*.diff + + build_php_extension_alpine_zts: + name: Build php ${{ matrix.php_version }} extension Alpine ZTS x86_64 + runs-on: ubuntu-24.04 + container: ghcr.io/aikidosec/firewall-php-build-extension-alpine-zts:${{ matrix.php_version }}-v1 + strategy: + matrix: + php_version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get Arch + run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV + + - name: Get Aikido version + run: | + AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') + echo $AIKIDO_VERSION + echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV + echo "AIKIDO_ARTIFACT=aikido-extension-alpine-php-${{ matrix.php_version }}" >> $GITHUB_ENV + + - name: Check PHP setup + run: | + php -v | grep -q "ZTS" || (echo "ERROR: PHP is not ZTS!" && php -v && exit 1) + + - name: Build extension + run: | + rm -rf build + mkdir build + cd lib/php-extension + phpize + cd ../../build + CXX=g++ CXXFLAGS="-fPIC -g -O2 -I../lib/php-extension/include" LDFLAGS="-lstdc++" ../lib/php-extension/configure + make -j"$(nproc)" + + - name: Version Aikido extension + run: | + cd ./build/modules + mv aikido.so ${{ env.AIKIDO_ARTIFACT }}-zts.so + + - name: Archive build artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ env.AIKIDO_ARTIFACT }}-zts-${{ env.ARCH }} + if-no-files-found: error + path: | + ${{ github.workspace }}/build/modules/${{ env.AIKIDO_ARTIFACT }}-zts.so + ${{ github.workspace }}/tests/*.diff + build_rpm: name: Build rpm ${{ matrix.arch == '' && 'x86_64' || 'arm' }} runs-on: ubuntu-24.04${{ matrix.arch }} @@ -379,6 +545,150 @@ jobs: path: | ${{ env.AIKIDO_ARTIFACT }} + build_apk: + name: Build apk x86_64 + runs-on: ubuntu-24.04 + container: + image: alpine:3.16 + needs: [ build_libs_alpine, build_php_extension_alpine_nts, build_php_extension_alpine_zts ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install tools + run: | + apk add --no-cache bash curl jq coreutils + + - name: Get Arch + run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV + + - name: Get Aikido version + run: | + AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') + echo $AIKIDO_VERSION + echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV + echo "AIKIDO_LIBZEN=libzen_internals_${{ env.ARCH }}-unknown-linux-musl.so" >> $GITHUB_ENV + echo "AIKIDO_LIBZEN_VERSION=0.1.60" >> $GITHUB_ENV + + - name: Download Alpine NTS extensions + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + pattern: | + aikido-extension-alpine-php-*-nts-${{ env.ARCH }} + + - name: Download Alpine ZTS extensions + uses: actions/download-artifact@v4 + with: + pattern: | + aikido-extension-alpine-php-*-zts-${{ env.ARCH }} + + - name: Download Alpine agent + uses: actions/download-artifact@v4 + with: + pattern: | + aikido-agent-alpine-${{ env.ARCH }} + + - name: Download Alpine request processor + uses: actions/download-artifact@v4 + with: + pattern: | + aikido-request-processor-alpine-${{ env.ARCH }} + + - name: Download Aikido Zen Internals Lib (musl) + run: | + curl -L -O https://github.com/AikidoSec/zen-internals/releases/download/v${{ env.AIKIDO_LIBZEN_VERSION }}/${{ env.AIKIDO_LIBZEN }} + + - name: Verify Aikido Zen Internals Lib SHA256 + run: | + EXPECTED_SHA256=$(curl -s https://api.github.com/repos/AikidoSec/zen-internals/releases/tags/v${{ env.AIKIDO_LIBZEN_VERSION }} | jq -r '.assets[] | select(.name == "${{ env.AIKIDO_LIBZEN }}") | .digest' | sed 's/sha256://') + ACTUAL_SHA256=$(sha256sum ${{ env.AIKIDO_LIBZEN }} | awk '{print $1}') + echo "Expected SHA256: $EXPECTED_SHA256" + echo "Actual SHA256: $ACTUAL_SHA256" + if [ "$EXPECTED_SHA256" != "$ACTUAL_SHA256" ]; then + echo "SHA256 mismatch! Downloaded file may be corrupted or tampered with." + exit 1 + fi + echo "SHA256 verification passed!" + + - name: Prepare APK package + run: | + mkdir -p pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }} + mv aikido-agent-alpine-${{ env.ARCH }}/aikido-agent pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/aikido-agent + mv aikido-request-processor-alpine-${{ env.ARCH }}/aikido-request-processor.so pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/aikido-request-processor.so + mv ${{ env.AIKIDO_LIBZEN }} pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/${{ env.AIKIDO_LIBZEN }} + # Copy NTS extensions + for dir in aikido-extension-alpine-php-*-nts-*/; do + if [ -d "$dir" ]; then + find "$dir" -name "aikido-extension-alpine-php-*-nts.so" -exec sh -c 'f="{}"; base=$(basename "$f"); newname=$(echo "$base" | sed "s/aikido-extension-alpine-/aikido-extension-/"); mv "$f" "pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/$newname"' \; + fi + done + # Copy ZTS extensions + for dir in aikido-extension-alpine-php-*-zts-*/; do + if [ -d "$dir" ]; then + find "$dir" -name "aikido-extension-alpine-php-*-zts.so" -exec sh -c 'f="{}"; base=$(basename "$f"); newname=$(echo "$base" | sed "s/aikido-extension-alpine-/aikido-extension-/"); mv "$f" "pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/$newname"' \; + fi + done + cp package/rpm/opt/aikido/aikido.ini pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/aikido.ini + sed -i "s/aikido.so/aikido-${{ env.AIKIDO_VERSION }}.so/" pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/aikido.ini + chmod 755 pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/* + echo "Package contents:" + ls -la pkg-root/opt/aikido-${{ env.AIKIDO_VERSION }}/ + + - name: Build APK package + run: | + INSTALLED_SIZE=$(du -sk pkg-root | awk '{print $1 * 1024}') + BUILD_DATE=$(date +%s) + + # Create data tarball + cd pkg-root + tar czf ../data.tar.gz . + cd .. + + DATAHASH=$(sha256sum data.tar.gz | awk '{print $1}') + + # Create .PKGINFO + cat > .PKGINFO < aikido-php-firewall.${{ env.ARCH }}.apk + + echo "APK package built:" + ls -la aikido-php-firewall.${{ env.ARCH }}.apk + + echo "AIKIDO_ARTIFACT=aikido-php-firewall.${{ env.ARCH }}.apk" >> $GITHUB_ENV + + - name: Archive APK package + uses: actions/upload-artifact@v4 + with: + name: aikido-php-firewall.${{ env.ARCH }}.apk + if-no-files-found: error + path: | + aikido-php-firewall.${{ env.ARCH }}.apk + test_php_centos: name: CentOS NTS php-${{ matrix.php_version }} ${{ matrix.server }} ${{ matrix.arch == '' && 'x86_64' || 'arm' }} runs-on: ubuntu-24.04${{ matrix.arch }} @@ -834,6 +1144,185 @@ jobs: cd tools python3 run_server_tests.py ../tests/server ../tests/testlib --server=${{ matrix.server }} --max-runs=3 + test_php_alpine: + name: Alpine NTS php-${{ matrix.php_version }} ${{ matrix.server }} x86_64 + runs-on: ubuntu-24.04 + container: + image: ghcr.io/aikidosec/firewall-php-test-alpine-nts:${{ matrix.php_version }}-v1 + options: --privileged + needs: [ build_apk ] + strategy: + matrix: + php_version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + server: ['nginx-php-fpm', 'php-built-in'] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup + run: | + uname -a + cat /etc/alpine-release + php -v + nginx -v || true + which php-fpm && php-fpm -v || true + + - name: Install and start MariaDB + run: | + mkdir -p /var/lib/mysql /run/mysqld + chown -R mysql:mysql /var/lib/mysql /run/mysqld + mysql_install_db --user=mysql --datadir=/var/lib/mysql + mysqld --user=mysql --datadir=/var/lib/mysql --socket=/var/lib/mysql/mysql.sock & + sleep 10 + mysql --socket=/var/lib/mysql/mysql.sock -u root -e "CREATE DATABASE IF NOT EXISTS db;" + mysql --socket=/var/lib/mysql/mysql.sock -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'pwd'; FLUSH PRIVILEGES;" + + - name: Test MySQL connection with mysqli + run: | + php -r ' + $mysqli = new mysqli("localhost", "root", "pwd", "db"); + if ($mysqli->connect_error) { + echo "MySQL connection failed: " . $mysqli->connect_error . "\n"; + exit(1); + } else { + echo "MySQL connection successful\n"; + $mysqli->close(); + } + ' + + - name: Get Arch + run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV + + - name: Check PHP setup + run: | + uname -m + php -v + php -i + + - name: Get Aikido version + run: | + AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') + echo $AIKIDO_VERSION + echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV + echo "AIKIDO_APK=aikido-php-firewall.${{ env.ARCH }}.apk" >> $GITHUB_ENV + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: | + ${{ env.AIKIDO_APK }} + + - name: Install APK + run: | + apk add --allow-untrusted ${{ env.AIKIDO_APK }}/${{ env.AIKIDO_APK }} + + - name: Run CLI tests + run: | + export TEST_PHP_EXECUTABLE=/usr/local/bin/php + cd lib/php-extension/ + phpize + cd ../../ + php lib/php-extension/run-tests.php ./tests/cli + + - name: Run ${{ matrix.server }} server tests + run: | + cd tools + python3 run_server_tests.py ../tests/server ../tests/testlib --server=${{ matrix.server }} --max-runs=3 + + test_php_alpine_zts: + name: Alpine ZTS php-${{ matrix.php_version }} ${{ matrix.server }} x86_64 + runs-on: ubuntu-24.04 + container: + image: ghcr.io/aikidosec/firewall-php-test-alpine-zts:${{ matrix.php_version }}-v1 + options: --privileged + needs: [ build_apk ] + strategy: + matrix: + php_version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + server: ['nginx-php-fpm', 'php-built-in'] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup + run: | + uname -a + cat /etc/alpine-release + php -v + nginx -v || true + which php-fpm && php-fpm -v || true + + - name: Verify ZTS is enabled + run: | + php -v | grep -q "ZTS" || (echo "ERROR: ZTS not enabled!" && exit 1) + php -v + + - name: Install and start MariaDB + run: | + mkdir -p /var/lib/mysql /run/mysqld + chown -R mysql:mysql /var/lib/mysql /run/mysqld + mysql_install_db --user=mysql --datadir=/var/lib/mysql + mysqld --user=mysql --datadir=/var/lib/mysql --socket=/var/lib/mysql/mysql.sock & + sleep 10 + mysql --socket=/var/lib/mysql/mysql.sock -u root -e "CREATE DATABASE IF NOT EXISTS db;" + mysql --socket=/var/lib/mysql/mysql.sock -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'pwd'; FLUSH PRIVILEGES;" + + - name: Test MySQL connection with mysqli + run: | + php -r ' + $mysqli = new mysqli("localhost", "root", "pwd", "db"); + if ($mysqli->connect_error) { + echo "MySQL connection failed: " . $mysqli->connect_error . "\n"; + exit(1); + } else { + echo "MySQL connection successful\n"; + $mysqli->close(); + } + ' + + - name: Get Arch + run: echo "ARCH=$(uname -m)" >> $GITHUB_ENV + + - name: Check PHP setup + run: | + uname -m + php -v + php -i + + - name: Get Aikido version + run: | + AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') + echo $AIKIDO_VERSION + echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV + echo "AIKIDO_APK=aikido-php-firewall.${{ env.ARCH }}.apk" >> $GITHUB_ENV + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: | + ${{ env.AIKIDO_APK }} + + - name: Install APK + run: | + apk add --allow-untrusted ${{ env.AIKIDO_APK }}/${{ env.AIKIDO_APK }} + + - name: Run CLI tests + run: | + export TEST_PHP_EXECUTABLE=/usr/local/bin/php + cd lib/php-extension/ + phpize + cd ../../ + php lib/php-extension/run-tests.php ./tests/cli + + - name: Run ${{ matrix.server }} server tests + run: | + cd tools + python3 run_server_tests.py ../tests/server ../tests/testlib --server=${{ matrix.server }} --max-runs=3 + test_php_qa_action_controlling_tests_apache_mod_php: name: QA apache-mod-php runs-on: ubuntu-latest diff --git a/package/apk/post-install.sh b/package/apk/post-install.sh new file mode 100644 index 00000000..c068ecb3 --- /dev/null +++ b/package/apk/post-install.sh @@ -0,0 +1,125 @@ +#!/bin/sh +set -e + +VERSION="__VERSION__" + +echo "Starting the installation process for Aikido PHP Firewall v${VERSION}..." + +pids=$(ps aux 2>/dev/null | grep aikido-agent | grep -v grep | awk '{print $2}') || true +if [ -n "$pids" ]; then + echo "Stopping Aikido Agent processes: $pids" + echo "$pids" | xargs kill -9 2>/dev/null || true + echo "Aikido Agent(s) stopped." +fi + +mkdir -p /var/log/aikido-${VERSION} +chmod 777 /var/log/aikido-${VERSION} + +PHP_VERSIONS="" +if command -v php >/dev/null 2>&1; then + ver=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') + PHP_VERSIONS="$ver" +fi + +for php_path in /usr/bin/php[0-9]* /usr/local/bin/php[0-9]*; do + if [ -x "$php_path" ]; then + ver=$("$php_path" -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;' 2>/dev/null) || continue + case " $PHP_VERSIONS " in + *" $ver "*) ;; + *) PHP_VERSIONS="$PHP_VERSIONS $ver" ;; + esac + fi +done + +PHP_VERSIONS=$(echo "$PHP_VERSIONS" | xargs) + +if [ -n "$PHP_VERSIONS" ]; then + echo "Found PHP versions: $PHP_VERSIONS" +fi + +FRANKENPHP_PHP_VERSION="" +if command -v frankenphp >/dev/null 2>&1; then + FRANKENPHP_PHP_VERSION=$(frankenphp -v 2>/dev/null | sed -n 's/.*PHP \([0-9]*\.[0-9]*\).*/\1/p' | head -1) + if [ -n "$FRANKENPHP_PHP_VERSION" ]; then + echo "Found FrankenPHP with embedded PHP $FRANKENPHP_PHP_VERSION" + fi +fi + +for PHP_VERSION in $PHP_VERSIONS; do + echo "Installing for PHP $PHP_VERSION..." + + PHP_BIN="php${PHP_VERSION}" + if ! command -v "$PHP_BIN" >/dev/null 2>&1; then + PHP_BIN="php" + fi + + PHP_EXT_DIR=$("$PHP_BIN" -i | grep "^extension_dir" | awk '{print $3}') + PHP_MOD_DIR=$("$PHP_BIN" -i | grep "Scan this dir for additional .ini files" | awk -F"=> " '{print $2}') + + PHP_THREAD_SAFETY=$("$PHP_BIN" -i | grep "Thread Safety" | awk -F"=> " '{print $2}' | tr -d ' ') + if [ "$PHP_THREAD_SAFETY" = "enabled" ]; then + EXT_SUFFIX="-zts" + echo "PHP $PHP_VERSION is ZTS (Thread Safe)" + else + EXT_SUFFIX="-nts" + echo "PHP $PHP_VERSION is NTS (Non-Thread Safe)" + fi + + if [ -d "$PHP_EXT_DIR" ]; then + EXT_FILE="aikido-extension-php-${PHP_VERSION}${EXT_SUFFIX}.so" + if [ -f "/opt/aikido-${VERSION}/${EXT_FILE}" ]; then + echo "Installing new Aikido extension in ${PHP_EXT_DIR}/aikido-${VERSION}.so..." + ln -sf "/opt/aikido-${VERSION}/${EXT_FILE}" "${PHP_EXT_DIR}/aikido-${VERSION}.so" + else + echo "Warning: Extension file /opt/aikido-${VERSION}/${EXT_FILE} not found! Skipping..." + continue + fi + else + echo "No extension dir for PHP ${PHP_VERSION}! Skipping..." + continue + fi + + PHP_DEBIAN_MOD_DIR="/etc/php/${PHP_VERSION}/mods-available" + PHP_DEBIAN_MOD_DIR_CLI="/etc/php/${PHP_VERSION}/cli/conf.d" + PHP_DEBIAN_MOD_DIR_CGI="/etc/php/${PHP_VERSION}/cgi/conf.d" + PHP_DEBIAN_MOD_DIR_FPM="/etc/php/${PHP_VERSION}/fpm/conf.d" + PHP_DEBIAN_MOD_DIR_APACHE2="/etc/php/${PHP_VERSION}/apache2/conf.d" + + if [ -d "$PHP_DEBIAN_MOD_DIR" ]; then + echo "Installing new Aikido mod in ${PHP_DEBIAN_MOD_DIR}/aikido-${VERSION}.ini..." + ln -sf "/opt/aikido-${VERSION}/aikido.ini" "${PHP_DEBIAN_MOD_DIR}/aikido-${VERSION}.ini" + for subdir in "$PHP_DEBIAN_MOD_DIR_CLI" "$PHP_DEBIAN_MOD_DIR_CGI" "$PHP_DEBIAN_MOD_DIR_FPM" "$PHP_DEBIAN_MOD_DIR_APACHE2"; do + if [ -d "$subdir" ]; then + echo "Installing new Aikido mod in ${subdir}/zz-aikido-${VERSION}.ini..." + ln -sf "${PHP_DEBIAN_MOD_DIR}/aikido-${VERSION}.ini" "${subdir}/zz-aikido-${VERSION}.ini" + fi + done + elif [ -d "$PHP_MOD_DIR" ]; then + echo "Installing new Aikido mod in ${PHP_MOD_DIR}/zz-aikido-${VERSION}.ini..." + ln -sf "/opt/aikido-${VERSION}/aikido.ini" "${PHP_MOD_DIR}/zz-aikido-${VERSION}.ini" + else + echo "No mod dir for PHP ${PHP_VERSION}! Skipping..." + continue + fi +done + +if [ -n "$FRANKENPHP_PHP_VERSION" ]; then + echo "Installing for FrankenPHP with PHP ${FRANKENPHP_PHP_VERSION}... ZTS (Thread Safe)" + + FRANKENPHP_EXT_DIR="/usr/lib/frankenphp/modules" + FRANKENPHP_INI_DIR="/etc/frankenphp/php.d" + + mkdir -p "$FRANKENPHP_EXT_DIR" "$FRANKENPHP_INI_DIR" + ln -sf "/opt/aikido-${VERSION}/aikido-extension-php-${FRANKENPHP_PHP_VERSION}-zts.so" "${FRANKENPHP_EXT_DIR}/aikido-${VERSION}.so" + ln -sf "/opt/aikido-${VERSION}/aikido.ini" "${FRANKENPHP_INI_DIR}/zz-aikido-${VERSION}.ini" +fi + +if [ -z "$PHP_VERSIONS" ] && [ -z "$FRANKENPHP_PHP_VERSION" ]; then + echo "No PHP or FrankenPHP found! Exiting!" + exit 1 +fi + +mkdir -p /run/aikido-${VERSION} +chmod 777 /run/aikido-${VERSION} + +echo "Installation process for Aikido v${VERSION} completed." diff --git a/package/apk/pre-deinstall.sh b/package/apk/pre-deinstall.sh new file mode 100644 index 00000000..1f85c2dc --- /dev/null +++ b/package/apk/pre-deinstall.sh @@ -0,0 +1,86 @@ +#!/bin/sh +set -e + +VERSION="__VERSION__" + +echo "Starting the uninstallation process for Aikido v${VERSION}..." + +pids=$(ps aux 2>/dev/null | grep aikido-agent | grep -v grep | awk '{print $2}') || true +if [ -n "$pids" ]; then + echo "Stopping Aikido Agent processes: $pids" + echo "$pids" | xargs kill -15 2>/dev/null || true + echo "Aikido Agent(s) stopped." +fi + +PHP_VERSIONS="" +if command -v php >/dev/null 2>&1; then + ver=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') + PHP_VERSIONS="$ver" +fi + +for php_path in /usr/bin/php[0-9]* /usr/local/bin/php[0-9]*; do + if [ -x "$php_path" ]; then + ver=$("$php_path" -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;' 2>/dev/null) || continue + case " $PHP_VERSIONS " in + *" $ver "*) ;; + *) PHP_VERSIONS="$PHP_VERSIONS $ver" ;; + esac + fi +done + +echo "Found PHP versions: $PHP_VERSIONS" + +FRANKENPHP_EXT_DIR="/usr/lib/frankenphp/modules" +FRANKENPHP_INI_DIR="/etc/frankenphp/php.d" + +for PHP_VERSION in $PHP_VERSIONS; do + echo "Uninstalling for PHP ${PHP_VERSION}..." + + PHP_BIN="php${PHP_VERSION}" + if ! command -v "$PHP_BIN" >/dev/null 2>&1; then + PHP_BIN="php" + fi + + PHP_EXT_DIR=$("$PHP_BIN" -i | grep "^extension_dir" | awk '{print $3}') + PHP_MOD_DIR=$("$PHP_BIN" -i | grep "Scan this dir for additional .ini files" | awk -F"=> " '{print $2}') + + PHP_DEBIAN_MOD_DIR="/etc/php/${PHP_VERSION}/mods-available" + PHP_DEBIAN_MOD_DIR_CLI="/etc/php/${PHP_VERSION}/cli/conf.d" + PHP_DEBIAN_MOD_DIR_CGI="/etc/php/${PHP_VERSION}/cgi/conf.d" + PHP_DEBIAN_MOD_DIR_FPM="/etc/php/${PHP_VERSION}/fpm/conf.d" + PHP_DEBIAN_MOD_DIR_APACHE2="/etc/php/${PHP_VERSION}/apache2/conf.d" + + if [ -d "$PHP_DEBIAN_MOD_DIR" ]; then + echo "Uninstalling Aikido mod from ${PHP_DEBIAN_MOD_DIR}/aikido-${VERSION}.ini..." + rm -f "${PHP_DEBIAN_MOD_DIR}/aikido-${VERSION}.ini" + for subdir in "$PHP_DEBIAN_MOD_DIR_CLI" "$PHP_DEBIAN_MOD_DIR_CGI" "$PHP_DEBIAN_MOD_DIR_FPM" "$PHP_DEBIAN_MOD_DIR_APACHE2"; do + if [ -d "$subdir" ]; then + rm -f "${subdir}/zz-aikido-${VERSION}.ini" + fi + done + elif [ -d "$PHP_MOD_DIR" ]; then + echo "Uninstalling Aikido mod from ${PHP_MOD_DIR}/zz-aikido-${VERSION}.ini..." + rm -f "${PHP_MOD_DIR}/zz-aikido-${VERSION}.ini" + fi + + if [ -d "$PHP_EXT_DIR" ]; then + echo "Uninstalling Aikido extension from ${PHP_EXT_DIR}/aikido-${VERSION}.so..." + rm -f "${PHP_EXT_DIR}/aikido-${VERSION}.so" + fi +done + +if [ -d "$FRANKENPHP_EXT_DIR" ] || [ -d "$FRANKENPHP_INI_DIR" ]; then + echo "Uninstalling for FrankenPHP..." + rm -f "${FRANKENPHP_EXT_DIR}/aikido-${VERSION}.so" 2>/dev/null || true + rm -f "${FRANKENPHP_INI_DIR}/zz-aikido-${VERSION}.ini" 2>/dev/null || true +fi + +rm -rf "/var/log/aikido-${VERSION}" + +SOCKET_FOLDER="/run/aikido-${VERSION}" +if [ -d "$SOCKET_FOLDER" ]; then + echo "Removing $SOCKET_FOLDER ..." + rm -rf "$SOCKET_FOLDER" +fi + +echo "Uninstallation process for Aikido v${VERSION} completed."