diff options
author | Andrei Belov <defan@nginx.com> | 2021-05-27 17:03:24 +0300 |
---|---|---|
committer | Andrei Belov <defan@nginx.com> | 2021-05-27 17:03:24 +0300 |
commit | 0afb4b5790c5a37ba6b880eb351a65fe00521fbe (patch) | |
tree | c7e0b6bed92ee62a5e8b13c945c4134e68554cec | |
parent | 21ff5e086ece7188df3b7338d228fa4fb7f886af (diff) | |
parent | d06e55dfa3692e27a92ff6c2534bb083416bc0c8 (diff) | |
download | unit-0afb4b5790c5a37ba6b880eb351a65fe00521fbe.tar.gz unit-0afb4b5790c5a37ba6b880eb351a65fe00521fbe.tar.bz2 |
Merged with the default branch.1.24.0-1
189 files changed, 4466 insertions, 1390 deletions
@@ -53,3 +53,4 @@ e3f504b6082ee97ed0d6c8660890585ef6a5796f 1.21.0-1 86b359acc93fe53f1f21b22abc8d1b40ca26158c 1.22.0-1 49ee24c03f5749f8a1b69dc1c600ad48517d9d7a 1.23.0 ad6aad2450c256d4f1a3c32f7091a78dbbc4a6d1 1.23.0-1 +847c88d10f26765b45149c14f88c2274adfc3f42 1.24.0 @@ -1,4 +1,32 @@ +Changes with Unit 1.24.0 27 May 2021 + + *) Change: PHP added to the default MIME type list. + + *) Feature: arbitrary configuration of TLS connections via OpenSSL + commands. + + *) Feature: the ability to limit static file serving by MIME types. + + *) Feature: support for chrooting, rejecting symlinks, and rejecting + mount point traversal on a per-request basis when serving static + files. + + *) Feature: a loader for automatically overriding the "http" and + "websocket" modules in Node.js. + + *) Feature: multiple "targets" in Python applications. + + *) Feature: compatibility with Ruby 3.0. + + *) Bugfix: the router process could crash while closing a TLS + connection. + + *) Bugfix: a segmentation fault might have occurred in the PHP module if + fastcgi_finish_request() was used with the "auto_globals_jit" option + enabled. + + Changes with Unit 1.23.0 25 Mar 2021 *) Feature: support for multiple certificate bundles on a listener via @@ -49,3 +49,35 @@ nxt_feature_test="#include <fcntl.h> return 0; }" . auto/feature + + +nxt_feature="openat2()" +nxt_feature_name=NXT_HAVE_OPENAT2 +nxt_feature_run= +nxt_feature_incs= +nxt_feature_libs= +nxt_feature_test="#include <fcntl.h> + #include <unistd.h> + #include <sys/syscall.h> + #include <linux/openat2.h> + #include <string.h> + + int main() { + struct open_how how; + + memset(&how, 0, sizeof(how)); + + how.flags = O_RDONLY; + how.mode = O_NONBLOCK; + how.resolve = RESOLVE_IN_ROOT + | RESOLVE_NO_SYMLINKS + | RESOLVE_NO_XDEV; + + int fd = syscall(SYS_openat2, AT_FDCWD, \".\", + &how, sizeof(how)); + if (fd == -1) + return 1; + + return 0; + }" +. auto/feature @@ -368,7 +368,7 @@ ${NXT_DAEMON}-install: $NXT_DAEMON install-check manpage-install: manpage install-check install -d \$(DESTDIR)$NXT_MANDIR/man8 - install -p $NXT_BUILD_DIR/unitd.8 \$(DESTDIR)$NXT_MANDIR/man8/ + install -p -m644 $NXT_BUILD_DIR/unitd.8 \$(DESTDIR)$NXT_MANDIR/man8/ .PHONY: uninstall ${NXT_DAEMON}-uninstall manpage-uninstall diff --git a/auto/modules/go b/auto/modules/go index 8bb9216e..7324ffbe 100644 --- a/auto/modules/go +++ b/auto/modules/go @@ -111,7 +111,7 @@ install: ${NXT_GO}-install ${NXT_GO}: ${NXT_GO}-install: ${NXT_GO}-install-src ${NXT_GO}-install-env - GOPATH=\$(DESTDIR)\$(GOPATH) ${NXT_GO} build ${NXT_GO_PKG} + GOPATH=\$(DESTDIR)\$(GOPATH) GO111MODULE=auto ${NXT_GO} build ${NXT_GO_PKG} ${NXT_GO}-install-src: install -d \$(DESTDIR)\$(NXT_GO_DST)/src/${NXT_GO_PKG} diff --git a/auto/ssltls b/auto/ssltls index f034b758..f9363dde 100644 --- a/auto/ssltls +++ b/auto/ssltls @@ -52,6 +52,20 @@ if [ $NXT_OPENSSL = YES ]; then $echo exit 1; fi + + + nxt_feature="OpenSSL SSL_CONF_cmd()" + nxt_feature_name=NXT_HAVE_OPENSSL_CONF_CMD + nxt_feature_run= + nxt_feature_incs= + nxt_feature_libs="$NXT_OPENSSL_LIBS" + nxt_feature_test="#include <openssl/ssl.h> + + int main() { + SSL_CONF_cmd(NULL, NULL, NULL); + return 0; + }" + . auto/feature fi diff --git a/docs/Makefile b/docs/Makefile index db63eec4..d27e69be 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,12 +7,13 @@ PACKAGES= unit \ unit-php \ unit-python unit-python2.7 unit-python3.4 \ unit-python3.5 unit-python3.6 unit-python3.7 \ - unit-python3.8 \ + unit-python3.8 unit-python3.9 \ unit-go unit-go1.7 unit-go1.8 unit-go1.9 unit-go1.10 \ unit-go1.12 unit-go1.13 \ unit-perl \ unit-ruby \ - unit-jsc-common unit-jsc8 unit-jsc10 unit-jsc11 + unit-jsc-common unit-jsc8 unit-jsc10 unit-jsc11 \ + unit-jsc13 unit-jsc14 unit-jsc15 unit-jsc16 unit-jsc17 CURDATE:=$(shell date +"%Y-%m-%d") CURTIME:=$(shell date +"%H:%M:%S %z") diff --git a/docs/changes.xml b/docs/changes.xml index 6c79b560..e3711d0c 100644 --- a/docs/changes.xml +++ b/docs/changes.xml @@ -5,6 +5,118 @@ <change_log title="unit"> +<changes apply="unit-jsc17" ver="1.24.0" rev="1" + date="2021-05-27" time="18:00:00 +0300" + packager="Andrei Belov <defan@nginx.com>"> + +<change> +<para> +Initial release of Java 17 module for NGINX Unit. +</para> +</change> + +</changes> + + +<changes apply="unit-jsc16" ver="1.24.0" rev="1" + date="2021-05-27" time="18:00:00 +0300" + packager="Andrei Belov <defan@nginx.com>"> + +<change> +<para> +Initial release of Java 16 module for NGINX Unit. +</para> +</change> + +</changes> + + +<changes apply="unit-php + unit-python unit-python2.7 + unit-python3.4 unit-python3.5 unit-python3.6 unit-python3.7 + unit-python3.8 unit-python3.9 + unit-go + unit-perl + unit-ruby + unit-jsc-common unit-jsc8 unit-jsc10 unit-jsc11 unit-jsc13 + unit-jsc14 unit-jsc15" + ver="1.24.0" rev="1" + date="2021-05-27" time="18:00:00 +0300" + packager="Andrei Belov <defan@nginx.com>"> + +<change> +<para> +NGINX Unit updated to 1.24.0. +</para> +</change> + +</changes> + + +<changes apply="unit" ver="1.24.0" rev="1" + date="2021-05-27" time="18:00:00 +0300" + packager="Andrei Belov <defan@nginx.com>"> + +<change type="change"> +<para> +PHP added to the default MIME type list. +</para> +</change> + +<change type="feature"> +<para> +arbitrary configuration of TLS connections via OpenSSL commands. +</para> +</change> + +<change type="feature"> +<para> +the ability to limit static file serving by MIME types. +</para> +</change> + +<change type="feature"> +<para> +support for chrooting, rejecting symlinks, and rejecting mount +point traversal on a per-request basis when serving static files. +</para> +</change> + +<change type="feature"> +<para> +a loader for automatically overriding the "http" and "websocket" modules in +Node.js. +</para> +</change> + +<change type="feature"> +<para> +multiple "targets" in Python applications. +</para> +</change> + +<change type="feature"> +<para> +compatibility with Ruby 3.0. +</para> +</change> + +<change type="bugfix"> +<para> +the router process could crash while closing a TLS connection. +</para> +</change> + +<change type="bugfix"> +<para> +a segmentation fault might have occurred in the PHP module if +fastcgi_finish_request() was used with the "auto_globals_jit" option enabled. +</para> +</change> + +</changes> + + <changes apply="unit-php unit-python unit-python2.7 unit-python3.4 unit-python3.5 unit-python3.6 unit-python3.7 diff --git a/pkg/Makefile b/pkg/Makefile index 15ff075d..4cf9ff80 100644 --- a/pkg/Makefile +++ b/pkg/Makefile @@ -14,7 +14,7 @@ dist: hg archive unit-$(VERSION).tar.gz \ -r $(VERSION) \ -p unit-$(VERSION) \ - -X "../.hg*" -X "../pkg/" -X "../docs/" + -X "../.hg*" -X "../pkg/" -X "../docs/*.*" -X "../docs/Makefile" $(SHA512SUM) unit-$(VERSION).tar.gz > unit-$(VERSION).tar.gz.sha512 rpm: diff --git a/pkg/deb/Makefile b/pkg/deb/Makefile index 8c33fc53..c343eb53 100644 --- a/pkg/deb/Makefile +++ b/pkg/deb/Makefile @@ -8,6 +8,8 @@ DEFAULT_RELEASE := 1 VERSION ?= $(DEFAULT_VERSION) RELEASE ?= $(DEFAULT_RELEASE) +PACKAGE_VENDOR = NGINX Packaging <nginx-packaging@f5.com> + SRCDIR= unit-$(VERSION) CODENAME = $(shell lsb_release -cs) @@ -17,6 +19,21 @@ BUILD_DEPENDS = $(BUILD_DEPENDS_unit) MODULES= +# Ubuntu 21.04 +ifeq ($(CODENAME),hirsute) +include Makefile.php +include Makefile.python27 +include Makefile.python39 +include Makefile.go +include Makefile.perl +include Makefile.ruby +include Makefile.jsc-common +include Makefile.jsc11 +include Makefile.jsc15 +include Makefile.jsc16 +include Makefile.jsc17 +endif + # Ubuntu 20.10 ifeq ($(CODENAME),groovy) include Makefile.php @@ -205,6 +222,9 @@ debuild/$(SRCDIR)/debian: echo '9' > debuild/$(SRCDIR)/debian/compat ; \ mkdir -p debuild/$(SRCDIR)/debian/source ; \ echo '3.0 (quilt)' > debuild/$(SRCDIR)/debian/source/format ; \ + cat debian/control.in | sed \ + -e "s#%%PACKAGE_VENDOR%%#$(PACKAGE_VENDOR)#g" \ + > debuild/$(SRCDIR)/debian/control ; \ cat debian/rules.in | sed \ -e "s#%%CONFIGURE_ARGS%%#$(CONFIGURE_ARGS)#g" \ > debuild/$(SRCDIR)/debian/rules ; \ @@ -280,6 +300,7 @@ endif -e "s#%%UNIT_RELEASE%%#$(RELEASE)#g" \ -e "s#%%VERSION%%#$(MODULE_VERSION_$*)#g" \ -e "s#%%RELEASE%%#$(MODULE_RELEASE_$*)#g" \ + -e "s#%%PACKAGE_VENDOR%%#$(PACKAGE_VENDOR)#g" \ -e "s#%%MODULE_BUILD_DEPENDS%%#$(MODULE_BUILD_DEPENDS_$*)#g" \ -e "s#%%MODULE_DEPENDS%%#$(MODULE_DEPENDS_$*)#g" \ > $@/$(SRCDIR)/debian/control ; \ diff --git a/pkg/deb/Makefile.jsc-common b/pkg/deb/Makefile.jsc-common index f7a6010b..1c4a77b5 100644 --- a/pkg/deb/Makefile.jsc-common +++ b/pkg/deb/Makefile.jsc-common @@ -6,7 +6,7 @@ MODULE_SUMMARY_jsc_common= Java shared packages for NGINX Unit MODULE_VERSION_jsc_common= $(VERSION) MODULE_RELEASE_jsc_common= 1 -ifneq (,$(findstring $(CODENAME),groovy focal eoan disco buster)) +ifneq (,$(findstring $(CODENAME),hirsute groovy focal eoan disco buster)) JAVA_MINVERSION= 11 else JAVA_MINVERSION= 8 diff --git a/pkg/deb/Makefile.jsc16 b/pkg/deb/Makefile.jsc16 new file mode 100644 index 00000000..f45e1299 --- /dev/null +++ b/pkg/deb/Makefile.jsc16 @@ -0,0 +1,71 @@ +MODULES+= jsc16 +MODULE_SUFFIX_jsc16= jsc16 + +MODULE_SUMMARY_jsc16= Java 16 module for NGINX Unit + +MODULE_VERSION_jsc16= $(VERSION) +MODULE_RELEASE_jsc16= 1 + +MODULE_CONFARGS_jsc16= java --module=java16 --home=/usr/lib/jvm/java-16-openjdk-$$\(DEB_HOST_ARCH\) --jars=/usr/share/unit-jsc-common/ +MODULE_MAKEARGS_jsc16= java16 +MODULE_INSTARGS_jsc16= java16-install + +MODULE_SOURCES_jsc16= unit.example-jsc-app \ + unit.example-jsc16-config + +BUILD_DEPENDS_jsc16= openjdk-16-jdk-headless openjdk-16-jre-headless +BUILD_DEPENDS+= $(BUILD_DEPENDS_jsc16) + +MODULE_BUILD_DEPENDS_jsc16=,openjdk-16-jdk-headless +MODULE_DEPENDS_jsc16=,openjdk-16-jre-headless,unit-jsc-common (= $(MODULE_VERSION_jsc_common)-$(MODULE_RELEASE_jsc_common)~$(CODENAME)) + +define MODULE_PREINSTALL_jsc16 + mkdir -p debian/unit-jsc16/usr/share/doc/unit-jsc16/examples/jsc-app + install -m 644 -p debian/unit.example-jsc-app debian/unit-jsc16/usr/share/doc/unit-jsc16/examples/jsc-app/index.jsp + install -m 644 -p debian/unit.example-jsc16-config debian/unit-jsc16/usr/share/doc/unit-jsc16/examples/unit.config + install -m 644 -p src/java/README.JSR-340 debian/unit-jsc16/usr/share/doc/unit-jsc16/ +endef +export MODULE_PREINSTALL_jsc16 + +define MODULE_POSTINSTALL_jsc16 + cd $$\(BUILDDIR_unit\) \&\& \ + DESTDIR=$$\(INSTALLDIR\) make java-shared-uninstall +endef +export MODULE_POSTINSTALL_jsc16 + +define MODULE_POST_jsc16 +cat <<BANNER +---------------------------------------------------------------------- + +The $(MODULE_SUMMARY_jsc16) has been installed. + +To check out the sample app, run these commands: + + sudo service unit restart + cd /usr/share/doc/unit-$(MODULE_SUFFIX_jsc16)/examples + sudo curl -X PUT --data-binary @unit.config --unix-socket /var/run/control.unit.sock http://localhost/config + curl http://localhost:8800/ + +Online documentation is available at https://unit.nginx.org + +NOTICE: + +This version of Unit code is made available in support of the open source +development process. This is an intermediate build made available for +testing purposes only. This Unit code is untested and presumed incompatible +with the JSR 340 Java Servlet 3.1 specification. You should not deploy or +write to this code. You should instead deploy and write production +applications on pre-built binaries that have been tested and certified +to meet the JSR-340 compatibility requirements such as certified binaries +published for the JSR-340 reference implementation available at +https://javaee.github.io/glassfish/. + +Redistribution of any Intermediate Build must retain this notice. + +Oracle and Java are registered trademarks of Oracle and/or its affiliates. +Other names may be trademarks of their respective owners. + +---------------------------------------------------------------------- +BANNER +endef +export MODULE_POST_jsc16 diff --git a/pkg/deb/Makefile.jsc17 b/pkg/deb/Makefile.jsc17 new file mode 100644 index 00000000..16f840da --- /dev/null +++ b/pkg/deb/Makefile.jsc17 @@ -0,0 +1,71 @@ +MODULES+= jsc17 +MODULE_SUFFIX_jsc17= jsc17 + +MODULE_SUMMARY_jsc17= Java 17 module for NGINX Unit + +MODULE_VERSION_jsc17= $(VERSION) +MODULE_RELEASE_jsc17= 1 + +MODULE_CONFARGS_jsc17= java --module=java17 --home=/usr/lib/jvm/java-17-openjdk-$$\(DEB_HOST_ARCH\) --jars=/usr/share/unit-jsc-common/ +MODULE_MAKEARGS_jsc17= java17 +MODULE_INSTARGS_jsc17= java17-install + +MODULE_SOURCES_jsc17= unit.example-jsc-app \ + unit.example-jsc17-config + +BUILD_DEPENDS_jsc17= openjdk-17-jdk-headless openjdk-17-jre-headless +BUILD_DEPENDS+= $(BUILD_DEPENDS_jsc17) + +MODULE_BUILD_DEPENDS_jsc17=,openjdk-17-jdk-headless +MODULE_DEPENDS_jsc17=,openjdk-17-jre-headless,unit-jsc-common (= $(MODULE_VERSION_jsc_common)-$(MODULE_RELEASE_jsc_common)~$(CODENAME)) + +define MODULE_PREINSTALL_jsc17 + mkdir -p debian/unit-jsc17/usr/share/doc/unit-jsc17/examples/jsc-app + install -m 644 -p debian/unit.example-jsc-app debian/unit-jsc17/usr/share/doc/unit-jsc17/examples/jsc-app/index.jsp + install -m 644 -p debian/unit.example-jsc17-config debian/unit-jsc17/usr/share/doc/unit-jsc17/examples/unit.config + install -m 644 -p src/java/README.JSR-340 debian/unit-jsc17/usr/share/doc/unit-jsc17/ +endef +export MODULE_PREINSTALL_jsc17 + +define MODULE_POSTINSTALL_jsc17 + cd $$\(BUILDDIR_unit\) \&\& \ + DESTDIR=$$\(INSTALLDIR\) make java-shared-uninstall +endef +export MODULE_POSTINSTALL_jsc17 + +define MODULE_POST_jsc17 +cat <<BANNER +---------------------------------------------------------------------- + +The $(MODULE_SUMMARY_jsc17) has been installed. + +To check out the sample app, run these commands: + + sudo service unit restart + cd /usr/share/doc/unit-$(MODULE_SUFFIX_jsc17)/examples + sudo curl -X PUT --data-binary @unit.config --unix-socket /var/run/control.unit.sock http://localhost/config + curl http://localhost:8800/ + +Online documentation is available at https://unit.nginx.org + +NOTICE: + +This version of Unit code is made available in support of the open source +development process. This is an intermediate build made available for +testing purposes only. This Unit code is untested and presumed incompatible +with the JSR 340 Java Servlet 3.1 specification. You should not deploy or +write to this code. You should instead deploy and write production +applications on pre-built binaries that have been tested and certified +to meet the JSR-340 compatibility requirements such as certified binaries +published for the JSR-340 reference implementation available at +https://javaee.github.io/glassfish/. + +Redistribution of any Intermediate Build must retain this notice. + +Oracle and Java are registered trademarks of Oracle and/or its affiliates. +Other names may be trademarks of their respective owners. + +---------------------------------------------------------------------- +BANNER +endef +export MODULE_POST_jsc17 diff --git a/pkg/deb/Makefile.python39 b/pkg/deb/Makefile.python39 new file mode 100644 index 00000000..11ce65f0 --- /dev/null +++ b/pkg/deb/Makefile.python39 @@ -0,0 +1,46 @@ +MODULES+= python39 +MODULE_SUFFIX_python39= python3.9 + +MODULE_SUMMARY_python39= Python 3.9 module for NGINX Unit + +MODULE_VERSION_python39= $(VERSION) +MODULE_RELEASE_python39= 1 + +MODULE_CONFARGS_python39= python --config=python3.9-config +MODULE_MAKEARGS_python39= python3.9 +MODULE_INSTARGS_python39= python3.9-install + +MODULE_SOURCES_python39= unit.example-python-app \ + unit.example-python3.9-config + +BUILD_DEPENDS_python39= python3.9-dev +BUILD_DEPENDS+= $(BUILD_DEPENDS_python39) + +MODULE_BUILD_DEPENDS_python39=,python3.9-dev + +define MODULE_PREINSTALL_python39 + mkdir -p debian/unit-python3.9/usr/share/doc/unit-python3.9/examples/python-app + install -m 644 -p debian/unit.example-python-app debian/unit-python3.9/usr/share/doc/unit-python3.9/examples/python-app/wsgi.py + install -m 644 -p debian/unit.example-python3.9-config debian/unit-python3.9/usr/share/doc/unit-python3.9/examples/unit.config +endef +export MODULE_PREINSTALL_python39 + +define MODULE_POST_python39 +cat <<BANNER +---------------------------------------------------------------------- + +The $(MODULE_SUMMARY_python39) has been installed. + +To check out the sample app, run these commands: + + sudo service unit restart + cd /usr/share/doc/unit-$(MODULE_SUFFIX_python39)/examples + sudo curl -X PUT --data-binary @unit.config --unix-socket /var/run/control.unit.sock http://localhost/config + curl http://localhost:8400/ + +Online documentation is available at https://unit.nginx.org + +---------------------------------------------------------------------- +BANNER +endef +export MODULE_POST_python39 diff --git a/pkg/deb/debian.module/control-noarch.in b/pkg/deb/debian.module/control-noarch.in index e22bb49a..d9d9e5e1 100644 --- a/pkg/deb/debian.module/control-noarch.in +++ b/pkg/deb/debian.module/control-noarch.in @@ -1,7 +1,7 @@ Source: %%NAME%% Section: admin Priority: extra -Maintainer: Andrei Belov <defan@nginx.com> +Maintainer: %%PACKAGE_VENDOR%% Build-Depends: debhelper (>= 9), linux-libc-dev%%MODULE_BUILD_DEPENDS%% Standards-Version: 3.9.5 diff --git a/pkg/deb/debian.module/control.in b/pkg/deb/debian.module/control.in index 7e28f5e9..9a6fa797 100644 --- a/pkg/deb/debian.module/control.in +++ b/pkg/deb/debian.module/control.in @@ -1,7 +1,7 @@ Source: %%NAME%% Section: admin Priority: extra -Maintainer: Andrei Belov <defan@nginx.com> +Maintainer: %%PACKAGE_VENDOR%% Build-Depends: debhelper (>= 9), linux-libc-dev, libssl-dev, diff --git a/pkg/deb/debian.module/unit.example-jsc16-config b/pkg/deb/debian.module/unit.example-jsc16-config new file mode 100644 index 00000000..0b10a44d --- /dev/null +++ b/pkg/deb/debian.module/unit.example-jsc16-config @@ -0,0 +1,15 @@ +{ + "applications": { + "example_java16": { + "processes": 1, + "type": "java 16", + "webapp": "/usr/share/doc/unit-jsc16/examples/jsc-app" + } + }, + + "listeners": { + "*:8800": { + "pass": "applications/example_java16" + } + } +} diff --git a/pkg/deb/debian.module/unit.example-jsc17-config b/pkg/deb/debian.module/unit.example-jsc17-config new file mode 100644 index 00000000..28b13e4d --- /dev/null +++ b/pkg/deb/debian.module/unit.example-jsc17-config @@ -0,0 +1,15 @@ +{ + "applications": { + "example_java17": { + "processes": 1, + "type": "java 17", + "webapp": "/usr/share/doc/unit-jsc17/examples/jsc-app" + } + }, + + "listeners": { + "*:8800": { + "pass": "applications/example_java17" + } + } +} diff --git a/pkg/deb/debian.module/unit.example-python3.9-config b/pkg/deb/debian.module/unit.example-python3.9-config new file mode 100644 index 00000000..fdb7e9db --- /dev/null +++ b/pkg/deb/debian.module/unit.example-python3.9-config @@ -0,0 +1,16 @@ +{ + "applications": { + "example_python": { + "type": "python 3.9", + "processes": 2, + "path": "/usr/share/doc/unit-python3.9/examples/python-app", + "module": "wsgi" + } + }, + + "listeners": { + "*:8400": { + "pass": "applications/example_python" + } + } +} diff --git a/pkg/deb/debian/control b/pkg/deb/debian/control.in index a8e8cdc4..4d59520e 100644 --- a/pkg/deb/debian/control +++ b/pkg/deb/debian/control.in @@ -1,7 +1,7 @@ Source: unit Section: admin Priority: extra -Maintainer: Andrei Belov <defan@nginx.com> +Maintainer: %%PACKAGE_VENDOR%% Build-Depends: debhelper (>= 9), linux-libc-dev, libssl-dev, diff --git a/pkg/docker/Dockerfile.go1.15 b/pkg/docker/Dockerfile.go1.15 index 45642662..d446a934 100644 --- a/pkg/docker/Dockerfile.go1.15 +++ b/pkg/docker/Dockerfile.go1.15 @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/docker/Dockerfile.jsc11 b/pkg/docker/Dockerfile.jsc11 index 4cfc541d..b66ebe73 100644 --- a/pkg/docker/Dockerfile.jsc11 +++ b/pkg/docker/Dockerfile.jsc11 @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/docker/Dockerfile.minimal b/pkg/docker/Dockerfile.minimal index 59aaa6a3..69a70e33 100644 --- a/pkg/docker/Dockerfile.minimal +++ b/pkg/docker/Dockerfile.minimal @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/docker/Dockerfile.node15 b/pkg/docker/Dockerfile.node15 index 7eddfb26..1e3846a3 100644 --- a/pkg/docker/Dockerfile.node15 +++ b/pkg/docker/Dockerfile.node15 @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/docker/Dockerfile.perl5.32 b/pkg/docker/Dockerfile.perl5.32 index aa000f63..2fccbf63 100644 --- a/pkg/docker/Dockerfile.perl5.32 +++ b/pkg/docker/Dockerfile.perl5.32 @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/docker/Dockerfile.php8.0 b/pkg/docker/Dockerfile.php8.0 index 4534f9b5..02db27cf 100644 --- a/pkg/docker/Dockerfile.php8.0 +++ b/pkg/docker/Dockerfile.php8.0 @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/docker/Dockerfile.python3.9 b/pkg/docker/Dockerfile.python3.9 index e8650d44..44472a12 100644 --- a/pkg/docker/Dockerfile.python3.9 +++ b/pkg/docker/Dockerfile.python3.9 @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/docker/Dockerfile.ruby2.7 b/pkg/docker/Dockerfile.ruby2.7 index aa8cdb3f..7875c470 100644 --- a/pkg/docker/Dockerfile.ruby2.7 +++ b/pkg/docker/Dockerfile.ruby2.7 @@ -8,7 +8,7 @@ RUN set -ex \ && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \ && hg clone https://hg.nginx.org/unit \ && cd unit \ - && hg up 1.23.0 \ + && hg up 1.24.0 \ && NCPU="$(getconf _NPROCESSORS_ONLN)" \ && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \ && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \ diff --git a/pkg/rpm/Makefile b/pkg/rpm/Makefile index 75327c49..e67846cf 100644 --- a/pkg/rpm/Makefile +++ b/pkg/rpm/Makefile @@ -8,6 +8,8 @@ DEFAULT_RELEASE := 1 VERSION ?= $(DEFAULT_VERSION) RELEASE ?= $(DEFAULT_RELEASE) +PACKAGE_VENDOR = NGINX Packaging <nginx-packaging@f5.com> + ifeq ($(shell test `rpm --eval '0%{?rhel} -eq 6 -a 0%{?amzn} -eq 0'`; echo $$?), 0) OSVER = centos6 else ifeq ($(shell test `rpm --eval '0%{?rhel} -eq 7 -a 0%{?amzn} -eq 0'`; echo $$?), 0) @@ -41,8 +43,12 @@ endif ifneq (,$(findstring $(OSVER),opensuse-leap opensuse-tumbleweed sles)) BUILD_DEPENDS_unit += libxml2-tools libxslt1 libopenssl-devel else +ifneq (,$(findstring $(OSVER),amazonlinux2)) +BUILD_DEPENDS_unit += libxml2 libxslt openssl11-devel +else BUILD_DEPENDS_unit += libxml2 libxslt openssl-devel endif +endif BUILD_DEPENDS = $(BUILD_DEPENDS_unit) @@ -201,6 +207,7 @@ rpmbuild/SPECS/unit.spec: unit.spec.in ../../docs/changes.xml | rpmbuild/SPECS sed -e "s#%%VERSION%%#$(VERSION)#g" \ -e "s#%%RELEASE%%#$(RELEASE)#g" \ -e "s#%%CONFIGURE_ARGS%%#$(CONFIGURE_ARGS)#g" \ + -e "s#%%PACKAGE_VENDOR%%#$(PACKAGE_VENDOR)#g" \ > rpmbuild/SPECS/unit.spec cd ../../docs && make ../build/unit.rpm-changelog ifneq ($(DEFAULT_VERSION)$(DEFAULT_RELEASE), $(VERSION)$(RELEASE)) @@ -248,6 +255,7 @@ rpmbuild/SPECS/unit-%.spec: unit.module.spec.in ../../docs/changes.xml | rpmbuil -e "s#%%RELEASE%%#$(MODULE_RELEASE_$*)#g" \ -e "s#%%UNIT_VERSION%%#$(VERSION)#g" \ -e "s#%%UNIT_RELEASE%%#$(RELEASE)#g" \ + -e "s#%%PACKAGE_VENDOR%%#$(PACKAGE_VENDOR)#g" \ -e "s#%%MODULE_SOURCES%%#$${sources}#g" \ -e "s#%%CONFIGURE_ARGS%%#$(CONFIGURE_ARGS)#g" \ -e "s#%%MODULE_CONFARGS%%#$(MODULE_CONFARGS_$*)#g" \ diff --git a/pkg/rpm/unit.module.spec.in b/pkg/rpm/unit.module.spec.in index 39083e66..4f096c73 100644 --- a/pkg/rpm/unit.module.spec.in +++ b/pkg/rpm/unit.module.spec.in @@ -9,8 +9,12 @@ %if 0%{?rhel}%{?fedora} BuildRequires: gcc +%if 0%{?amzn2} +BuildRequires: openssl11-devel +%else BuildRequires: openssl-devel %endif +%endif %if 0%{?suse_version} >= 1315 BuildRequires: libopenssl-devel @@ -28,9 +32,8 @@ Summary: %%SUMMARY%% Version: %%VERSION%% Release: %%RELEASE%%%{?dist}.ngx License: ASL 2.0 -Vendor: Nginx Software, Inc. +Vendor: %%PACKAGE_VENDOR%% URL: https://unit.nginx.org/ -Packager: Nginx Software, Inc. <https://www.nginx.com> Group: System Environment/Daemons Source0: unit-%{version}.tar.gz diff --git a/pkg/rpm/unit.spec.in b/pkg/rpm/unit.spec.in index 3a148b9d..b35b8998 100644 --- a/pkg/rpm/unit.spec.in +++ b/pkg/rpm/unit.spec.in @@ -4,8 +4,12 @@ %if 0%{?rhel}%{?fedora} BuildRequires: gcc +%if 0%{?amzn2} +BuildRequires: openssl11-devel +%else BuildRequires: openssl-devel %endif +%endif %if 0%{?rhel} %if 0%{?amzn} == 0 @@ -29,9 +33,8 @@ Summary: NGINX Unit Version: %%VERSION%% Release: %%RELEASE%%%{?dist}.ngx License: ASL 2.0 -Vendor: Nginx Software, Inc. +Vendor: %%PACKAGE_VENDOR%% URL: https://unit.nginx.org/ -Packager: Nginx Software, Inc. <https://www.nginx.com> Group: System Environment/Daemons Source0: unit-%{version}.tar.gz @@ -203,7 +206,7 @@ BANNER %dir %{_libdir}/unit/modules %dir %{_libdir}/unit/debug-modules %dir %{_sharedstatedir}/unit -%dir %attr(0700,root,root) %{_localstatedir}/log/unit +%dir %attr(0755,root,root) %{_localstatedir}/log/unit %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} %{_mandir}/man8/unitd.8* diff --git a/src/nodejs/unit-http/http.js b/src/nodejs/unit-http/http.js index 3a25fa2f..d298a35f 100644 --- a/src/nodejs/unit-http/http.js +++ b/src/nodejs/unit-http/http.js @@ -5,19 +5,22 @@ 'use strict'; -const server = require('unit-http/http_server'); - -const { Server } = server; +const { + Server, + ServerRequest, + ServerResponse, +} = require('./http_server'); function createServer (requestHandler) { return new Server(requestHandler); } +const http = require("http") module.exports = { + ...http, Server, - STATUS_CODES: server.STATUS_CODES, createServer, - IncomingMessage: server.ServerRequest, - ServerResponse: server.ServerResponse + IncomingMessage: ServerRequest, + ServerResponse, }; diff --git a/src/nodejs/unit-http/http_server.js b/src/nodejs/unit-http/http_server.js index e59296ae..89964ec3 100644 --- a/src/nodejs/unit-http/http_server.js +++ b/src/nodejs/unit-http/http_server.js @@ -444,17 +444,30 @@ Server.prototype.setTimeout = function setTimeout(msecs, callback) { Server.prototype.listen = function (...args) { this.unit.listen(); - const cb = args.pop(); - - if (typeof cb === 'function') { - this.once('listening', cb); + if (typeof args[args.length - 1] === 'function') { + this.once('listening', args[args.length - 1]); } - this.emit('listening'); + /* + * Some express.js apps use the returned server object inside the listening + * callback, so we timeout the listening event to occur after this function + * returns. + */ + setImmediate(function() { + this.emit('listening') + }.bind(this)) return this; }; +Server.prototype.address = function () { + return { + family: "IPv4", + address: "127.0.0.1", + port: 80 + } +} + Server.prototype.emit_request = function (req, res) { if (req._websocket_handshake && this._upgradeListenerCount > 0) { this.emit('upgrade', req, req.socket); @@ -530,7 +543,6 @@ function connectionListener(socket) { } module.exports = { - STATUS_CODES: http.STATUS_CODES, Server, ServerResponse, ServerRequest, diff --git a/src/nodejs/unit-http/loader.js b/src/nodejs/unit-http/loader.js new file mode 100644 index 00000000..e5aa3558 --- /dev/null +++ b/src/nodejs/unit-http/loader.js @@ -0,0 +1,27 @@ +// can only be ran as part of a --require param on the node process +if (module.parent && module.parent.id === "internal/preload") { + const { Module } = require("module") + + if (!Module.prototype.require.__unit_loader) { + const http = require("./http") + const websocket = require("./websocket") + + const original = Module.prototype.require; + + Module.prototype.require = function (id) { + switch(id) { + case "http": + case "unit-http": + return http + + case "websocket": + case "unit-http/websocket": + return websocket + } + + return original.apply(this, arguments); + } + + Module.prototype.require.__unit_loader = true; + } +} diff --git a/src/nodejs/unit-http/loader.mjs b/src/nodejs/unit-http/loader.mjs new file mode 100644 index 00000000..067d63d4 --- /dev/null +++ b/src/nodejs/unit-http/loader.mjs @@ -0,0 +1,18 @@ +// must be ran as part of a --loader or --experimental-loader param +export async function resolve(specifier, context, defaultResolver) { + switch (specifier) { + case "websocket": + return { + url: new URL("./websocket.js", import.meta.url).href, + format: "cjs" + } + + case "http": + return { + url: new URL("./http.js", import.meta.url).href, + format: "cjs" + } + } + + return defaultResolver(specifier, context, defaultResolver) +} diff --git a/src/nxt_application.h b/src/nxt_application.h index 632c5632..45e7fa48 100644 --- a/src/nxt_application.h +++ b/src/nxt_application.h @@ -54,6 +54,7 @@ typedef struct { nxt_str_t protocol; uint32_t threads; uint32_t thread_stack_size; + nxt_conf_value_t *targets; } nxt_python_app_conf_t; diff --git a/src/nxt_cert.c b/src/nxt_cert.c index 3cdb69c1..1806bc19 100644 --- a/src/nxt_cert.c +++ b/src/nxt_cert.c @@ -48,6 +48,7 @@ static nxt_conf_value_t *nxt_cert_name_details(nxt_mp_t *mp, X509 *x509, nxt_bool_t issuer); static nxt_conf_value_t *nxt_cert_alt_names_details(nxt_mp_t *mp, STACK_OF(GENERAL_NAME) *alt_names); +static void nxt_cert_buf_completion(nxt_task_t *task, void *obj, void *data); static nxt_lvlhsh_t nxt_cert_info; @@ -1073,6 +1074,9 @@ nxt_cert_store_get(nxt_task_t *task, nxt_str_t *name, nxt_mp_t *mp, goto fail; } + nxt_mp_retain(mp); + b->completion_handler = nxt_cert_buf_completion; + nxt_buf_cpystr(b, name); *b->mem.free++ = '\0'; @@ -1102,6 +1106,21 @@ fail: } +static void +nxt_cert_buf_completion(nxt_task_t *task, void *obj, void *data) +{ + nxt_mp_t *mp; + nxt_buf_t *b; + + b = obj; + mp = b->data; + nxt_assert(b->next == NULL); + + nxt_mp_free(mp, b); + nxt_mp_release(mp); +} + + void nxt_cert_store_get_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) { diff --git a/src/nxt_conf_validation.c b/src/nxt_conf_validation.c index 8c5d1ec7..06ae2847 100644 --- a/src/nxt_conf_validation.c +++ b/src/nxt_conf_validation.c @@ -75,6 +75,8 @@ static nxt_int_t nxt_conf_vldt_error(nxt_conf_validation_t *vldt, const char *fmt, ...); static nxt_int_t nxt_conf_vldt_var(nxt_conf_validation_t *vldt, const char *option, nxt_str_t *value); +nxt_inline nxt_int_t nxt_conf_vldt_unsupported(nxt_conf_validation_t *vldt, + nxt_conf_value_t *value, void *data); static nxt_int_t nxt_conf_vldt_mtypes(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); @@ -87,6 +89,10 @@ static nxt_int_t nxt_conf_vldt_listener(nxt_conf_validation_t *vldt, #if (NXT_TLS) static nxt_int_t nxt_conf_vldt_certificate(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); +#if (NXT_HAVE_OPENSSL_CONF_CMD) +static nxt_int_t nxt_conf_vldt_object_conf_commands(nxt_conf_validation_t *vldt, + nxt_conf_value_t *value, void *data); +#endif static nxt_int_t nxt_conf_vldt_certificate_element(nxt_conf_validation_t *vldt, nxt_conf_value_t *value); #endif @@ -98,6 +104,8 @@ static nxt_int_t nxt_conf_vldt_return(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); static nxt_int_t nxt_conf_vldt_proxy(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); +static nxt_int_t nxt_conf_vldt_python(nxt_conf_validation_t *vldt, + nxt_conf_value_t *value, void *data); static nxt_int_t nxt_conf_vldt_python_path(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); static nxt_int_t nxt_conf_vldt_python_path_element(nxt_conf_validation_t *vldt, @@ -154,16 +162,16 @@ static nxt_int_t nxt_conf_vldt_array_iterator(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); static nxt_int_t nxt_conf_vldt_environment(nxt_conf_validation_t *vldt, nxt_str_t *name, nxt_conf_value_t *value); +static nxt_int_t nxt_conf_vldt_targets_exclusive( + nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); +static nxt_int_t nxt_conf_vldt_targets(nxt_conf_validation_t *vldt, + nxt_conf_value_t *value, void *data); +static nxt_int_t nxt_conf_vldt_target(nxt_conf_validation_t *vldt, + nxt_str_t *name, nxt_conf_value_t *value); static nxt_int_t nxt_conf_vldt_argument(nxt_conf_validation_t *vldt, nxt_conf_value_t *value); static nxt_int_t nxt_conf_vldt_php(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); -static nxt_int_t nxt_conf_vldt_php_targets_exclusive( - nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data); -static nxt_int_t nxt_conf_vldt_php_targets(nxt_conf_validation_t *vldt, - nxt_conf_value_t *value, void *data); -static nxt_int_t nxt_conf_vldt_php_target(nxt_conf_validation_t *vldt, - nxt_str_t *name, nxt_conf_value_t *value); static nxt_int_t nxt_conf_vldt_php_option(nxt_conf_validation_t *vldt, nxt_str_t *name, nxt_conf_value_t *value); static nxt_int_t nxt_conf_vldt_java_classpath(nxt_conf_validation_t *vldt, @@ -200,8 +208,10 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_static_members[]; static nxt_conf_vldt_object_t nxt_conf_vldt_tls_members[]; #endif static nxt_conf_vldt_object_t nxt_conf_vldt_match_members[]; +static nxt_conf_vldt_object_t nxt_conf_vldt_python_target_members[]; static nxt_conf_vldt_object_t nxt_conf_vldt_php_common_members[]; static nxt_conf_vldt_object_t nxt_conf_vldt_php_options_members[]; +static nxt_conf_vldt_object_t nxt_conf_vldt_php_target_members[]; static nxt_conf_vldt_object_t nxt_conf_vldt_common_members[]; static nxt_conf_vldt_object_t nxt_conf_vldt_app_limits_members[]; static nxt_conf_vldt_object_t nxt_conf_vldt_app_processes_members[]; @@ -357,7 +367,17 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_tls_members[] = { { .name = nxt_string("certificate"), .type = NXT_CONF_VLDT_STRING | NXT_CONF_VLDT_ARRAY, + .flags = NXT_CONF_VLDT_REQUIRED, .validator = nxt_conf_vldt_certificate, + }, { + .name = nxt_string("conf_commands"), + .type = NXT_CONF_VLDT_OBJECT, +#if (NXT_HAVE_OPENSSL_CONF_CMD) + .validator = nxt_conf_vldt_object_conf_commands, +#else + .validator = nxt_conf_vldt_unsupported, + .u.string = "conf_commands", +#endif }, NXT_CONF_VLDT_END @@ -455,9 +475,34 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_share_action_members[] = { .name = nxt_string("share"), .type = NXT_CONF_VLDT_STRING, }, { + .name = nxt_string("types"), + .type = NXT_CONF_VLDT_STRING | NXT_CONF_VLDT_ARRAY, + .validator = nxt_conf_vldt_match_patterns, + }, { .name = nxt_string("fallback"), .type = NXT_CONF_VLDT_OBJECT, .validator = nxt_conf_vldt_action, + }, { + .name = nxt_string("chroot"), + .type = NXT_CONF_VLDT_STRING, +#if !(NXT_HAVE_OPENAT2) + .validator = nxt_conf_vldt_unsupported, + .u.string = "chroot", +#endif + }, { + .name = nxt_string("follow_symlinks"), + .type = NXT_CONF_VLDT_BOOLEAN, +#if !(NXT_HAVE_OPENAT2) + .validator = nxt_conf_vldt_unsupported, + .u.string = "follow_symlinks", +#endif + }, { + .name = nxt_string("traverse_mounts"), + .type = NXT_CONF_VLDT_BOOLEAN, +#if !(NXT_HAVE_OPENAT2) + .validator = nxt_conf_vldt_unsupported, + .u.string = "traverse_mounts", +#endif }, NXT_CONF_VLDT_END @@ -491,7 +536,7 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_external_members[] = { }; -static nxt_conf_vldt_object_t nxt_conf_vldt_python_members[] = { +static nxt_conf_vldt_object_t nxt_conf_vldt_python_common_members[] = { { .name = nxt_string("home"), .type = NXT_CONF_VLDT_STRING, @@ -500,13 +545,6 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_python_members[] = { .type = NXT_CONF_VLDT_STRING | NXT_CONF_VLDT_ARRAY, .validator = nxt_conf_vldt_python_path, }, { - .name = nxt_string("module"), - .type = NXT_CONF_VLDT_STRING, - .flags = NXT_CONF_VLDT_REQUIRED, - }, { - .name = nxt_string("callable"), - .type = NXT_CONF_VLDT_STRING, - }, { .name = nxt_string("protocol"), .type = NXT_CONF_VLDT_STRING, .validator = nxt_conf_vldt_python_protocol, @@ -523,27 +561,77 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_python_members[] = { NXT_CONF_VLDT_NEXT(nxt_conf_vldt_common_members) }; +static nxt_conf_vldt_object_t nxt_conf_vldt_python_members[] = { + { + .name = nxt_string("module"), + .type = NXT_CONF_VLDT_STRING, + .validator = nxt_conf_vldt_targets_exclusive, + .u.string = "module", + }, { + .name = nxt_string("callable"), + .type = NXT_CONF_VLDT_STRING, + .validator = nxt_conf_vldt_targets_exclusive, + .u.string = "callable", + }, { + .name = nxt_string("targets"), + .type = NXT_CONF_VLDT_OBJECT, + .validator = nxt_conf_vldt_targets, + .u.members = nxt_conf_vldt_python_target_members + }, + + NXT_CONF_VLDT_NEXT(nxt_conf_vldt_python_common_members) +}; + + +static nxt_conf_vldt_object_t nxt_conf_vldt_python_target_members[] = { + { + .name = nxt_string("module"), + .type = NXT_CONF_VLDT_STRING, + .flags = NXT_CONF_VLDT_REQUIRED, + }, { + .name = nxt_string("callable"), + .type = NXT_CONF_VLDT_STRING, + }, + + NXT_CONF_VLDT_END +}; + + +static nxt_conf_vldt_object_t nxt_conf_vldt_python_notargets_members[] = { + { + .name = nxt_string("module"), + .type = NXT_CONF_VLDT_STRING, + .flags = NXT_CONF_VLDT_REQUIRED, + }, { + .name = nxt_string("callable"), + .type = NXT_CONF_VLDT_STRING, + }, + + NXT_CONF_VLDT_NEXT(nxt_conf_vldt_python_common_members) +}; + static nxt_conf_vldt_object_t nxt_conf_vldt_php_members[] = { { .name = nxt_string("root"), .type = NXT_CONF_VLDT_ANY_TYPE, - .validator = nxt_conf_vldt_php_targets_exclusive, + .validator = nxt_conf_vldt_targets_exclusive, .u.string = "root", }, { .name = nxt_string("script"), .type = NXT_CONF_VLDT_ANY_TYPE, - .validator = nxt_conf_vldt_php_targets_exclusive, + .validator = nxt_conf_vldt_targets_exclusive, .u.string = "script", }, { .name = nxt_string("index"), .type = NXT_CONF_VLDT_ANY_TYPE, - .validator = nxt_conf_vldt_php_targets_exclusive, + .validator = nxt_conf_vldt_targets_exclusive, .u.string = "index", }, { .name = nxt_string("targets"), .type = NXT_CONF_VLDT_OBJECT, - .validator = nxt_conf_vldt_php_targets, + .validator = nxt_conf_vldt_targets, + .u.members = nxt_conf_vldt_php_target_members }, NXT_CONF_VLDT_NEXT(nxt_conf_vldt_php_common_members) @@ -1032,6 +1120,15 @@ nxt_conf_vldt_error(nxt_conf_validation_t *vldt, const char *fmt, ...) } +nxt_inline nxt_int_t +nxt_conf_vldt_unsupported(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, + void *data) +{ + return nxt_conf_vldt_error(vldt, "Unit is built without the \"%s\" " + "option support.", data); +} + + static nxt_int_t nxt_conf_vldt_var(nxt_conf_validation_t *vldt, const char *option, nxt_str_t *value) @@ -1137,7 +1234,7 @@ nxt_conf_vldt_mtypes_extension(nxt_conf_validation_t *vldt, dup_type = nxt_http_static_mtypes_hash_find(&ctx->hash, &ext); - if (dup_type != NULL) { + if (dup_type->length != 0) { return nxt_conf_vldt_error(vldt, "The \"%V\" file extension has been " "declared for \"%V\" and \"%V\" " "MIME types at the same time.", @@ -1384,6 +1481,25 @@ nxt_conf_vldt_proxy(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, static nxt_int_t +nxt_conf_vldt_python(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, + void *data) +{ + nxt_conf_value_t *targets; + + static nxt_str_t targets_str = nxt_string("targets"); + + targets = nxt_conf_get_object_member(value, &targets_str, NULL); + + if (targets != NULL) { + return nxt_conf_vldt_object(vldt, value, nxt_conf_vldt_python_members); + } + + return nxt_conf_vldt_object(vldt, value, + nxt_conf_vldt_python_notargets_members); +} + + +static nxt_int_t nxt_conf_vldt_python_path(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data) { @@ -1869,6 +1985,38 @@ nxt_conf_vldt_certificate_element(nxt_conf_validation_t *vldt, return NXT_OK; } + +#if (NXT_HAVE_OPENSSL_CONF_CMD) + +static nxt_int_t +nxt_conf_vldt_object_conf_commands(nxt_conf_validation_t *vldt, + nxt_conf_value_t *value, void *data) +{ + uint32_t index; + nxt_int_t ret; + nxt_str_t name; + nxt_conf_value_t *member; + + index = 0; + + for ( ;; ) { + member = nxt_conf_next_object_member(value, &name, &index); + + if (member == NULL) { + break; + } + + ret = nxt_conf_vldt_type(vldt, &name, member, NXT_CONF_VLDT_STRING); + if (ret != NXT_OK) { + return ret; + } + } + + return NXT_OK; +} + +#endif + #endif @@ -1923,7 +2071,7 @@ nxt_conf_vldt_app(nxt_conf_validation_t *vldt, nxt_str_t *name, } types[] = { { nxt_conf_vldt_object, nxt_conf_vldt_external_members }, - { nxt_conf_vldt_object, nxt_conf_vldt_python_members }, + { nxt_conf_vldt_python, NULL }, { nxt_conf_vldt_php, NULL }, { nxt_conf_vldt_object, nxt_conf_vldt_perl_members }, { nxt_conf_vldt_object, nxt_conf_vldt_ruby_members }, @@ -2250,6 +2398,57 @@ nxt_conf_vldt_environment(nxt_conf_validation_t *vldt, nxt_str_t *name, static nxt_int_t +nxt_conf_vldt_targets_exclusive(nxt_conf_validation_t *vldt, + nxt_conf_value_t *value, void *data) +{ + return nxt_conf_vldt_error(vldt, "The \"%s\" option is mutually exclusive " + "with the \"targets\" object.", data); +} + + +static nxt_int_t +nxt_conf_vldt_targets(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, + void *data) +{ + nxt_int_t ret; + nxt_uint_t n; + + n = nxt_conf_object_members_count(value); + + if (n > 254) { + return nxt_conf_vldt_error(vldt, "The \"targets\" object must not " + "contain more than 254 members."); + } + + vldt->ctx = data; + + ret = nxt_conf_vldt_object_iterator(vldt, value, &nxt_conf_vldt_target); + + vldt->ctx = NULL; + + return ret; +} + + +static nxt_int_t +nxt_conf_vldt_target(nxt_conf_validation_t *vldt, nxt_str_t *name, + nxt_conf_value_t *value) +{ + if (name->length == 0) { + return nxt_conf_vldt_error(vldt, + "The target name must not be empty."); + } + + if (nxt_conf_type(value) != NXT_CONF_OBJECT) { + return nxt_conf_vldt_error(vldt, "The \"%V\" target must be " + "an object.", name); + } + + return nxt_conf_vldt_object(vldt, value, vldt->ctx); +} + + +static nxt_int_t nxt_conf_vldt_clone_namespaces(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, void *data) { @@ -2417,51 +2616,6 @@ nxt_conf_vldt_php(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, static nxt_int_t -nxt_conf_vldt_php_targets_exclusive(nxt_conf_validation_t *vldt, - nxt_conf_value_t *value, void *data) -{ - return nxt_conf_vldt_error(vldt, "The \"%s\" option is mutually exclusive " - "with the \"targets\" object.", data); -} - - -static nxt_int_t -nxt_conf_vldt_php_targets(nxt_conf_validation_t *vldt, nxt_conf_value_t *value, - void *data) -{ - nxt_uint_t n; - - n = nxt_conf_object_members_count(value); - - if (n > 254) { - return nxt_conf_vldt_error(vldt, "The \"targets\" object must not " - "contain more than 254 members."); - } - - return nxt_conf_vldt_object_iterator(vldt, value, - &nxt_conf_vldt_php_target); -} - - -static nxt_int_t -nxt_conf_vldt_php_target(nxt_conf_validation_t *vldt, nxt_str_t *name, - nxt_conf_value_t *value) -{ - if (name->length == 0) { - return nxt_conf_vldt_error(vldt, - "The PHP target name must not be empty."); - } - - if (nxt_conf_type(value) != NXT_CONF_OBJECT) { - return nxt_conf_vldt_error(vldt, "The \"%V\" PHP target must be " - "an object.", name); - } - - return nxt_conf_vldt_object(vldt, value, &nxt_conf_vldt_php_target_members); -} - - -static nxt_int_t nxt_conf_vldt_php_option(nxt_conf_validation_t *vldt, nxt_str_t *name, nxt_conf_value_t *value) { diff --git a/src/nxt_errno.h b/src/nxt_errno.h index 40bcfa3f..ec700537 100644 --- a/src/nxt_errno.h +++ b/src/nxt_errno.h @@ -22,6 +22,7 @@ typedef int nxt_err_t; #define NXT_EACCES EACCES #define NXT_EBUSY EBUSY #define NXT_EEXIST EEXIST +#define NXT_ELOOP ELOOP #define NXT_EXDEV EXDEV #define NXT_ENOTDIR ENOTDIR #define NXT_EISDIR EISDIR diff --git a/src/nxt_file.c b/src/nxt_file.c index a9595dd9..5d38d57e 100644 --- a/src/nxt_file.c +++ b/src/nxt_file.c @@ -42,6 +42,50 @@ nxt_file_open(nxt_task_t *task, nxt_file_t *file, nxt_uint_t mode, } +#if (NXT_HAVE_OPENAT2) + +nxt_int_t +nxt_file_openat2(nxt_task_t *task, nxt_file_t *file, nxt_uint_t mode, + nxt_uint_t create, nxt_file_access_t access, nxt_fd_t dfd, + nxt_uint_t resolve) +{ + struct open_how how; + + nxt_memzero(&how, sizeof(how)); + + /* O_NONBLOCK is to prevent blocking on FIFOs, special devices, etc. */ + mode |= (O_NONBLOCK | create); + + how.flags = mode; + how.mode = access; + how.resolve = resolve; + + file->fd = syscall(SYS_openat2, dfd, file->name, &how, sizeof(how)); + + file->error = (file->fd == -1) ? nxt_errno : 0; + +#if (NXT_DEBUG) + nxt_thread_time_update(task->thread); +#endif + + nxt_debug(task, "openat2(%FD, \"%FN\"): %FD err:%d", dfd, file->name, + file->fd, file->error); + + if (file->fd != -1) { + return NXT_OK; + } + + if (file->log_level != 0) { + nxt_log(task, file->log_level, "openat2(%FD, \"%FN\") failed %E", dfd, + file->name, file->error); + } + + return NXT_ERROR; +} + +#endif + + void nxt_file_close(nxt_task_t *task, nxt_file_t *file) { diff --git a/src/nxt_file.h b/src/nxt_file.h index 4f56e746..4846305b 100644 --- a/src/nxt_file.h +++ b/src/nxt_file.h @@ -109,6 +109,12 @@ typedef struct { NXT_EXPORT nxt_int_t nxt_file_open(nxt_task_t *task, nxt_file_t *file, nxt_uint_t mode, nxt_uint_t create, nxt_file_access_t access); +#if (NXT_HAVE_OPENAT2) +NXT_EXPORT nxt_int_t nxt_file_openat2(nxt_task_t *task, nxt_file_t *file, + nxt_uint_t mode, nxt_uint_t create, nxt_file_access_t access, nxt_fd_t dfd, + nxt_uint_t resolve); +#endif + /* The file open access modes. */ #define NXT_FILE_RDONLY O_RDONLY @@ -116,6 +122,32 @@ NXT_EXPORT nxt_int_t nxt_file_open(nxt_task_t *task, nxt_file_t *file, #define NXT_FILE_RDWR O_RDWR #define NXT_FILE_APPEND (O_WRONLY | O_APPEND) +#if (NXT_HAVE_OPENAT2) + +#if defined(O_DIRECTORY) +#define NXT_FILE_DIRECTORY O_DIRECTORY +#else +#define NXT_FILE_DIRECTORY 0 +#endif + +#if defined(O_SEARCH) +#define NXT_FILE_SEARCH (O_SEARCH|NXT_FILE_DIRECTORY) + +#elif defined(O_EXEC) +#define NXT_FILE_SEARCH (O_EXEC|NXT_FILE_DIRECTORY) + +#else +/* + * O_PATH is used in combination with O_RDONLY. The last one is ignored + * if O_PATH is used, but it allows Unit to not fail when it was built on + * modern system (i.e. glibc 2.14+) and run with a kernel older than 2.6.39. + * Then O_PATH is unknown to the kernel and ignored, while O_RDONLY is used. + */ +#define NXT_FILE_SEARCH (O_PATH|O_RDONLY|NXT_FILE_DIRECTORY) +#endif + +#endif /* NXT_HAVE_OPENAT2 */ + /* The file creation modes. */ #define NXT_FILE_CREATE_OR_OPEN O_CREAT #define NXT_FILE_OPEN 0 diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c index d3da6942..d3da6942 100644..100755 --- a/src/nxt_h1proto.c +++ b/src/nxt_h1proto.c diff --git a/src/nxt_http.h b/src/nxt_http.h index e30bfeb4..f82d837e 100644 --- a/src/nxt_http.h +++ b/src/nxt_http.h @@ -197,7 +197,8 @@ struct nxt_http_request_s { }; -typedef struct nxt_http_route_s nxt_http_route_t; +typedef struct nxt_http_route_s nxt_http_route_t; +typedef struct nxt_http_route_rule_s nxt_http_route_rule_t; struct nxt_http_action_s { @@ -206,16 +207,25 @@ struct nxt_http_action_s { nxt_http_action_t *action); union { nxt_http_route_t *route; - nxt_app_t *application; - nxt_http_action_t *fallback; nxt_upstream_t *upstream; uint32_t upstream_number; nxt_http_status_t return_code; nxt_var_t *var; + + struct { + nxt_app_t *application; + nxt_int_t target; + } app; + + struct { + nxt_str_t chroot; + nxt_uint_t resolve; + nxt_http_route_rule_t *types; + nxt_http_action_t *fallback; + } share; } u; nxt_str_t name; - nxt_int_t target; }; @@ -298,6 +308,8 @@ nxt_int_t nxt_http_pass_segments(nxt_mp_t *mp, nxt_str_t *pass, nxt_str_t *segments, nxt_uint_t n); nxt_http_action_t *nxt_http_pass_application(nxt_task_t *task, nxt_router_conf_t *rtcf, nxt_str_t *name); +nxt_int_t nxt_http_route_test_rule(nxt_http_request_t *r, + nxt_http_route_rule_t *rule, u_char *start, size_t length); nxt_int_t nxt_upstreams_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_conf_value_t *conf); diff --git a/src/nxt_http_request.c b/src/nxt_http_request.c index 650c1a89..779cfcf8 100644 --- a/src/nxt_http_request.c +++ b/src/nxt_http_request.c @@ -348,9 +348,9 @@ nxt_http_application_handler(nxt_task_t *task, nxt_http_request_t *r, nxt_str_set(&r->server_name, "localhost"); } - r->app_target = action->target; + r->app_target = action->u.app.target; - nxt_router_process_http_request(task, r, action->u.application); + nxt_router_process_http_request(task, r, action->u.app.application); return NULL; } diff --git a/src/nxt_http_route.c b/src/nxt_http_route.c index 28545fc9..15b85544 100644 --- a/src/nxt_http_route.c +++ b/src/nxt_http_route.c @@ -50,8 +50,12 @@ typedef struct { nxt_conf_value_t *pass; nxt_conf_value_t *ret; nxt_str_t location; - nxt_conf_value_t *share; nxt_conf_value_t *proxy; + nxt_conf_value_t *share; + nxt_str_t chroot; + nxt_conf_value_t *follow_symlinks; + nxt_conf_value_t *traverse_mounts; + nxt_conf_value_t *types; nxt_conf_value_t *fallback; } nxt_http_route_action_conf_t; @@ -112,7 +116,7 @@ typedef struct { } nxt_http_cookie_t; -typedef struct { +struct nxt_http_route_rule_s { /* The object must be the first field. */ nxt_http_route_object_t object:8; uint32_t items; @@ -128,7 +132,7 @@ typedef struct { } u; nxt_http_route_pattern_t pattern[0]; -} nxt_http_route_rule_t; +}; typedef struct { @@ -195,8 +199,9 @@ static nxt_http_route_t *nxt_http_route_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_conf_value_t *cv); static nxt_http_route_match_t *nxt_http_route_match_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_conf_value_t *cv); -static nxt_int_t nxt_http_route_action_create(nxt_router_temp_conf_t *tmcf, - nxt_conf_value_t *cv, nxt_http_action_t *action); +static nxt_int_t nxt_http_route_action_create(nxt_task_t *task, + nxt_router_temp_conf_t *tmcf, nxt_conf_value_t *cv, + nxt_http_action_t *action); static nxt_http_route_table_t *nxt_http_route_table_create(nxt_task_t *task, nxt_mp_t *mp, nxt_conf_value_t *table_cv, nxt_http_route_object_t object, nxt_bool_t case_sensitive, nxt_http_route_encoding_t encoding); @@ -274,8 +279,6 @@ static nxt_http_name_value_t *nxt_http_route_cookie(nxt_array_t *array, u_char *name, size_t name_length, u_char *start, u_char *end); static nxt_int_t nxt_http_route_test_cookie(nxt_http_request_t *r, nxt_http_route_rule_t *rule, nxt_array_t *array); -static nxt_int_t nxt_http_route_test_rule(nxt_http_request_t *r, - nxt_http_route_rule_t *rule, u_char *start, size_t length); static nxt_int_t nxt_http_route_pattern(nxt_http_request_t *r, nxt_http_route_pattern_t *pattern, u_char *start, size_t length); static nxt_int_t nxt_http_route_memcmp(u_char *start, u_char *test, @@ -457,7 +460,7 @@ nxt_http_route_match_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, match_conf = nxt_conf_get_path(cv, &match_path); n = (match_conf != NULL) ? nxt_conf_object_members_count(match_conf) : 0; - size = sizeof(nxt_http_route_match_t) + n * sizeof(nxt_http_route_rule_t *); + size = sizeof(nxt_http_route_match_t) + n * sizeof(nxt_http_route_test_t *); mp = tmcf->router_conf->mem_pool; @@ -473,7 +476,7 @@ nxt_http_route_match_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, return NULL; } - ret = nxt_http_route_action_create(tmcf, action_conf, &match->action); + ret = nxt_http_route_action_create(task, tmcf, action_conf, &match->action); if (nxt_slow_path(ret != NXT_OK)) { return NULL; } @@ -627,14 +630,34 @@ static nxt_conf_map_t nxt_http_route_action_conf[] = { offsetof(nxt_http_route_action_conf_t, location) }, { + nxt_string("proxy"), + NXT_CONF_MAP_PTR, + offsetof(nxt_http_route_action_conf_t, proxy) + }, + { nxt_string("share"), NXT_CONF_MAP_PTR, offsetof(nxt_http_route_action_conf_t, share) }, { - nxt_string("proxy"), + nxt_string("chroot"), + NXT_CONF_MAP_STR, + offsetof(nxt_http_route_action_conf_t, chroot) + }, + { + nxt_string("follow_symlinks"), NXT_CONF_MAP_PTR, - offsetof(nxt_http_route_action_conf_t, proxy) + offsetof(nxt_http_route_action_conf_t, follow_symlinks) + }, + { + nxt_string("traverse_mounts"), + NXT_CONF_MAP_PTR, + offsetof(nxt_http_route_action_conf_t, traverse_mounts) + }, + { + nxt_string("types"), + NXT_CONF_MAP_PTR, + offsetof(nxt_http_route_action_conf_t, types) }, { nxt_string("fallback"), @@ -645,14 +668,20 @@ static nxt_conf_map_t nxt_http_route_action_conf[] = { static nxt_int_t -nxt_http_route_action_create(nxt_router_temp_conf_t *tmcf, nxt_conf_value_t *cv, - nxt_http_action_t *action) +nxt_http_route_action_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, + nxt_conf_value_t *cv, nxt_http_action_t *action) { +#if (NXT_HAVE_OPENAT2) + u_char *p; + uint8_t slash; + nxt_str_t *chroot; +#endif nxt_mp_t *mp; nxt_int_t ret; nxt_str_t name, *string; nxt_uint_t encode; nxt_conf_value_t *conf; + nxt_http_route_rule_t *rule; nxt_http_route_action_conf_t accf; nxt_memzero(&accf, sizeof(accf)); @@ -700,14 +729,14 @@ nxt_http_route_action_create(nxt_router_temp_conf_t *tmcf, nxt_conf_value_t *cv, return NXT_OK; } - conf = accf.pass; - if (accf.share != NULL) { conf = accf.share; - action->handler = nxt_http_static_handler; } else if (accf.proxy != NULL) { conf = accf.proxy; + + } else { + conf = accf.pass; } nxt_conf_get_string(conf, &name); @@ -717,14 +746,70 @@ nxt_http_route_action_create(nxt_router_temp_conf_t *tmcf, nxt_conf_value_t *cv, return NXT_ERROR; } - if (accf.fallback != NULL) { - action->u.fallback = nxt_mp_alloc(mp, sizeof(nxt_http_action_t)); - if (nxt_slow_path(action->u.fallback == NULL)) { - return NXT_ERROR; + if (accf.share != NULL) { + action->handler = nxt_http_static_handler; + +#if (NXT_HAVE_OPENAT2) + string = &accf.chroot; + chroot = &action->u.share.chroot; + + if (string->length > 0) { + action->u.share.resolve |= RESOLVE_IN_ROOT; + + slash = (string->start[string->length - 1] != '/'); + + chroot->length = string->length + (slash ? 1 : 0); + + chroot->start = nxt_mp_alloc(mp, chroot->length + 1); + if (nxt_slow_path(chroot->start == NULL)) { + return NXT_ERROR; + } + + p = nxt_cpymem(chroot->start, string->start, string->length); + + if (slash) { + *p++ = '/'; + } + + *p = '\0'; + } + + if (accf.follow_symlinks != NULL + && !nxt_conf_get_boolean(accf.follow_symlinks)) + { + action->u.share.resolve |= RESOLVE_NO_SYMLINKS; + } + + if (accf.traverse_mounts != NULL + && !nxt_conf_get_boolean(accf.traverse_mounts)) + { + action->u.share.resolve |= RESOLVE_NO_XDEV; + } +#endif + + if (accf.types != NULL) { + rule = nxt_http_route_rule_create(task, mp, accf.types, 0, + NXT_HTTP_ROUTE_PATTERN_LOWCASE, + NXT_HTTP_ROUTE_ENCODING_NONE); + if (nxt_slow_path(rule == NULL)) { + return NXT_ERROR; + } + + action->u.share.types = rule; } - return nxt_http_route_action_create(tmcf, accf.fallback, - action->u.fallback); + if (accf.fallback != NULL) { + action->u.share.fallback = nxt_mp_alloc(mp, + sizeof(nxt_http_action_t)); + if (nxt_slow_path(action->u.share.fallback == NULL)) { + return NXT_ERROR; + } + + return nxt_http_route_action_create(task, tmcf, accf.fallback, + action->u.share.fallback); + } + + return NXT_OK; } if (accf.proxy != NULL) { @@ -1411,9 +1496,10 @@ nxt_http_action_resolve(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, if (action->handler != NULL) { if (action->handler == nxt_http_static_handler - && action->u.fallback != NULL) + && action->u.share.fallback != NULL) { - return nxt_http_action_resolve(task, tmcf, action->u.fallback); + return nxt_http_action_resolve(task, tmcf, + action->u.share.fallback); } return NXT_OK; @@ -1533,14 +1619,14 @@ nxt_http_pass_find(nxt_task_t *task, nxt_mp_t *mp, nxt_router_conf_t *rtcf, } if (segments[2].length != 0) { - targets = action->u.application->targets; + targets = action->u.app.application->targets; for (i = 0; !nxt_strstr_eq(&segments[2], &targets[i]); i++); - action->target = i; + action->u.app.target = i; } else { - action->target = 0; + action->u.app.target = 0; } return NXT_OK; @@ -1678,7 +1764,7 @@ nxt_http_pass_application(nxt_task_t *task, nxt_router_conf_t *rtcf, (void) nxt_router_listener_application(rtcf, name, action); - action->target = 0; + action->u.app.target = 0; return action; } @@ -2426,7 +2512,7 @@ nxt_http_route_test_cookie(nxt_http_request_t *r, } -static nxt_int_t +nxt_int_t nxt_http_route_test_rule(nxt_http_request_t *r, nxt_http_route_rule_t *rule, u_char *start, size_t length) { diff --git a/src/nxt_http_static.c b/src/nxt_http_static.c index df2655fc..c8b73fac 100644 --- a/src/nxt_http_static.c +++ b/src/nxt_http_static.c @@ -31,15 +31,15 @@ nxt_http_action_t * nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, nxt_http_action_t *action) { - size_t alloc, encode; - u_char *p; + size_t length, encode; + u_char *p, *fname; struct tm tm; nxt_buf_t *fb; nxt_int_t ret; - nxt_str_t index, extension, *mtype; + nxt_str_t index, extension, *mtype, *chroot; nxt_uint_t level; nxt_bool_t need_body; - nxt_file_t *f; + nxt_file_t *f, file; nxt_file_info_t fi; nxt_http_field_t *field; nxt_http_status_t status; @@ -49,8 +49,8 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, if (nxt_slow_path(!nxt_str_eq(r->method, "GET", 3))) { if (!nxt_str_eq(r->method, "HEAD", 4)) { - if (action->u.fallback != NULL) { - return action->u.fallback; + if (action->u.share.fallback != NULL) { + return action->u.share.fallback; } nxt_http_request_error(task, r, NXT_HTTP_METHOD_NOT_ALLOWED); @@ -63,13 +63,6 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, need_body = 1; } - f = nxt_mp_zget(r->mem_pool, sizeof(nxt_file_t)); - if (nxt_slow_path(f == NULL)) { - goto fail; - } - - f->fd = NXT_FILE_INVALID; - if (r->path->start[r->path->length - 1] == '/') { /* TODO: dynamic index setting. */ nxt_str_set(&index, "index.html"); @@ -80,23 +73,110 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, nxt_str_null(&extension); } - alloc = action->name.length + r->path->length + index.length + 1; + f = NULL; + + rtcf = r->conf->socket_conf->router_conf; + + mtype = NULL; + + if (action->u.share.types != NULL && extension.start == NULL) { + nxt_http_static_extract_extension(r->path, &extension); + mtype = nxt_http_static_mtypes_hash_find(&rtcf->mtypes_hash, + &extension); + + ret = nxt_http_route_test_rule(r, action->u.share.types, + mtype->start, mtype->length); + if (nxt_slow_path(ret == NXT_ERROR)) { + goto fail; + } + + if (ret == 0) { + if (action->u.share.fallback != NULL) { + return action->u.share.fallback; + } - f->name = nxt_mp_nget(r->mem_pool, alloc); - if (nxt_slow_path(f->name == NULL)) { + nxt_http_request_error(task, r, NXT_HTTP_FORBIDDEN); + return NULL; + } + } + + length = action->name.length + r->path->length + index.length; + + fname = nxt_mp_nget(r->mem_pool, length + 1); + if (nxt_slow_path(fname == NULL)) { goto fail; } - p = f->name; + p = fname; p = nxt_cpymem(p, action->name.start, action->name.length); p = nxt_cpymem(p, r->path->start, r->path->length); p = nxt_cpymem(p, index.start, index.length); *p = '\0'; - ret = nxt_file_open(task, f, NXT_FILE_RDONLY, NXT_FILE_OPEN, 0); + nxt_memzero(&file, sizeof(nxt_file_t)); + + file.name = fname; + + chroot = &action->u.share.chroot; + +#if (NXT_HAVE_OPENAT2) + + if (action->u.share.resolve != 0) { + + if (chroot->length > 0) { + file.name = chroot->start; + + if (length > chroot->length + && nxt_memcmp(fname, chroot->start, chroot->length) == 0) + { + fname += chroot->length; + ret = nxt_file_open(task, &file, NXT_FILE_SEARCH, NXT_FILE_OPEN, + 0); + + } else { + file.error = NXT_EACCES; + ret = NXT_ERROR; + } + + } else if (fname[0] == '/') { + file.name = (u_char *) "/"; + ret = nxt_file_open(task, &file, NXT_FILE_SEARCH, NXT_FILE_OPEN, 0); + + } else { + file.name = (u_char *) "."; + file.fd = AT_FDCWD; + ret = NXT_OK; + } + + if (nxt_fast_path(ret == NXT_OK)) { + nxt_file_t af; + + af = file; + nxt_memzero(&file, sizeof(nxt_file_t)); + file.name = fname; + + ret = nxt_file_openat2(task, &file, NXT_FILE_RDONLY, + NXT_FILE_OPEN, 0, af.fd, + action->u.share.resolve); + + if (af.fd != AT_FDCWD) { + nxt_file_close(task, &af); + } + } + + } else { + ret = nxt_file_open(task, &file, NXT_FILE_RDONLY, NXT_FILE_OPEN, 0); + } + +#else + + ret = nxt_file_open(task, &file, NXT_FILE_RDONLY, NXT_FILE_OPEN, 0); + +#endif if (nxt_slow_path(ret != NXT_OK)) { - switch (f->error) { + + switch (file.error) { /* * For Unix domain sockets "errno" is set to: @@ -117,6 +197,10 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, break; case NXT_EACCES: +#if (NXT_HAVE_OPENAT2) + case NXT_ELOOP: + case NXT_EXDEV: +#endif level = NXT_LOG_ERR; status = NXT_HTTP_FORBIDDEN; break; @@ -127,18 +211,32 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, break; } - if (level == NXT_LOG_ERR && action->u.fallback != NULL) { - return action->u.fallback; + if (level == NXT_LOG_ERR && action->u.share.fallback != NULL) { + return action->u.share.fallback; } if (status != NXT_HTTP_NOT_FOUND) { - nxt_log(task, level, "open(\"%FN\") failed %E", f->name, f->error); + if (chroot->length > 0) { + nxt_log(task, level, "opening \"%s\" at \"%V\" failed %E", + fname, chroot, file.error); + + } else { + nxt_log(task, level, "opening \"%s\" failed %E", + fname, file.error); + } } nxt_http_request_error(task, r, status); return NULL; } + f = nxt_mp_get(r->mem_pool, sizeof(nxt_file_t)); + if (nxt_slow_path(f == NULL)) { + goto fail; + } + + *f = file; + ret = nxt_file_info(f, &fi); if (nxt_slow_path(ret != NXT_OK)) { goto fail; @@ -172,15 +270,15 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, nxt_http_field_name_set(field, "ETag"); - alloc = NXT_TIME_T_HEXLEN + NXT_OFF_T_HEXLEN + 3; + length = NXT_TIME_T_HEXLEN + NXT_OFF_T_HEXLEN + 3; - p = nxt_mp_nget(r->mem_pool, alloc); + p = nxt_mp_nget(r->mem_pool, length); if (nxt_slow_path(p == NULL)) { goto fail; } field->value = p; - field->value_length = nxt_sprintf(p, p + alloc, "\"%xT-%xO\"", + field->value_length = nxt_sprintf(p, p + length, "\"%xT-%xO\"", nxt_file_mtime(&fi), nxt_file_size(&fi)) - p; @@ -189,12 +287,12 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, nxt_http_static_extract_extension(r->path, &extension); } - rtcf = r->conf->socket_conf->router_conf; - - mtype = nxt_http_static_mtypes_hash_find(&rtcf->mtypes_hash, - &extension); + if (mtype == NULL) { + mtype = nxt_http_static_mtypes_hash_find(&rtcf->mtypes_hash, + &extension); + } - if (mtype != NULL) { + if (mtype->length != 0) { field = nxt_list_zero_add(r->resp.fields); if (nxt_slow_path(field == NULL)) { goto fail; @@ -230,8 +328,8 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, nxt_file_close(task, f); if (nxt_slow_path(!nxt_is_dir(&fi))) { - if (action->u.fallback != NULL) { - return action->u.fallback; + if (action->u.share.fallback != NULL) { + return action->u.share.fallback; } nxt_log(task, NXT_LOG_ERR, "\"%FN\" is not a regular file", @@ -254,19 +352,19 @@ nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r, nxt_http_field_name_set(field, "Location"); encode = nxt_encode_uri(NULL, r->path->start, r->path->length); - alloc = r->path->length + encode * 2 + 1; + length = r->path->length + encode * 2 + 1; if (r->args->length > 0) { - alloc += 1 + r->args->length; + length += 1 + r->args->length; } - p = nxt_mp_nget(r->mem_pool, alloc); + p = nxt_mp_nget(r->mem_pool, length); if (nxt_slow_path(p == NULL)) { goto fail; } field->value = p; - field->value_length = alloc; + field->value_length = length; if (encode > 0) { p = (u_char *) nxt_encode_uri(p, r->path->start, r->path->length); @@ -294,7 +392,7 @@ fail: nxt_http_request_error(task, r, NXT_HTTP_INTERNAL_SERVER_ERROR); - if (f != NULL && f->fd != NXT_FILE_INVALID) { + if (f != NULL) { nxt_file_close(task, f); } @@ -539,6 +637,8 @@ nxt_http_static_mtypes_init(nxt_mp_t *mp, nxt_lvlhsh_t *hash) { nxt_string("application/octet-stream"), ".deb" }, { nxt_string("application/octet-stream"), ".rpm" }, + + { nxt_string("application/x-httpd-php"), ".php" }, }; for (i = 0; i < nxt_nitems(default_types); i++) { @@ -605,6 +705,8 @@ nxt_http_static_mtypes_hash_find(nxt_lvlhsh_t *hash, nxt_str_t *extension) nxt_lvlhsh_query_t lhq; nxt_http_static_mtype_t *mtype; + static nxt_str_t empty = nxt_string(""); + lhq.key = *extension; lhq.key_hash = nxt_djb_hash_lowcase(lhq.key.start, lhq.key.length); lhq.proto = &nxt_http_static_mtypes_hash_proto; @@ -614,7 +716,7 @@ nxt_http_static_mtypes_hash_find(nxt_lvlhsh_t *hash, nxt_str_t *extension) return mtype->type; } - return NULL; + return ∅ } diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c index f20f2c2c..00f336f6 100644 --- a/src/nxt_main_process.c +++ b/src/nxt_main_process.c @@ -211,6 +211,12 @@ static nxt_conf_map_t nxt_python_app_conf[] = { }, { + nxt_string("targets"), + NXT_CONF_MAP_PTR, + offsetof(nxt_common_app_conf_t, u.python.targets), + }, + + { nxt_string("thread_stack_size"), NXT_CONF_MAP_INT32, offsetof(nxt_common_app_conf_t, u.python.thread_stack_size), diff --git a/src/nxt_openssl.c b/src/nxt_openssl.c index 9b86150f..2fd5d1bf 100644 --- a/src/nxt_openssl.c +++ b/src/nxt_openssl.c @@ -5,6 +5,7 @@ */ #include <nxt_main.h> +#include <nxt_conf.h> #include <openssl/ssl.h> #include <openssl/conf.h> #include <openssl/err.h> @@ -42,9 +43,14 @@ static unsigned long nxt_openssl_thread_id(void); static void nxt_openssl_locks_free(void); #endif static nxt_int_t nxt_openssl_server_init(nxt_task_t *task, - nxt_tls_conf_t *conf, nxt_mp_t *mp, nxt_bool_t last); + nxt_tls_conf_t *conf, nxt_mp_t *mp, nxt_conf_value_t *conf_cmds, + nxt_bool_t last); static nxt_int_t nxt_openssl_chain_file(nxt_task_t *task, SSL_CTX *ctx, nxt_tls_conf_t *conf, nxt_mp_t *mp, nxt_bool_t single); +#if (NXT_HAVE_OPENSSL_CONF_CMD) +static nxt_int_t nxt_ssl_conf_commands(nxt_task_t *task, SSL_CTX *ctx, + nxt_conf_value_t *value, nxt_mp_t *mp); +#endif static nxt_uint_t nxt_openssl_cert_get_names(nxt_task_t *task, X509 *cert, nxt_tls_conf_t *conf, nxt_mp_t *mp); static nxt_int_t nxt_openssl_bundle_hash_test(nxt_lvlhsh_query_t *lhq, @@ -66,6 +72,8 @@ static void nxt_openssl_conn_io_shutdown(nxt_task_t *task, void *obj, void *data); static nxt_int_t nxt_openssl_conn_test_error(nxt_task_t *task, nxt_conn_t *c, int ret, nxt_err_t sys_err, nxt_openssl_io_t io); +static void nxt_openssl_conn_io_shutdown_timeout(nxt_task_t *task, void *obj, + void *data); static void nxt_cdecl nxt_openssl_conn_error(nxt_task_t *task, nxt_err_t err, const char *fmt, ...); static nxt_uint_t nxt_openssl_log_error_level(nxt_err_t err); @@ -258,7 +266,7 @@ nxt_openssl_locks_free(void) static nxt_int_t nxt_openssl_server_init(nxt_task_t *task, nxt_tls_conf_t *conf, - nxt_mp_t *mp, nxt_bool_t last) + nxt_mp_t *mp, nxt_conf_value_t *conf_cmds, nxt_bool_t last) { SSL_CTX *ctx; const char *ciphers, *ca_certificate; @@ -318,6 +326,7 @@ nxt_openssl_server_init(nxt_task_t *task, nxt_tls_conf_t *conf, goto fail; } */ + ciphers = (conf->ciphers != NULL) ? conf->ciphers : "HIGH:!aNULL:!MD5"; if (SSL_CTX_set_cipher_list(ctx, ciphers) == 0) { @@ -327,6 +336,14 @@ nxt_openssl_server_init(nxt_task_t *task, nxt_tls_conf_t *conf, goto fail; } +#if (NXT_HAVE_OPENSSL_CONF_CMD) + if (conf_cmds != NULL + && nxt_ssl_conf_commands(task, ctx, conf_cmds, mp) != NXT_OK) + { + goto fail; + } +#endif + SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); if (conf->ca_certificate != NULL) { @@ -482,6 +499,89 @@ clean: } +#if (NXT_HAVE_OPENSSL_CONF_CMD) + +static nxt_int_t +nxt_ssl_conf_commands(nxt_task_t *task, SSL_CTX *ctx, nxt_conf_value_t *conf, + nxt_mp_t *mp) +{ + int ret; + char *zcmd, *zvalue; + uint32_t index; + nxt_str_t cmd, value; + SSL_CONF_CTX *cctx; + nxt_conf_value_t *member; + + cctx = SSL_CONF_CTX_new(); + if (nxt_slow_path(cctx == NULL)) { + nxt_openssl_log_error(task, NXT_LOG_ALERT, + "SSL_CONF_CTX_new() failed"); + return NXT_ERROR; + } + + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_FILE + | SSL_CONF_FLAG_SERVER + | SSL_CONF_FLAG_CERTIFICATE + | SSL_CONF_FLAG_SHOW_ERRORS); + + SSL_CONF_CTX_set_ssl_ctx(cctx, ctx); + + index = 0; + + for ( ;; ) { + member = nxt_conf_next_object_member(conf, &cmd, &index); + if (nxt_slow_path(member == NULL)) { + break; + } + + nxt_conf_get_string(member, &value); + + zcmd = nxt_str_cstrz(mp, &cmd); + zvalue = nxt_str_cstrz(mp, &value); + + if (nxt_slow_path(zcmd == NULL || zvalue == NULL)) { + goto fail; + } + + ret = SSL_CONF_cmd(cctx, zcmd, zvalue); + if (ret == -2) { + nxt_openssl_log_error(task, NXT_LOG_ERR, + "unknown command \"%s\" in " + "\"conf_commands\" option", zcmd); + goto fail; + } + + if (ret <= 0) { + nxt_openssl_log_error(task, NXT_LOG_ERR, + "invalid value \"%s\" for command \"%s\" " + "in \"conf_commands\" option", + zvalue, zcmd); + goto fail; + } + + nxt_debug(task, "SSL_CONF_cmd(\"%s\", \"%s\")", zcmd, zvalue); + } + + if (SSL_CONF_CTX_finish(cctx) != 1) { + nxt_openssl_log_error(task, NXT_LOG_ALERT, + "SSL_CONF_finish() failed"); + goto fail; + } + + SSL_CONF_CTX_free(cctx); + + return NXT_OK; + +fail: + + SSL_CONF_CTX_free(cctx); + + return NXT_ERROR; +} + +#endif + + static nxt_uint_t nxt_openssl_cert_get_names(nxt_task_t *task, X509 *cert, nxt_tls_conf_t *conf, nxt_mp_t *mp) @@ -548,7 +648,7 @@ nxt_openssl_cert_get_names(nxt_task_t *task, X509 *cert, nxt_tls_conf_t *conf, NULL, 0); if (len <= 0) { nxt_log(task, NXT_LOG_WARN, "certificate \"%V\" has neither " - "Subject Alternative Name nor Common Name", bundle->name); + "Subject Alternative Name nor Common Name", &bundle->name); return NXT_OK; } @@ -627,7 +727,7 @@ nxt_openssl_bundle_hash_insert(nxt_task_t *task, nxt_lvlhsh_t *lvlhsh, if (item->name.length == 0 || item->name.start[0] != '.') { nxt_log(task, NXT_LOG_WARN, "ignored invalid name \"%V\" " "in certificate \"%V\": missing \".\" " - "after wildcard symbol", &str, item->bundle->name); + "after wildcard symbol", &str, &item->bundle->name); return NXT_OK; } } @@ -642,7 +742,7 @@ nxt_openssl_bundle_hash_insert(nxt_task_t *task, nxt_lvlhsh_t *lvlhsh, ret = nxt_lvlhsh_insert(lvlhsh, &lhq); if (nxt_fast_path(ret == NXT_OK)) { nxt_debug(task, "name \"%V\" for certificate \"%V\" is inserted", - &str, item->bundle->name); + &str, &item->bundle->name); return NXT_OK; } @@ -651,7 +751,7 @@ nxt_openssl_bundle_hash_insert(nxt_task_t *task, nxt_lvlhsh_t *lvlhsh, if (old->bundle != item->bundle) { nxt_log(task, NXT_LOG_WARN, "ignored duplicate name \"%V\" " "in certificate \"%V\", identical name appears in \"%V\"", - &str, old->bundle->name, item->bundle->name); + &str, &old->bundle->name, &item->bundle->name); old->bundle = item->bundle; } @@ -728,8 +828,8 @@ nxt_openssl_servername(SSL *s, int *ad, void *arg) if (bundle != NULL) { nxt_debug(c->socket.task, "new tls context found for \"%V\": \"%V\" " - "(old: \"%V\")", &str, bundle->name, - conf->bundle->name); + "(old: \"%V\")", &str, &bundle->name, + &conf->bundle->name); if (bundle != conf->bundle) { if (SSL_set_SSL_CTX(s, bundle->ctx) == NULL) { @@ -839,11 +939,7 @@ nxt_openssl_conn_init(nxt_task_t *task, nxt_tls_conf_t *conf, nxt_conn_t *c) c->sendfile = NXT_CONN_SENDFILE_OFF; nxt_openssl_conn_handshake(task, c, c->socket.data); - /* - * TLS configuration might be destroyed after the TLS connection - * is established. - */ - tls->conf = NULL; + return; fail: @@ -1099,6 +1195,10 @@ nxt_openssl_conn_io_shutdown(nxt_task_t *task, void *obj, void *data) SSL_set_quiet_shutdown(s, quiet); + if (tls->conf->no_wait_shutdown) { + mode |= SSL_RECEIVED_SHUTDOWN; + } + once = 1; for ( ;; ) { @@ -1153,7 +1253,8 @@ nxt_openssl_conn_io_shutdown(nxt_task_t *task, void *obj, void *data) break; case NXT_AGAIN: - nxt_timer_add(task->thread->engine, &c->read_timer, 5000); + c->write_timer.handler = nxt_openssl_conn_io_shutdown_timeout; + nxt_timer_add(task->thread->engine, &c->write_timer, 5000); return; default: @@ -1237,6 +1338,23 @@ nxt_openssl_conn_test_error(nxt_task_t *task, nxt_conn_t *c, int ret, } +static void +nxt_openssl_conn_io_shutdown_timeout(nxt_task_t *task, void *obj, void *data) +{ + nxt_conn_t *c; + nxt_timer_t *timer; + + timer = obj; + + nxt_debug(task, "openssl conn shutdown timeout"); + + c = nxt_write_timer_conn(timer); + + c->socket.timedout = 1; + nxt_openssl_conn_io_shutdown(task, c, NULL); +} + + static void nxt_cdecl nxt_openssl_conn_error(nxt_task_t *task, nxt_err_t err, const char *fmt, ...) { diff --git a/src/nxt_pcre2.c b/src/nxt_pcre2.c index 22c4d2d4..cb51062c 100644 --- a/src/nxt_pcre2.c +++ b/src/nxt_pcre2.c @@ -144,7 +144,7 @@ nxt_regex_match(nxt_regex_t *re, u_char *subject, size_t length, if (pcre2_get_error_message(ret, errptr, ERR_BUF_SIZE) < 0) { nxt_thread_log_error(NXT_LOG_ERR, "pcre2_match() failed: %d on \"%*s\" " - "using \"%V\"", ret, subject, length, subject, + "using \"%V\"", ret, length, subject, &re->pattern); } else { diff --git a/src/nxt_php_sapi.c b/src/nxt_php_sapi.c index 8fbe7f65..3fb3b0db 100644 --- a/src/nxt_php_sapi.c +++ b/src/nxt_php_sapi.c @@ -163,7 +163,8 @@ static const zend_function_entry nxt_php_ext_functions[] = { }; -zif_handler nxt_php_chdir_handler; +zif_handler nxt_php_chdir_handler; +zend_auto_global *nxt_php_server_ag; static zend_module_entry nxt_php_unit_module = { @@ -211,6 +212,7 @@ ZEND_NAMED_FUNCTION(nxt_php_chdir) PHP_FUNCTION(fastcgi_finish_request) { + zend_auto_global *ag; nxt_php_run_ctx_t *ctx; if (nxt_slow_path(zend_parse_parameters_none() == FAILURE)) { @@ -240,6 +242,16 @@ PHP_FUNCTION(fastcgi_finish_request) php_header(TSRMLS_C); #endif + ag = nxt_php_server_ag; + + if (ag->armed) { +#ifdef NXT_PHP7 + ag->armed = ag->auto_global_callback(ag->name); +#else + ag->armed = ag->auto_global_callback(ag->name, ag->name_len TSRMLS_CC); +#endif + } + nxt_unit_request_done(ctx->req, NXT_UNIT_OK); ctx->req = NULL; @@ -411,6 +423,19 @@ nxt_php_setup(nxt_task_t *task, nxt_process_t *process, nxt_php_set_options(task, value, ZEND_INI_USER); } +#ifdef NXT_PHP7 + nxt_php_server_ag = zend_hash_str_find_ptr(CG(auto_globals), "_SERVER", + nxt_length("_SERVER")); +#else + zend_hash_quick_find(CG(auto_globals), "_SERVER", sizeof("_SERVER"), + zend_hash_func("_SERVER", sizeof("_SERVER")), + (void **) &nxt_php_server_ag); +#endif + if (nxt_slow_path(nxt_php_server_ag == NULL)) { + nxt_alert(task, "failed to find $_SERVER auto global"); + return NXT_ERROR; + } + return NXT_OK; } @@ -1076,10 +1101,20 @@ nxt_php_execute(nxt_php_run_ctx_t *ctx, nxt_unit_request_t *r) nxt_memzero(&file_handle, sizeof(file_handle)); file_handle.type = ZEND_HANDLE_FILENAME; +#if (PHP_VERSION_ID >= 80100) + file_handle.filename = zend_string_init((char *) ctx->script_filename.start, + ctx->script_filename.length, 0); + file_handle.primary_script = 1; +#else file_handle.filename = (char *) ctx->script_filename.start; +#endif php_execute_script(&file_handle TSRMLS_CC); +#if (PHP_VERSION_ID >= 80100) + zend_destroy_file_handle(&file_handle); +#endif + /* Prevention of consuming possible unread request body. */ #if (PHP_VERSION_ID < 50600) read_post = sapi_module.read_post; diff --git a/src/nxt_router.c b/src/nxt_router.c index 8524b358..015ae226 100644 --- a/src/nxt_router.c +++ b/src/nxt_router.c @@ -41,8 +41,11 @@ typedef struct { #if (NXT_TLS) typedef struct { - nxt_str_t name; - nxt_socket_conf_t *conf; + nxt_str_t name; + nxt_socket_conf_t *socket_conf; + nxt_router_temp_conf_t *temp_conf; + nxt_conf_value_t *conf_cmds; + nxt_bool_t last; nxt_queue_link_t link; /* for nxt_socket_conf_t.tls */ } nxt_router_tlssock_t; @@ -117,12 +120,11 @@ static void nxt_router_listen_socket_ready(nxt_task_t *task, static void nxt_router_listen_socket_error(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data); #if (NXT_TLS) -static void nxt_router_tls_rpc_create(nxt_task_t *task, - nxt_router_temp_conf_t *tmcf, nxt_router_tlssock_t *tls, nxt_bool_t last); static void nxt_router_tls_rpc_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data); static nxt_int_t nxt_router_conf_tls_insert(nxt_router_temp_conf_t *tmcf, - nxt_conf_value_t *value, nxt_socket_conf_t *skcf); + nxt_conf_value_t *value, nxt_socket_conf_t *skcf, + nxt_conf_value_t * conf_cmds); #endif static void nxt_router_app_rpc_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_app_t *app); @@ -773,7 +775,7 @@ fail: msg->port_msg.stream, 0, NULL); if (tmcf != NULL) { - nxt_mp_destroy(tmcf->mem_pool); + nxt_mp_release(tmcf->mem_pool); } cleanup: @@ -954,8 +956,10 @@ nxt_router_conf_apply(nxt_task_t *task, void *obj, void *data) tls = nxt_queue_link_data(qlk, nxt_router_tlssock_t, link); - nxt_router_tls_rpc_create(task, tmcf, tls, - nxt_queue_is_empty(&tmcf->tls)); + tls->last = nxt_queue_is_empty(&tmcf->tls); + + nxt_cert_store_get(task, &tls->name, tmcf->mem_pool, + nxt_router_tls_rpc_handler, tls); return; } #endif @@ -1061,7 +1065,7 @@ nxt_router_conf_ready(nxt_task_t *task, nxt_router_temp_conf_t *tmcf) nxt_mp_destroy(rtcf->mem_pool); } - nxt_mp_destroy(tmcf->mem_pool); + nxt_mp_release(tmcf->mem_pool); } @@ -1120,7 +1124,7 @@ nxt_router_conf_error(nxt_task_t *task, nxt_router_temp_conf_t *tmcf) nxt_router_conf_send(task, tmcf, NXT_PORT_MSG_RPC_ERROR); - nxt_mp_destroy(tmcf->mem_pool); + nxt_mp_release(tmcf->mem_pool); } @@ -1337,7 +1341,7 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_router_t *router; nxt_app_joint_t *app_joint; #if (NXT_TLS) - nxt_conf_value_t *certificate; + nxt_conf_value_t *certificate, *conf_cmds; #endif nxt_conf_value_t *conf, *http, *value, *websocket; nxt_conf_value_t *applications, *application; @@ -1358,6 +1362,7 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, static nxt_str_t access_log_path = nxt_string("/access_log"); #if (NXT_TLS) static nxt_str_t certificate_path = nxt_string("/tls/certificate"); + static nxt_str_t conf_commands_path = nxt_string("/tls/conf_commands"); #endif static nxt_str_t static_path = nxt_string("/settings/http/static"); static nxt_str_t websocket_path = nxt_string("/settings/http/websocket"); @@ -1736,6 +1741,8 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, certificate = nxt_conf_get_path(listener, &certificate_path); if (certificate != NULL) { + conf_cmds = nxt_conf_get_path(listener, &conf_commands_path); + if (nxt_conf_type(certificate) == NXT_CONF_ARRAY) { n = nxt_conf_array_elements_count(certificate); @@ -1744,7 +1751,8 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_assert(value != NULL); - ret = nxt_router_conf_tls_insert(tmcf, value, skcf); + ret = nxt_router_conf_tls_insert(tmcf, value, skcf, + conf_cmds); if (nxt_slow_path(ret != NXT_OK)) { goto fail; } @@ -1752,7 +1760,8 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, } else { /* NXT_CONF_STRING */ - ret = nxt_router_conf_tls_insert(tmcf, certificate, skcf); + ret = nxt_router_conf_tls_insert(tmcf, certificate, skcf, + conf_cmds); if (nxt_slow_path(ret != NXT_OK)) { goto fail; } @@ -1846,25 +1855,20 @@ fail: static nxt_int_t nxt_router_conf_tls_insert(nxt_router_temp_conf_t *tmcf, - nxt_conf_value_t *value, nxt_socket_conf_t *skcf) + nxt_conf_value_t *value, nxt_socket_conf_t *skcf, + nxt_conf_value_t *conf_cmds) { - nxt_mp_t *mp; - nxt_str_t str; nxt_router_tlssock_t *tls; - mp = tmcf->router_conf->mem_pool; - - tls = nxt_mp_get(mp, sizeof(nxt_router_tlssock_t)); + tls = nxt_mp_get(tmcf->mem_pool, sizeof(nxt_router_tlssock_t)); if (nxt_slow_path(tls == NULL)) { return NXT_ERROR; } - tls->conf = skcf; - nxt_conf_get_string(value, &str); - - if (nxt_slow_path(nxt_str_dup(mp, &tls->name, &str) == NULL)) { - return NXT_ERROR; - } + tls->socket_conf = skcf; + tls->conf_cmds = conf_cmds; + tls->temp_conf = tmcf; + nxt_conf_get_string(value, &tls->name); nxt_queue_insert_tail(&tmcf->tls, &tls->link); @@ -2144,7 +2148,7 @@ nxt_router_listener_application(nxt_router_conf_t *rtcf, nxt_str_t *name, return NXT_DECLINED; } - action->u.application = app; + action->u.app.application = app; action->handler = nxt_http_application_handler; return NXT_OK; @@ -2428,42 +2432,20 @@ nxt_router_listen_socket_error(nxt_task_t *task, nxt_port_recv_msg_t *msg, #if (NXT_TLS) static void -nxt_router_tls_rpc_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, - nxt_router_tlssock_t *tls, nxt_bool_t last) -{ - nxt_socket_rpc_t *rpc; - - rpc = nxt_mp_alloc(tmcf->mem_pool, sizeof(nxt_socket_rpc_t)); - if (rpc == NULL) { - nxt_router_conf_error(task, tmcf); - return; - } - - rpc->name = &tls->name; - rpc->socket_conf = tls->conf; - rpc->temp_conf = tmcf; - rpc->last = last; - - nxt_cert_store_get(task, &tls->name, tmcf->mem_pool, - nxt_router_tls_rpc_handler, rpc); -} - - -static void nxt_router_tls_rpc_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data) { nxt_mp_t *mp; nxt_int_t ret; nxt_tls_conf_t *tlscf; - nxt_socket_rpc_t *rpc; + nxt_router_tlssock_t *tls; nxt_tls_bundle_conf_t *bundle; nxt_router_temp_conf_t *tmcf; nxt_debug(task, "tls rpc handler"); - rpc = data; - tmcf = rpc->temp_conf; + tls = data; + tmcf = tls->temp_conf; if (msg == NULL || msg->port_msg.type == _NXT_PORT_MSG_RPC_ERROR) { goto fail; @@ -2471,16 +2453,17 @@ nxt_router_tls_rpc_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg, mp = tmcf->router_conf->mem_pool; - if (rpc->socket_conf->tls == NULL){ + if (tls->socket_conf->tls == NULL){ tlscf = nxt_mp_zget(mp, sizeof(nxt_tls_conf_t)); if (nxt_slow_path(tlscf == NULL)) { goto fail; } - rpc->socket_conf->tls = tlscf; + tlscf->no_wait_shutdown = 1; + tls->socket_conf->tls = tlscf; } else { - tlscf = rpc->socket_conf->tls; + tlscf = tls->socket_conf->tls; } bundle = nxt_mp_get(mp, sizeof(nxt_tls_bundle_conf_t)); @@ -2488,12 +2471,16 @@ nxt_router_tls_rpc_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg, goto fail; } - bundle->name = rpc->name; + if (nxt_slow_path(nxt_str_dup(mp, &bundle->name, &tls->name) == NULL)) { + goto fail; + } + bundle->chain_file = msg->fd[0]; bundle->next = tlscf->bundle; tlscf->bundle = bundle; - ret = task->thread->runtime->tls->server_init(task, tlscf, mp, rpc->last); + ret = task->thread->runtime->tls->server_init(task, tlscf, mp, + tls->conf_cmds, tls->last); if (nxt_slow_path(ret != NXT_OK)) { goto fail; } @@ -3223,12 +3210,11 @@ nxt_router_listen_socket_update(nxt_task_t *task, void *obj, void *data) static void nxt_router_listen_socket_delete(nxt_task_t *task, void *obj, void *data) { - nxt_joint_job_t *job; - nxt_socket_conf_t *skcf; - nxt_listen_event_t *lev; - nxt_event_engine_t *engine; + nxt_socket_conf_t *skcf; + nxt_listen_event_t *lev; + nxt_event_engine_t *engine; + nxt_socket_conf_joint_t *joint; - job = obj; skcf = data; engine = task->thread->engine; @@ -3240,15 +3226,13 @@ nxt_router_listen_socket_delete(nxt_task_t *task, void *obj, void *data) nxt_debug(task, "engine %p: listen socket delete: %d", engine, lev->socket.fd); + joint = lev->socket.data; + joint->close_job = obj; + lev->timer.handler = nxt_router_listen_socket_close; lev->timer.work_queue = &engine->fast_work_queue; nxt_timer_add(engine, &lev->timer, 0); - - job->work.next = NULL; - job->work.handler = nxt_router_conf_wait; - - nxt_event_engine_post(job->tmcf->engine, &job->work); } @@ -3273,6 +3257,7 @@ static void nxt_router_listen_socket_close(nxt_task_t *task, void *obj, void *data) { nxt_timer_t *timer; + nxt_joint_job_t *job; nxt_listen_event_t *lev; nxt_socket_conf_joint_t *joint; @@ -3292,6 +3277,12 @@ nxt_router_listen_socket_close(nxt_task_t *task, void *obj, void *data) nxt_router_listen_socket_release(task, joint->socket_conf); + job = joint->close_job; + job->work.next = NULL; + job->work.handler = nxt_router_conf_wait; + + nxt_event_engine_post(job->tmcf->engine, &job->work); + nxt_router_listen_event_release(task, lev, joint); } diff --git a/src/nxt_router.h b/src/nxt_router.h index 5804840f..b1ccdf51 100644 --- a/src/nxt_router.h +++ b/src/nxt_router.h @@ -205,6 +205,8 @@ typedef struct { nxt_event_engine_t *engine; nxt_socket_conf_t *socket_conf; + nxt_joint_job_t *close_job; + nxt_upstream_t **upstreams; /* Modules configuraitons. */ diff --git a/src/nxt_tls.h b/src/nxt_tls.h index c44bfe56..63c49ee4 100644 --- a/src/nxt_tls.h +++ b/src/nxt_tls.h @@ -8,6 +8,9 @@ #define _NXT_TLS_H_INCLUDED_ +#include <nxt_conf.h> + + /* * The SSL/TLS libraries lack vector I/O interface yet add noticeable * overhead to each SSL/TLS record so buffering allows to decrease the @@ -32,6 +35,7 @@ typedef struct { nxt_int_t (*server_init)(nxt_task_t *task, nxt_tls_conf_t *conf, nxt_mp_t *mp, + nxt_conf_value_t *conf_cmds, nxt_bool_t last); void (*server_free)(nxt_task_t *task, nxt_tls_conf_t *conf); @@ -49,7 +53,7 @@ struct nxt_tls_bundle_conf_s { void *ctx; nxt_fd_t chain_file; - nxt_str_t *name; + nxt_str_t name; nxt_tls_bundle_conf_t *next; }; @@ -69,6 +73,8 @@ struct nxt_tls_conf_s { char *ca_certificate; size_t buffer_size; + + uint8_t no_wait_shutdown; /* 1 bit */ }; diff --git a/src/nxt_unix.h b/src/nxt_unix.h index 609f7e95..393f61d9 100644 --- a/src/nxt_unix.h +++ b/src/nxt_unix.h @@ -242,6 +242,10 @@ #include <sys/mount.h> #endif +#if (NXT_HAVE_OPENAT2) +#include <linux/openat2.h> +#endif + #if (NXT_TEST_BUILD) #include <nxt_test_build.h> #endif diff --git a/src/python/nxt_python.c b/src/python/nxt_python.c index d8204937..588a147a 100644 --- a/src/python/nxt_python.c +++ b/src/python/nxt_python.c @@ -24,6 +24,8 @@ typedef struct { static nxt_int_t nxt_python_start(nxt_task_t *task, nxt_process_data_t *data); +static nxt_int_t nxt_python_set_target(nxt_task_t *task, + nxt_python_target_t *target, nxt_conf_value_t *conf); static nxt_int_t nxt_python_set_path(nxt_task_t *task, nxt_conf_value_t *value); static int nxt_python_init_threads(nxt_python_app_conf_t *c); static int nxt_python_ready_handler(nxt_unit_ctx_t *ctx); @@ -49,7 +51,7 @@ NXT_EXPORT nxt_app_module_t nxt_app_module = { }; static PyObject *nxt_py_stderr_flush; -PyObject *nxt_py_application; +nxt_python_targets_t *nxt_py_targets; #if PY_MAJOR_VERSION == 3 static wchar_t *nxt_py_home; @@ -66,18 +68,19 @@ static nxt_int_t nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) { int rc; - char *nxt_py_module; - size_t len; + size_t len, size; + uint32_t next; PyObject *obj, *module; - nxt_str_t proto; - const char *callable; + nxt_str_t proto, probe_proto, name; + nxt_int_t ret, n, i; nxt_unit_ctx_t *unit_ctx; nxt_unit_init_t python_init; + nxt_conf_value_t *cv; + nxt_python_targets_t *targets; nxt_common_app_conf_t *app_conf; nxt_python_app_conf_t *c; #if PY_MAJOR_VERSION == 3 char *path; - size_t size; nxt_int_t pep405; static const char pyvenv[] = "/pyvenv.cfg"; @@ -190,38 +193,42 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) Py_CLEAR(obj); - nxt_py_module = nxt_alloca(c->module.length + 1); - nxt_memcpy(nxt_py_module, c->module.start, c->module.length); - nxt_py_module[c->module.length] = '\0'; + n = (c->targets != NULL ? nxt_conf_object_members_count(c->targets) : 1); - module = PyImport_ImportModule(nxt_py_module); - if (nxt_slow_path(module == NULL)) { - nxt_alert(task, "Python failed to import module \"%s\"", nxt_py_module); - nxt_python_print_exception(); + size = sizeof(nxt_python_targets_t) + n * sizeof(nxt_python_target_t); + + targets = nxt_unit_malloc(NULL, size); + if (nxt_slow_path(targets == NULL)) { + nxt_alert(task, "Could not allocate targets"); goto fail; } - callable = (c->callable != NULL) ? c->callable : "application"; + memset(targets, 0, size); - obj = PyDict_GetItemString(PyModule_GetDict(module), callable); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"%s\" " - "from module \"%s\"", callable, nxt_py_module); - goto fail; - } + targets->count = n; + nxt_py_targets = targets; - if (nxt_slow_path(PyCallable_Check(obj) == 0)) { - nxt_alert(task, "\"%s\" in module \"%s\" " - "is not a callable object", callable, nxt_py_module); - goto fail; - } + if (c->targets != NULL) { + next = 0; - nxt_py_application = obj; - obj = NULL; + for (i = 0; /* void */; i++) { + cv = nxt_conf_next_object_member(c->targets, &name, &next); + if (cv == NULL) { + break; + } - Py_INCREF(nxt_py_application); + ret = nxt_python_set_target(task, &targets->target[i], cv); + if (nxt_slow_path(ret != NXT_OK)) { + goto fail; + } + } - Py_CLEAR(module); + } else { + ret = nxt_python_set_target(task, &targets->target[0], app_conf->self); + if (nxt_slow_path(ret != NXT_OK)) { + goto fail; + } + } nxt_unit_default_init(task, &python_init); @@ -232,7 +239,18 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) proto = c->protocol; if (proto.length == 0) { - proto = nxt_python_asgi_check(nxt_py_application) ? asgi : wsgi; + proto = nxt_python_asgi_check(targets->target[0].application) + ? asgi : wsgi; + + for (i = 1; i < targets->count; i++) { + probe_proto = nxt_python_asgi_check(targets->target[i].application) + ? asgi : wsgi; + if (probe_proto.start != proto.start) { + nxt_alert(task, "A mix of ASGI & WSGI targets is forbidden, " + "specify protocol in config if incorrect"); + goto fail; + } + } } if (nxt_strstr_eq(&proto, &asgi)) { @@ -299,6 +317,81 @@ fail: static nxt_int_t +nxt_python_set_target(nxt_task_t *task, nxt_python_target_t *target, + nxt_conf_value_t *conf) +{ + char *callable, *module_name; + PyObject *module, *obj; + nxt_str_t str; + nxt_conf_value_t *value; + + static nxt_str_t module_str = nxt_string("module"); + static nxt_str_t callable_str = nxt_string("callable"); + + module = obj = NULL; + + value = nxt_conf_get_object_member(conf, &module_str, NULL); + if (nxt_slow_path(value == NULL)) { + goto fail; + } + + nxt_conf_get_string(value, &str); + + module_name = nxt_alloca(str.length + 1); + nxt_memcpy(module_name, str.start, str.length); + module_name[str.length] = '\0'; + + module = PyImport_ImportModule(module_name); + if (nxt_slow_path(module == NULL)) { + nxt_alert(task, "Python failed to import module \"%s\"", module_name); + nxt_python_print_exception(); + goto fail; + } + + value = nxt_conf_get_object_member(conf, &callable_str, NULL); + if (value == NULL) { + callable = nxt_alloca(12); + nxt_memcpy(callable, "application", 12); + + } else { + nxt_conf_get_string(value, &str); + + callable = nxt_alloca(str.length + 1); + nxt_memcpy(callable, str.start, str.length); + callable[str.length] = '\0'; + } + + obj = PyDict_GetItemString(PyModule_GetDict(module), callable); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to get \"%s\" from module \"%s\"", + callable, module); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(obj) == 0)) { + nxt_alert(task, "\"%s\" in module \"%s\" is not a callable object", + callable, module); + goto fail; + } + + target->application = obj; + obj = NULL; + + Py_INCREF(target->application); + Py_CLEAR(module); + + return NXT_OK; + +fail: + + Py_XDECREF(obj); + Py_XDECREF(module); + + return NXT_ERROR; +} + + +static nxt_int_t nxt_python_set_path(nxt_task_t *task, nxt_conf_value_t *value) { int ret; @@ -596,12 +689,21 @@ nxt_python_done_strings(nxt_python_string_t *pstr) static void nxt_python_atexit(void) { + nxt_int_t i; + if (nxt_py_proto.done != NULL) { nxt_py_proto.done(); } Py_XDECREF(nxt_py_stderr_flush); - Py_XDECREF(nxt_py_application); + + if (nxt_py_targets != NULL) { + for (i = 0; i < nxt_py_targets->count; i++) { + Py_XDECREF(nxt_py_targets->target[i].application); + } + + nxt_unit_free(NULL, nxt_py_targets); + } Py_Finalize(); diff --git a/src/python/nxt_python.h b/src/python/nxt_python.h index b581dd46..a5c1d9a6 100644 --- a/src/python/nxt_python.h +++ b/src/python/nxt_python.h @@ -37,13 +37,28 @@ #define NXT_HAVE_ASGI 1 #endif -extern PyObject *nxt_py_application; + +typedef struct { + PyObject *application; + nxt_bool_t asgi_legacy; +} nxt_python_target_t; + + +typedef struct { + nxt_int_t count; + nxt_python_target_t target[0]; +} nxt_python_targets_t; + + +extern nxt_python_targets_t *nxt_py_targets; + typedef struct { nxt_str_t string; PyObject **object_p; } nxt_python_string_t; + typedef struct { int (*ctx_data_alloc)(void **pdata); void (*ctx_data_free)(void *data); diff --git a/src/python/nxt_python_asgi.c b/src/python/nxt_python_asgi.c index a6f94507..1d220678 100644 --- a/src/python/nxt_python_asgi.c +++ b/src/python/nxt_python_asgi.c @@ -43,8 +43,6 @@ static void nxt_py_asgi_shm_ack_handler(nxt_unit_ctx_t *ctx); static PyObject *nxt_py_asgi_port_read(PyObject *self, PyObject *args); static void nxt_python_asgi_done(void); - -int nxt_py_asgi_legacy; static PyObject *nxt_py_port_read; static nxt_unit_port_t *nxt_py_shared_port; @@ -137,6 +135,7 @@ int nxt_python_asgi_init(nxt_unit_init_t *init, nxt_python_proto_t *proto) { PyObject *func; + nxt_int_t i; PyCodeObject *code; nxt_unit_debug(NULL, "asgi_init"); @@ -161,21 +160,23 @@ nxt_python_asgi_init(nxt_unit_init_t *init, nxt_python_proto_t *proto) return NXT_UNIT_ERROR; } - func = nxt_python_asgi_get_func(nxt_py_application); - if (nxt_slow_path(func == NULL)) { - nxt_unit_alert(NULL, "Python cannot find function for callable"); - return NXT_UNIT_ERROR; - } + for (i = 0; i < nxt_py_targets->count; i++) { + func = nxt_python_asgi_get_func(nxt_py_targets->target[i].application); + if (nxt_slow_path(func == NULL)) { + nxt_unit_alert(NULL, "Python cannot find function for callable"); + return NXT_UNIT_ERROR; + } - code = (PyCodeObject *) PyFunction_GET_CODE(func); + code = (PyCodeObject *) PyFunction_GET_CODE(func); - if ((code->co_flags & CO_COROUTINE) == 0) { - nxt_unit_debug(NULL, "asgi: callable is not a coroutine function " - "switching to legacy mode"); - nxt_py_asgi_legacy = 1; - } + if ((code->co_flags & CO_COROUTINE) == 0) { + nxt_unit_debug(NULL, "asgi: callable is not a coroutine function " + "switching to legacy mode"); + nxt_py_targets->target[i].asgi_legacy = 1; + } - Py_DECREF(func); + Py_DECREF(func); + } init->callbacks.request_handler = nxt_py_asgi_request_handler; init->callbacks.data_handler = nxt_py_asgi_http_data_handler; @@ -408,6 +409,7 @@ nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) { PyObject *scope, *res, *task, *receive, *send, *done, *asgi; PyObject *stage2; + nxt_python_target_t *target; nxt_py_asgi_ctx_data_t *ctx_data; if (req->request->websocket_handshake) { @@ -456,17 +458,18 @@ nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) } req->data = asgi; + target = &nxt_py_targets->target[req->request->app_target]; - if (!nxt_py_asgi_legacy) { + if (!target->asgi_legacy) { nxt_unit_req_debug(req, "Python call ASGI 3.0 application"); - res = PyObject_CallFunctionObjArgs(nxt_py_application, + res = PyObject_CallFunctionObjArgs(target->application, scope, receive, send, NULL); } else { nxt_unit_req_debug(req, "Python call legacy application"); - res = PyObject_CallFunctionObjArgs(nxt_py_application, scope, NULL); + res = PyObject_CallFunctionObjArgs(target->application, scope, NULL); if (nxt_slow_path(res == NULL)) { nxt_unit_req_error(req, "Python failed to call legacy app stage1"); diff --git a/src/python/nxt_python_asgi.h b/src/python/nxt_python_asgi.h index 37f2a099..20702065 100644 --- a/src/python/nxt_python_asgi.h +++ b/src/python/nxt_python_asgi.h @@ -33,7 +33,7 @@ typedef struct { PyObject *loop_remove_reader; PyObject *quit_future; PyObject *quit_future_set_result; - PyObject *lifespan; + PyObject **target_lifespans; nxt_unit_port_t *port; } nxt_py_asgi_ctx_data_t; @@ -69,6 +69,4 @@ int nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data); int nxt_py_asgi_lifespan_shutdown(nxt_unit_ctx_t *ctx); -extern int nxt_py_asgi_legacy; - #endif /* _NXT_PYTHON_ASGI_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi_lifespan.c b/src/python/nxt_python_asgi_lifespan.c index 506eaf4d..1fc0e6b7 100644 --- a/src/python/nxt_python_asgi_lifespan.c +++ b/src/python/nxt_python_asgi_lifespan.c @@ -27,7 +27,10 @@ typedef struct { PyObject *receive_future; } nxt_py_asgi_lifespan_t; - +static PyObject *nxt_py_asgi_lifespan_target_startup( + nxt_py_asgi_ctx_data_t *ctx_data, nxt_python_target_t *target); +static int nxt_py_asgi_lifespan_target_shutdown( + nxt_py_asgi_lifespan_t *lifespan); static PyObject *nxt_py_asgi_lifespan_receive(PyObject *self, PyObject *none); static PyObject *nxt_py_asgi_lifespan_send(PyObject *self, PyObject *dict); static PyObject *nxt_py_asgi_lifespan_send_startup( @@ -69,24 +72,60 @@ static PyTypeObject nxt_py_asgi_lifespan_type = { int nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data) { - int rc; + size_t size; + PyObject *lifespan; + PyObject **target_lifespans; + nxt_int_t i; + nxt_python_target_t *target; + + size = nxt_py_targets->count * sizeof(PyObject*); + + target_lifespans = nxt_unit_malloc(NULL, size); + if (nxt_slow_path(target_lifespans == NULL)) { + nxt_unit_alert(NULL, "Failed to allocate lifespan data"); + return NXT_UNIT_ERROR; + } + + memset(target_lifespans, 0, size); + + for (i = 0; i < nxt_py_targets->count; i++) { + target = &nxt_py_targets->target[i]; + + lifespan = nxt_py_asgi_lifespan_target_startup(ctx_data, target); + if (nxt_slow_path(lifespan == NULL)) { + return NXT_UNIT_ERROR; + } + + target_lifespans[i] = lifespan; + } + + ctx_data->target_lifespans = target_lifespans; + + return NXT_UNIT_OK; +} + + +static PyObject * +nxt_py_asgi_lifespan_target_startup(nxt_py_asgi_ctx_data_t *ctx_data, + nxt_python_target_t *target) +{ PyObject *scope, *res, *py_task, *receive, *send, *done; PyObject *stage2; - nxt_py_asgi_lifespan_t *lifespan; + nxt_py_asgi_lifespan_t *lifespan, *ret; if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_lifespan_type) != 0)) { nxt_unit_alert(NULL, "Python failed to initialize the 'asgi_lifespan' type object"); - return NXT_UNIT_ERROR; + return NULL; } lifespan = PyObject_New(nxt_py_asgi_lifespan_t, &nxt_py_asgi_lifespan_type); if (nxt_slow_path(lifespan == NULL)) { nxt_unit_alert(NULL, "Python failed to create lifespan object"); - return NXT_UNIT_ERROR; + return NULL; } - rc = NXT_UNIT_ERROR; + ret = NULL; receive = PyObject_GetAttrString((PyObject *) lifespan, "receive"); if (nxt_slow_path(receive == NULL)) { @@ -130,23 +169,25 @@ nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data) goto release_future; } - if (!nxt_py_asgi_legacy) { + if (!target->asgi_legacy) { nxt_unit_req_debug(NULL, "Python call ASGI 3.0 application"); - res = PyObject_CallFunctionObjArgs(nxt_py_application, + res = PyObject_CallFunctionObjArgs(target->application, scope, receive, send, NULL); } else { nxt_unit_req_debug(NULL, "Python call legacy application"); - res = PyObject_CallFunctionObjArgs(nxt_py_application, scope, NULL); + res = PyObject_CallFunctionObjArgs(target->application, scope, NULL); if (nxt_slow_path(res == NULL)) { nxt_unit_log(NULL, NXT_UNIT_LOG_INFO, "ASGI Lifespan processing exception"); nxt_python_print_exception(); lifespan->disabled = 1; - rc = NXT_UNIT_OK; + + Py_INCREF(lifespan); + ret = lifespan; goto release_scope; } @@ -211,10 +252,9 @@ nxt_py_asgi_lifespan_startup(nxt_py_asgi_ctx_data_t *ctx_data) Py_DECREF(res); if (lifespan->startup_sent == 1 || lifespan->disabled) { - ctx_data->lifespan = (PyObject *) lifespan; - Py_INCREF(ctx_data->lifespan); + Py_INCREF(lifespan); - rc = NXT_UNIT_OK; + ret = lifespan; } release_task: @@ -232,20 +272,41 @@ release_receive: release_lifespan: Py_DECREF(lifespan); - return rc; + return (PyObject *) ret; } int nxt_py_asgi_lifespan_shutdown(nxt_unit_ctx_t *ctx) { - PyObject *msg, *future, *res; + nxt_int_t i, ret; nxt_py_asgi_lifespan_t *lifespan; nxt_py_asgi_ctx_data_t *ctx_data; ctx_data = ctx->data; - lifespan = (nxt_py_asgi_lifespan_t *) ctx_data->lifespan; + for (i = 0; i < nxt_py_targets->count; i++) { + lifespan = (nxt_py_asgi_lifespan_t *)ctx_data->target_lifespans[i]; + + ret = nxt_py_asgi_lifespan_target_shutdown(lifespan); + if (nxt_slow_path(ret != NXT_UNIT_OK)) { + return NXT_UNIT_ERROR; + } + } + + nxt_unit_free(NULL, ctx_data->target_lifespans); + + return NXT_UNIT_OK; +} + + +static int +nxt_py_asgi_lifespan_target_shutdown(nxt_py_asgi_lifespan_t *lifespan) +{ + PyObject *msg, *future, *res; + nxt_py_asgi_ctx_data_t *ctx_data; + + ctx_data = lifespan->ctx_data; if (nxt_slow_path(lifespan == NULL || lifespan->disabled)) { return NXT_UNIT_OK; diff --git a/src/python/nxt_python_wsgi.c b/src/python/nxt_python_wsgi.c index 77c45af5..b80d10fa 100644 --- a/src/python/nxt_python_wsgi.c +++ b/src/python/nxt_python_wsgi.c @@ -302,7 +302,7 @@ nxt_python_request_handler(nxt_unit_request_info_t *req) { int rc; PyObject *environ, *args, *response, *iterator, *item; - PyObject *close, *result; + PyObject *close, *result, *application; nxt_bool_t prepare_environ; nxt_python_ctx_t *pctx; @@ -348,7 +348,8 @@ nxt_python_request_handler(nxt_unit_request_info_t *req) Py_INCREF(pctx->start_resp); PyTuple_SET_ITEM(args, 1, pctx->start_resp); - response = PyObject_CallObject(nxt_py_application, args); + application = nxt_py_targets->target[req->request->app_target].application; + response = PyObject_CallObject(application, args); Py_DECREF(args); diff --git a/src/ruby/nxt_ruby_stream_io.c b/src/ruby/nxt_ruby_stream_io.c index 69bf289e..82ad3908 100644 --- a/src/ruby/nxt_ruby_stream_io.c +++ b/src/ruby/nxt_ruby_stream_io.c @@ -25,7 +25,9 @@ nxt_ruby_stream_io_input_init(void) { VALUE stream_io; - stream_io = rb_define_class("NGINX_Unit_Stream_IO_Read", rb_cData); + stream_io = rb_define_class("NGINX_Unit_Stream_IO_Read", rb_cObject); + + rb_undef_alloc_func(stream_io); rb_gc_register_address(&stream_io); @@ -46,7 +48,9 @@ nxt_ruby_stream_io_error_init(void) { VALUE stream_io; - stream_io = rb_define_class("NGINX_Unit_Stream_IO_Error", rb_cData); + stream_io = rb_define_class("NGINX_Unit_Stream_IO_Error", rb_cObject); + + rb_undef_alloc_func(stream_io); rb_gc_register_address(&stream_io); diff --git a/test/conftest.py b/test/conftest.py index 20ac6e81..5ea4e49d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,7 +6,6 @@ import platform import re import shutil import signal -import socket import stat import subprocess import sys @@ -15,13 +14,16 @@ import time from multiprocessing import Process import pytest + +from unit.check.chroot import check_chroot from unit.check.go import check_go from unit.check.isolation import check_isolation from unit.check.node import check_node -from unit.check.tls import check_openssl from unit.check.regex import check_regex +from unit.check.tls import check_openssl from unit.http import TestHTTP from unit.option import option +from unit.log import Log from unit.utils import public_dir from unit.utils import waitforfiles @@ -57,6 +59,12 @@ def pytest_addoption(parser): help="Default user for non-privileged processes of unitd", ) parser.addoption( + "--fds-threshold", + type=int, + default=0, + help="File descriptors threshold", + ) + parser.addoption( "--restart", default=False, action="store_true", @@ -65,14 +73,25 @@ def pytest_addoption(parser): unit_instance = {} -unit_log_copy = "unit.log.copy" _processes = [] +_fds_check = { + 'main': {'fds': 0, 'skip': False}, + 'router': {'name': 'unit: router', 'pid': -1, 'fds': 0, 'skip': False}, + 'controller': { + 'name': 'unit: controller', + 'pid': -1, + 'fds': 0, + 'skip': False, + }, +} http = TestHTTP() + def pytest_configure(config): option.config = config.option option.detailed = config.option.detailed + option.fds_threshold = config.option.fds_threshold option.print_log = config.option.print_log option.save_log = config.option.save_log option.unsafe = config.option.unsafe @@ -98,9 +117,11 @@ def pytest_configure(config): def pytest_generate_tests(metafunc): cls = metafunc.cls - if (not hasattr(cls, 'application_type') - or cls.application_type == None - or cls.application_type == 'external'): + if ( + not hasattr(cls, 'application_type') + or cls.application_type == None + or cls.application_type == 'external' + ): return type = cls.application_type @@ -145,12 +166,11 @@ def pytest_sessionstart(session): option.available = {'modules': {}, 'features': {}} unit = unit_run() - option.temp_dir = unit['temp_dir'] # read unit.log for i in range(50): - with open(unit['temp_dir'] + '/unit.log', 'r') as f: + with open(Log.get_path(), 'r') as f: log = f.read() m = re.search('controller started', log) @@ -185,6 +205,7 @@ def pytest_sessionstart(session): k: v for k, v in option.available['modules'].items() if v is not None } + check_chroot() check_isolation() _clear_conf(unit['temp_dir'] + '/control.unit.sock') @@ -196,8 +217,6 @@ def pytest_sessionstart(session): if option.restart: shutil.rmtree(unit_instance['temp_dir']) - elif option.save_log: - open(unit_instance['temp_dir'] + '/' + unit_log_copy, 'w').close() @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -248,7 +267,6 @@ def check_prerequisites(request): @pytest.fixture(autouse=True) def run(request): unit = unit_run() - option.temp_dir = unit['temp_dir'] option.skip_alerts = [ r'read signalfd\(4\) failed', @@ -257,6 +275,10 @@ def run(request): ] option.skip_sanitizer = False + _fds_check['main']['skip'] = False + _fds_check['router']['skip'] = False + _fds_check['controller']['skip'] = False + yield # stop unit @@ -266,44 +288,83 @@ def run(request): # prepare log - with open( - unit_instance['log'], 'r', encoding='utf-8', errors='ignore' - ) as f: + with Log.open(encoding='utf-8') as f: log = f.read() - - if not option.restart and option.save_log: - with open(unit_instance['temp_dir'] + '/' + unit_log_copy, 'a') as f: - f.write(log) - - # remove unit.log + Log.set_pos(f.tell()) if not option.save_log and option.restart: shutil.rmtree(unit['temp_dir']) + Log.set_pos(0) # clean temp_dir before the next test if not option.restart: _clear_conf(unit['temp_dir'] + '/control.unit.sock', log) - open(unit['log'], 'w').close() - for item in os.listdir(unit['temp_dir']): if item not in [ 'control.unit.sock', 'state', 'unit.pid', 'unit.log', - unit_log_copy, ]: path = os.path.join(unit['temp_dir'], item) public_dir(path) - if os.path.isfile(path) or stat.S_ISSOCK(os.stat(path).st_mode): + if os.path.isfile(path) or stat.S_ISSOCK( + os.stat(path).st_mode + ): os.remove(path) else: shutil.rmtree(path) + # check descriptors (wait for some time before check) + + def waitforfds(diff): + for i in range(600): + fds_diff = diff() + + if fds_diff <= option.fds_threshold: + break + + time.sleep(0.1) + + return fds_diff + + ps = _fds_check['main'] + if not ps['skip']: + fds_diff = waitforfds( + lambda: _count_fds(unit_instance['pid']) - ps['fds'] + ) + ps['fds'] += fds_diff + + assert ( + fds_diff <= option.fds_threshold + ), 'descriptors leak main process' + + else: + ps['fds'] = _count_fds(unit_instance['pid']) + + for name in ['controller', 'router']: + ps = _fds_check[name] + ps_pid = ps['pid'] + ps['pid'] = pid_by_name(ps['name']) + + if not ps['skip']: + fds_diff = waitforfds(lambda: _count_fds(ps['pid']) - ps['fds']) + ps['fds'] += fds_diff + + if not option.restart: + assert ps['pid'] == ps_pid, 'same pid %s' % name + + assert fds_diff <= option.fds_threshold, ( + 'descriptors leak %s' % name + ) + + else: + ps['fds'] = _count_fds(ps['pid']) + # print unit.log in case of error if hasattr(request.node, 'rep_call') and request.node.rep_call.failed: @@ -319,6 +380,7 @@ def run(request): _check_alerts(log=log) + def unit_run(): global unit_instance @@ -367,10 +429,27 @@ def unit_run(): exit('Could not start unit') unit_instance['temp_dir'] = temp_dir - unit_instance['log'] = temp_dir + '/unit.log' unit_instance['control_sock'] = temp_dir + '/control.unit.sock' unit_instance['unitd'] = unitd + option.temp_dir = temp_dir + Log.temp_dir = temp_dir + + with open(temp_dir + '/unit.pid', 'r') as f: + unit_instance['pid'] = f.read().rstrip() + + _clear_conf(unit_instance['temp_dir'] + '/control.unit.sock') + + _fds_check['main']['fds'] = _count_fds(unit_instance['pid']) + + router = _fds_check['router'] + router['pid'] = pid_by_name(router['name']) + router['fds'] = _count_fds(router['pid']) + + controller = _fds_check['controller'] + controller['pid'] = pid_by_name(controller['name']) + controller['fds'] = _count_fds(controller['pid']) + return unit_instance @@ -402,13 +481,9 @@ def unit_stop(): return 'Could not terminate unit' - -def _check_alerts(path=None, log=None): - if path is None: - path = unit_instance['log'] - +def _check_alerts(log=None): if log is None: - with open(path, 'r', encoding='utf-8', errors='ignore') as f: + with Log.open(encoding='utf-8') as f: log = f.read() found = False @@ -440,7 +515,7 @@ def _check_alerts(path=None, log=None): def _print_log(data=None): - path = unit_instance['log'] + path = Log.get_path() print('Path to unit.log:\n' + path + '\n') @@ -474,24 +549,47 @@ def _clear_conf(sock, log=None): return try: - certs = json.loads(http.get( - url='/certificates', - sock_type='unix', - addr=sock, - )['body']).keys() + certs = json.loads( + http.get(url='/certificates', sock_type='unix', addr=sock,)['body'] + ).keys() except json.JSONDecodeError: pytest.fail('Can\'t parse certificates list.') for cert in certs: resp = http.delete( - url='/certificates/' + cert, - sock_type='unix', - addr=sock, + url='/certificates/' + cert, sock_type='unix', addr=sock, )['body'] check_success(resp) + +def _count_fds(pid): + procfile = '/proc/%s/fd' % pid + if os.path.isdir(procfile): + return len(os.listdir(procfile)) + + try: + out = subprocess.check_output( + ['procstat', '-f', pid], stderr=subprocess.STDOUT, + ).decode() + return len(out.splitlines()) + + except (FileNotFoundError, TypeError, subprocess.CalledProcessError): + pass + + try: + out = subprocess.check_output( + ['lsof', '-n', '-p', pid], stderr=subprocess.STDOUT, + ).decode() + return len(out.splitlines()) + + except (FileNotFoundError, TypeError, subprocess.CalledProcessError): + pass + + return 0 + + def run_process(target, *args): global _processes @@ -500,6 +598,7 @@ def run_process(target, *args): _processes.append(process) + def stop_processes(): if not _processes: return @@ -517,6 +616,18 @@ def stop_processes(): return 'Fail to stop process(es)' +def pid_by_name(name): + output = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode() + m = re.search( + r'\s*(\d+)\s*' + str(unit_instance['pid']) + r'.*' + name, output + ) + return None if m is None else m.group(1) + + +def find_proc(name, ps_output): + return re.findall(str(unit_instance['pid']) + r'.*' + name, ps_output) + + @pytest.fixture() def skip_alert(): def _skip(*alerts): @@ -525,27 +636,47 @@ def skip_alert(): return _skip +@pytest.fixture() +def skip_fds_check(): + def _skip(main=False, router=False, controller=False): + _fds_check['main']['skip'] = main + _fds_check['router']['skip'] = router + _fds_check['controller']['skip'] = controller + + return _skip + + @pytest.fixture def temp_dir(request): return unit_instance['temp_dir'] + @pytest.fixture def is_unsafe(request): return request.config.getoption("--unsafe") + @pytest.fixture def is_su(request): return os.geteuid() == 0 + @pytest.fixture def unit_pid(request): return unit_instance['process'].pid + def pytest_sessionfinish(session): if not option.restart and option.save_log: - print('Path to unit.log:\n' + unit_instance['log'] + '\n') + print('Path to unit.log:\n' + Log.get_path() + '\n') option.restart = True unit_stop() + + public_dir(option.cache_dir) shutil.rmtree(option.cache_dir) + + if not option.save_log and os.path.isdir(option.temp_dir): + public_dir(option.temp_dir) + shutil.rmtree(option.temp_dir) diff --git a/test/node/404/app.js b/test/node/404/app.js index 587c432d..ba15c104 100755..100644 --- a/test/node/404/app.js +++ b/test/node/404/app.js @@ -1,7 +1,6 @@ -#!/usr/bin/env node var fs = require('fs'); -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(404, {}).end(fs.readFileSync('404.html')); }).listen(7080); diff --git a/test/node/basic/app.js b/test/node/basic/app.js index 7820c474..9092022c 100755..100644 --- a/test/node/basic/app.js +++ b/test/node/basic/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'}) .end('Hello World\n'); }).listen(7080); diff --git a/test/node/double_end/app.js b/test/node/double_end/app.js index 63912097..653e33b1 100755..100644 --- a/test/node/double_end/app.js +++ b/test/node/double_end/app.js @@ -1,5 +1,4 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.end().end(); }).listen(7080); diff --git a/test/node/get_header_names/app.js b/test/node/get_header_names/app.js index 4cbccc16..a938b762 100755..100644 --- a/test/node/get_header_names/app.js +++ b/test/node/get_header_names/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('DATE', ['date1', 'date2']); res.setHeader('X-Header', 'blah'); res.setHeader('X-Names', res.getHeaderNames()); diff --git a/test/node/get_header_type/app.js b/test/node/get_header_type/app.js index b606f142..6e45b71f 100755..100644 --- a/test/node/get_header_type/app.js +++ b/test/node/get_header_type/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('X-Number', 100); res.setHeader('X-Type', typeof(res.getHeader('X-Number'))); res.end(); diff --git a/test/node/get_variables/app.js b/test/node/get_variables/app.js index 5c1faf41..cded43d2 100755..100644 --- a/test/node/get_variables/app.js +++ b/test/node/get_variables/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { let query = require('url').parse(req.url, true).query; res.setHeader('X-Var-1', query.var1); res.setHeader('X-Var-2', query.var2); diff --git a/test/node/has_header/app.js b/test/node/has_header/app.js index eff7f4ff..04b13916 100755..100644 --- a/test/node/has_header/app.js +++ b/test/node/has_header/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('X-Has-Header', res.hasHeader(req.headers['x-header']) + ''); res.end(); }).listen(7080); diff --git a/test/node/header_name_case/app.js b/test/node/header_name_case/app.js index 490bd4d5..af157547 100755..100644 --- a/test/node/header_name_case/app.js +++ b/test/node/header_name_case/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('X-Header', '1'); res.setHeader('X-header', '2'); res.setHeader('X-HEADER', '3'); diff --git a/test/node/header_name_valid/app.js b/test/node/header_name_valid/app.js index 425f026f..c0c36098 100755..100644 --- a/test/node/header_name_valid/app.js +++ b/test/node/header_name_valid/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(200, {}); res.setHeader('@$', 'test'); res.end(); diff --git a/test/node/header_value_object/app.js b/test/node/header_value_object/app.js index ff4e2bb0..bacdc7d5 100755..100644 --- a/test/node/header_value_object/app.js +++ b/test/node/header_value_object/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('X-Header', {}); res.end(); }).listen(7080); diff --git a/test/node/loader/es_modules_http/app.mjs b/test/node/loader/es_modules_http/app.mjs new file mode 100644 index 00000000..c7bcfe49 --- /dev/null +++ b/test/node/loader/es_modules_http/app.mjs @@ -0,0 +1,6 @@ +import http from "http" + +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'}) + .end('Hello World\n'); +}).listen(7080); diff --git a/test/node/loader/es_modules_http_indirect/app.js b/test/node/loader/es_modules_http_indirect/app.js new file mode 100644 index 00000000..535befba --- /dev/null +++ b/test/node/loader/es_modules_http_indirect/app.js @@ -0,0 +1 @@ +import("./module.mjs") diff --git a/test/node/loader/es_modules_http_indirect/module.mjs b/test/node/loader/es_modules_http_indirect/module.mjs new file mode 100644 index 00000000..c7bcfe49 --- /dev/null +++ b/test/node/loader/es_modules_http_indirect/module.mjs @@ -0,0 +1,6 @@ +import http from "http" + +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'}) + .end('Hello World\n'); +}).listen(7080); diff --git a/test/node/loader/es_modules_websocket/app.mjs b/test/node/loader/es_modules_websocket/app.mjs new file mode 100644 index 00000000..a71ffa9d --- /dev/null +++ b/test/node/loader/es_modules_websocket/app.mjs @@ -0,0 +1,30 @@ +import http from "http" +import websocket from "websocket" + +let server = http.createServer(function() {}); +let webSocketServer = websocket.server; + +server.listen(7080, function() {}); + +var wsServer = new webSocketServer({ + maxReceivedMessageSize: 0x1000000000, + maxReceivedFrameSize: 0x1000000000, + fragmentOutgoingMessages: false, + fragmentationThreshold: 0x1000000000, + httpServer: server, +}); + +wsServer.on('request', function(request) { + var connection = request.accept(null); + + connection.on('message', function(message) { + if (message.type === 'utf8') { + connection.send(message.utf8Data); + } else if (message.type === 'binary') { + connection.send(message.binaryData); + } + + }); + + connection.on('close', function(r) {}); +}); diff --git a/test/node/loader/es_modules_websocket_indirect/app.js b/test/node/loader/es_modules_websocket_indirect/app.js new file mode 100644 index 00000000..535befba --- /dev/null +++ b/test/node/loader/es_modules_websocket_indirect/app.js @@ -0,0 +1 @@ +import("./module.mjs") diff --git a/test/node/loader/es_modules_websocket_indirect/module.mjs b/test/node/loader/es_modules_websocket_indirect/module.mjs new file mode 100644 index 00000000..a71ffa9d --- /dev/null +++ b/test/node/loader/es_modules_websocket_indirect/module.mjs @@ -0,0 +1,30 @@ +import http from "http" +import websocket from "websocket" + +let server = http.createServer(function() {}); +let webSocketServer = websocket.server; + +server.listen(7080, function() {}); + +var wsServer = new webSocketServer({ + maxReceivedMessageSize: 0x1000000000, + maxReceivedFrameSize: 0x1000000000, + fragmentOutgoingMessages: false, + fragmentationThreshold: 0x1000000000, + httpServer: server, +}); + +wsServer.on('request', function(request) { + var connection = request.accept(null); + + connection.on('message', function(message) { + if (message.type === 'utf8') { + connection.send(message.utf8Data); + } else if (message.type === 'binary') { + connection.send(message.binaryData); + } + + }); + + connection.on('close', function(r) {}); +}); diff --git a/test/node/loader/transitive_dependency/app.js b/test/node/loader/transitive_dependency/app.js new file mode 100644 index 00000000..aaca5216 --- /dev/null +++ b/test/node/loader/transitive_dependency/app.js @@ -0,0 +1 @@ +require("./transitive_http") diff --git a/test/node/loader/transitive_dependency/transitive_http.js b/test/node/loader/transitive_dependency/transitive_http.js new file mode 100644 index 00000000..f1eb98e5 --- /dev/null +++ b/test/node/loader/transitive_dependency/transitive_http.js @@ -0,0 +1,8 @@ +const http = require("http"); + +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'}) + .end('Hello World\n'); +}).listen(7080); + +module.exports = http; diff --git a/test/node/loader/unit_http/app.js b/test/node/loader/unit_http/app.js new file mode 100644 index 00000000..9172e44f --- /dev/null +++ b/test/node/loader/unit_http/app.js @@ -0,0 +1,4 @@ +require("unit-http").createServer(function (req, res) { + res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'}) + .end('Hello World\n'); +}).listen(7080); diff --git a/test/node/mirror/app.js b/test/node/mirror/app.js index 1488917e..bdefe1cd 100755..100644 --- a/test/node/mirror/app.js +++ b/test/node/mirror/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); diff --git a/test/node/post_variables/app.js b/test/node/post_variables/app.js index 928a38cf..12b867cb 100755..100644 --- a/test/node/post_variables/app.js +++ b/test/node/post_variables/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); diff --git a/test/node/promise_end/app.js b/test/node/promise_end/app.js index ed22464c..373c3bc6 100755..100644 --- a/test/node/promise_end/app.js +++ b/test/node/promise_end/app.js @@ -1,8 +1,7 @@ -#!/usr/bin/env node var fs = require('fs'); -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.write('blah'); Promise.resolve().then(() => { diff --git a/test/node/promise_handler/app.js b/test/node/promise_handler/app.js index 51c3666b..32d7d7b9 100755..100644 --- a/test/node/promise_handler/app.js +++ b/test/node/promise_handler/app.js @@ -1,8 +1,7 @@ -#!/usr/bin/env node var fs = require('fs'); -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.end(); if (req.headers['x-write-call']) { diff --git a/test/node/remove_header/app.js b/test/node/remove_header/app.js index cd7b80c3..2a591235 100755..100644 --- a/test/node/remove_header/app.js +++ b/test/node/remove_header/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('X-Header', 'test'); res.setHeader('Was-Header', res.hasHeader('X-Header').toString()); diff --git a/test/node/set_header_array/app.js b/test/node/set_header_array/app.js index faac45c7..965330e2 100755..100644 --- a/test/node/set_header_array/app.js +++ b/test/node/set_header_array/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('Set-Cookie', ['tc=one,two,three', 'tc=four,five,six']); res.end(); }).listen(7080); diff --git a/test/node/status_message/app.js b/test/node/status_message/app.js index e8a798dd..ba51d35b 100755..100644 --- a/test/node/status_message/app.js +++ b/test/node/status_message/app.js @@ -1,5 +1,4 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(200, 'blah', {'Content-Type': 'text/plain'}).end(); }).listen(7080); diff --git a/test/node/update_header/app.js b/test/node/update_header/app.js index 0c5cd237..905ac294 100755..100644 --- a/test/node/update_header/app.js +++ b/test/node/update_header/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.setHeader('X-Header', 'test'); res.setHeader('X-Header', 'new'); res.end(); diff --git a/test/node/variables/app.js b/test/node/variables/app.js index d8cdc20c..a569dddd 100755..100644 --- a/test/node/variables/app.js +++ b/test/node/variables/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); diff --git a/test/node/websockets/mirror/app.js b/test/node/websockets/mirror/app.js index 23746465..0443adb2 100755..100644 --- a/test/node/websockets/mirror/app.js +++ b/test/node/websockets/mirror/app.js @@ -1,9 +1,6 @@ -#!/usr/bin/env node -server = require('unit-http').createServer(function() {}); -webSocketServer = require('unit-http/websocket').server; -//server = require('http').createServer(function() {}); -//webSocketServer = require('websocket').server; +server = require('http').createServer(function() {}); +webSocketServer = require('websocket').server; server.listen(7080, function() {}); diff --git a/test/node/websockets/mirror_fragmentation/app.js b/test/node/websockets/mirror_fragmentation/app.js index 7024252a..ea580ac2 100755..100644 --- a/test/node/websockets/mirror_fragmentation/app.js +++ b/test/node/websockets/mirror_fragmentation/app.js @@ -1,9 +1,6 @@ -#!/usr/bin/env node -server = require('unit-http').createServer(function() {}); -webSocketServer = require('unit-http/websocket').server; -//server = require('http').createServer(function() {}); -//webSocketServer = require('websocket').server; +server = require('http').createServer(function() {}); +webSocketServer = require('websocket').server; server.listen(7080, function() {}); diff --git a/test/node/write_before_write_head/app.js b/test/node/write_before_write_head/app.js index 724b0efb..2293111a 100755..100644 --- a/test/node/write_before_write_head/app.js +++ b/test/node/write_before_write_head/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.write('blah'); res.writeHead(200, {'Content-Type': 'text/plain'}).end(); }).listen(7080); diff --git a/test/node/write_buffer/app.js b/test/node/write_buffer/app.js index a7623523..506e8613 100755..100644 --- a/test/node/write_buffer/app.js +++ b/test/node/write_buffer/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}) .end(new Buffer([0x62, 0x75, 0x66, 0x66, 0x65, 0x72])); }).listen(7080); diff --git a/test/node/write_callback/app.js b/test/node/write_callback/app.js index 3a9e51e8..71eb4116 100755..100644 --- a/test/node/write_callback/app.js +++ b/test/node/write_callback/app.js @@ -1,8 +1,7 @@ -#!/usr/bin/env node var fs = require('fs'); -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); var a = 'world'; res.write('hello', 'utf8', function() { diff --git a/test/node/write_multiple/app.js b/test/node/write_multiple/app.js index 3cbb3b86..e9c51ae0 100755..100644 --- a/test/node/write_multiple/app.js +++ b/test/node/write_multiple/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain', 'Content-Length': 14}); res.write('write'); res.write('write2'); diff --git a/test/node/write_return/app.js b/test/node/write_return/app.js index 82dfbc6e..345b6c4b 100755..100644 --- a/test/node/write_return/app.js +++ b/test/node/write_return/app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node -require('unit-http').createServer(function (req, res) { +require('http').createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}) .end(res.write('body').toString()); }).listen(7080); diff --git a/test/php/fastcgi_finish_request/index.php b/test/php/fastcgi_finish_request/index.php index a6211303..fbfae71b 100644 --- a/test/php/fastcgi_finish_request/index.php +++ b/test/php/fastcgi_finish_request/index.php @@ -8,4 +8,6 @@ if (!fastcgi_finish_request()) { } echo "4567"; + +include 'server.php'; ?> diff --git a/test/php/fastcgi_finish_request/server.php b/test/php/fastcgi_finish_request/server.php new file mode 100644 index 00000000..af71c5f2 --- /dev/null +++ b/test/php/fastcgi_finish_request/server.php @@ -0,0 +1,4 @@ +<?php +echo $_SERVER['REQUEST_METHOD']; +echo $_SERVER['HTTP_HOST']; +?> diff --git a/test/python/atexit/wsgi.py b/test/python/atexit/wsgi.py index a5a9918d..b64b8477 100644 --- a/test/python/atexit/wsgi.py +++ b/test/python/atexit/wsgi.py @@ -1,5 +1,6 @@ import atexit + def application(environ, start_response): def at_exit(): environ['wsgi.errors'].write('At exit called.\n') diff --git a/test/python/body_generate/wsgi.py b/test/python/body_generate/wsgi.py new file mode 100644 index 00000000..73462be6 --- /dev/null +++ b/test/python/body_generate/wsgi.py @@ -0,0 +1,6 @@ +def application(env, start_response): + length = env.get('HTTP_X_LENGTH', '10') + bytes = b'X' * int(length) + + start_response('200', [('Content-Length', length)]) + return [bytes] diff --git a/test/python/body_io/wsgi.py b/test/python/body_io/wsgi.py index 14303b5f..58ce76a4 100644 --- a/test/python/body_io/wsgi.py +++ b/test/python/body_io/wsgi.py @@ -1,5 +1,6 @@ import io + def application(env, start_response): start_response('200', [('Content-Length', '10')]) f = io.BytesIO(b'0123456789') diff --git a/test/python/callable/wsgi.py b/test/python/callable/wsgi.py index 365f82fa..374ecb28 100644 --- a/test/python/callable/wsgi.py +++ b/test/python/callable/wsgi.py @@ -2,6 +2,7 @@ def application(env, start_response): start_response('204', [('Content-Length', '0')]) return [] + def app(env, start_response): start_response('200', [('Content-Length', '0')]) return [] diff --git a/test/python/ctx_iter_atexit/wsgi.py b/test/python/ctx_iter_atexit/wsgi.py index d0b33daa..b2f12c35 100644 --- a/test/python/ctx_iter_atexit/wsgi.py +++ b/test/python/ctx_iter_atexit/wsgi.py @@ -1,5 +1,6 @@ import atexit + class application: def __init__(self, environ, start_response): self.environ = environ @@ -11,13 +12,14 @@ class application: content_length = int(self.environ.get('CONTENT_LENGTH', 0)) body = bytes(self.environ['wsgi.input'].read(content_length)) - self.start('200', [ - ('Content-Type', self.environ.get('CONTENT_TYPE')), - ('Content-Length', str(len(body))) - ]) + self.start( + '200', + [ + ('Content-Type', self.environ.get('CONTENT_TYPE')), + ('Content-Length', str(len(body))), + ], + ) yield body def _atexit(self): - self.start('200', [ - ('Content-Length', '0') - ]) + self.start('200', [('Content-Length', '0')]) diff --git a/test/python/custom_header/wsgi.py b/test/python/custom_header/wsgi.py index 44f145d1..44fc2af5 100644 --- a/test/python/custom_header/wsgi.py +++ b/test/python/custom_header/wsgi.py @@ -1,7 +1,10 @@ def application(environ, start_response): - start_response('200', [ - ('Content-Length', '0'), - ('Custom-Header', environ.get('HTTP_CUSTOM_HEADER')) - ]) + start_response( + '200', + [ + ('Content-Length', '0'), + ('Custom-Header', environ.get('HTTP_CUSTOM_HEADER')), + ], + ) return [] diff --git a/test/python/delayed/asgi.py b/test/python/delayed/asgi.py index d5cad929..1cb15a92 100644 --- a/test/python/delayed/asgi.py +++ b/test/python/delayed/asgi.py @@ -1,5 +1,6 @@ import asyncio + async def application(scope, receive, send): assert scope['type'] == 'http' @@ -28,13 +29,13 @@ async def application(scope, receive, send): loop.call_later(n, future.set_result, None) await future - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', str(len(body)).encode()), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', str(len(body)).encode()),], + } + ) if not body: await sleep(delay) @@ -42,10 +43,12 @@ async def application(scope, receive, send): step = int(len(body) / parts) for i in range(0, len(body), step): - await send({ - 'type': 'http.response.body', - 'body': body[i : i + step], - 'more_body': True, - }) + await send( + { + 'type': 'http.response.body', + 'body': body[i : i + step], + 'more_body': True, + } + ) await sleep(delay) diff --git a/test/python/empty/asgi.py b/test/python/empty/asgi.py index 58b7c1f2..14a40629 100644 --- a/test/python/empty/asgi.py +++ b/test/python/empty/asgi.py @@ -1,10 +1,10 @@ async def application(scope, receive, send): assert scope['type'] == 'http' - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', b'0'), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', b'0')], + } + ) diff --git a/test/python/environment/wsgi.py b/test/python/environment/wsgi.py index fa3a1d2b..d1564f29 100644 --- a/test/python/environment/wsgi.py +++ b/test/python/environment/wsgi.py @@ -1,5 +1,6 @@ import os + def application(env, start_response): body = '' vars = env.get('HTTP_X_VARIABLES').split(',') diff --git a/test/python/header_fields/wsgi.py b/test/python/header_fields/wsgi.py index bd1ba0e2..41144528 100644 --- a/test/python/header_fields/wsgi.py +++ b/test/python/header_fields/wsgi.py @@ -2,8 +2,7 @@ def application(environ, start_response): h = (k for k, v in environ.items() if k.startswith('HTTP_')) - start_response('200', [ - ('Content-Length', '0'), - ('All-Headers', ','.join(h)) - ]) + start_response( + '200', [('Content-Length', '0'), ('All-Headers', ','.join(h))] + ) return [] diff --git a/test/python/host/wsgi.py b/test/python/host/wsgi.py index db7de306..0a08bc36 100644 --- a/test/python/host/wsgi.py +++ b/test/python/host/wsgi.py @@ -1,7 +1,10 @@ def application(env, start_response): - start_response('200', [ - ('Content-Length', '0'), - ('X-Server-Name', env.get('SERVER_NAME')), - ('X-Http-Host', str(env.get('HTTP_HOST'))) - ]) + start_response( + '200', + [ + ('Content-Length', '0'), + ('X-Server-Name', env.get('SERVER_NAME')), + ('X-Http-Host', str(env.get('HTTP_HOST'))), + ], + ) return [] diff --git a/test/python/iter_exception/wsgi.py b/test/python/iter_exception/wsgi.py index 66a09af7..2779a845 100644 --- a/test/python/iter_exception/wsgi.py +++ b/test/python/iter_exception/wsgi.py @@ -8,7 +8,9 @@ class application: def __iter__(self): self.__i = 0 self._skip_level = int(self.environ.get('HTTP_X_SKIP', 0)) - self._not_skip_close = int(self.environ.get('HTTP_X_NOT_SKIP_CLOSE', 0)) + self._not_skip_close = int( + self.environ.get('HTTP_X_NOT_SKIP_CLOSE', 0) + ) self._is_chunked = self.environ.get('HTTP_X_CHUNKED') headers = [(('Content-Length', '10'))] diff --git a/test/python/legacy/asgi.py b/test/python/legacy/asgi.py index f065d026..1d45cc4f 100644 --- a/test/python/legacy/asgi.py +++ b/test/python/legacy/asgi.py @@ -3,11 +3,12 @@ def application(scope): return app_http + async def app_http(receive, send): - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', b'0'), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', b'0'),], + } + ) diff --git a/test/python/legacy_force/asgi.py b/test/python/legacy_force/asgi.py index 2e5859f2..ad2785f2 100644 --- a/test/python/legacy_force/asgi.py +++ b/test/python/legacy_force/asgi.py @@ -7,11 +7,12 @@ def application(scope, receive=None, send=None): else: return app_http(receive, send) + async def app_http(receive, send): - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', b'0'), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', b'0'),], + } + ) diff --git a/test/python/lifespan/empty/asgi.py b/test/python/lifespan/empty/asgi.py index ea43af13..8ceecc2f 100644 --- a/test/python/lifespan/empty/asgi.py +++ b/test/python/lifespan/empty/asgi.py @@ -1,19 +1,19 @@ import os -async def application(scope, receive, send): +async def handler(prefix, scope, receive, send): if scope['type'] == 'lifespan': - with open('version', 'w+') as f: + with open(prefix + 'version', 'w+') as f: f.write( scope['asgi']['version'] + ' ' + scope['asgi']['spec_version'] ) while True: message = await receive() if message['type'] == 'lifespan.startup': - os.remove('startup') + os.remove(prefix + 'startup') await send({'type': 'lifespan.startup.complete'}) elif message['type'] == 'lifespan.shutdown': - os.remove('shutdown') + os.remove(prefix + 'shutdown') await send({'type': 'lifespan.shutdown.complete'}) return @@ -25,3 +25,11 @@ async def application(scope, receive, send): 'headers': [(b'content-length', b'0'),], } ) + + +async def application(scope, receive, send): + return await handler('', scope, receive, send) + + +async def application2(scope, receive, send): + return await handler('app2_', scope, receive, send) diff --git a/test/python/mirror/asgi.py b/test/python/mirror/asgi.py index 7088e893..18a66a4c 100644 --- a/test/python/mirror/asgi.py +++ b/test/python/mirror/asgi.py @@ -8,15 +8,12 @@ async def application(scope, receive, send): if not m.get('more_body', False): break - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', str(len(body)).encode()), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', str(len(body)).encode())], + } + ) - await send({ - 'type': 'http.response.body', - 'body': body, - }) + await send({'type': 'http.response.body', 'body': body}) diff --git a/test/python/mirror/wsgi.py b/test/python/mirror/wsgi.py index eb1fb922..3cd95437 100644 --- a/test/python/mirror/wsgi.py +++ b/test/python/mirror/wsgi.py @@ -3,7 +3,5 @@ def application(environ, start_response): content_length = int(environ.get('CONTENT_LENGTH', 0)) body = bytes(environ['wsgi.input'].read(content_length)) - start_response('200', [ - ('Content-Length', str(len(body))) - ]) + start_response('200', [('Content-Length', str(len(body)))]) return [body] diff --git a/test/python/path/wsgi.py b/test/python/path/wsgi.py index 2807f6ef..da7e1ff1 100644 --- a/test/python/path/wsgi.py +++ b/test/python/path/wsgi.py @@ -1,6 +1,7 @@ import os import sys + def application(environ, start_response): body = os.pathsep.join(sys.path).encode() diff --git a/test/python/query_string/asgi.py b/test/python/query_string/asgi.py index 28f4d107..5b659f9c 100644 --- a/test/python/query_string/asgi.py +++ b/test/python/query_string/asgi.py @@ -1,11 +1,13 @@ async def application(scope, receive, send): assert scope['type'] == 'http' - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', b'0'), - (b'query-string', scope['query_string']), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'query-string', scope['query_string']), + ], + } + ) diff --git a/test/python/query_string/wsgi.py b/test/python/query_string/wsgi.py index 90f1c7ec..54a67b03 100644 --- a/test/python/query_string/wsgi.py +++ b/test/python/query_string/wsgi.py @@ -1,7 +1,10 @@ def application(environ, start_response): - start_response('200', [ - ('Content-Length', '0'), - ('Query-String', environ.get('QUERY_STRING')) - ]) + start_response( + '200', + [ + ('Content-Length', '0'), + ('Query-String', environ.get('QUERY_STRING')), + ], + ) return [] diff --git a/test/python/server_port/asgi.py b/test/python/server_port/asgi.py index e79ced00..810a182c 100644 --- a/test/python/server_port/asgi.py +++ b/test/python/server_port/asgi.py @@ -1,11 +1,13 @@ async def application(scope, receive, send): assert scope['type'] == 'http' - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', b'0'), - (b'server-port', str(scope['server'][1]).encode()), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'server-port', str(scope['server'][1]).encode()), + ], + } + ) diff --git a/test/python/server_port/wsgi.py b/test/python/server_port/wsgi.py index 89cd82b3..c796da95 100644 --- a/test/python/server_port/wsgi.py +++ b/test/python/server_port/wsgi.py @@ -1,7 +1,7 @@ def application(environ, start_response): - start_response('200', [ - ('Content-Length', '0'), - ('Server-Port', environ.get('SERVER_PORT')) - ]) + start_response( + '200', + [('Content-Length', '0'), ('Server-Port', environ.get('SERVER_PORT'))], + ) return [] diff --git a/test/python/targets/asgi.py b/test/python/targets/asgi.py new file mode 100644 index 00000000..b51f3964 --- /dev/null +++ b/test/python/targets/asgi.py @@ -0,0 +1,54 @@ +async def application_201(scope, receive, send): + assert scope['type'] == 'http' + + await send( + { + 'type': 'http.response.start', + 'status': 201, + 'headers': [(b'content-length', b'0')], + } + ) + + +async def application_200(scope, receive, send): + assert scope['type'] == 'http' + + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', b'0')], + } + ) + + +def legacy_application_200(scope): + assert scope['type'] == 'http' + + return legacy_app_http_200 + + +async def legacy_app_http_200(receive, send): + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', b'0')], + } + ) + + +def legacy_application_201(scope, receive=None, send=None): + assert scope['type'] == 'http' + + return legacy_app_http_201 + + +async def legacy_app_http_201(receive, send): + await send( + { + 'type': 'http.response.start', + 'status': 201, + 'headers': [(b'content-length', b'0')], + } + ) diff --git a/test/python/targets/wsgi.py b/test/python/targets/wsgi.py new file mode 100644 index 00000000..fa17ab87 --- /dev/null +++ b/test/python/targets/wsgi.py @@ -0,0 +1,8 @@ +def wsgi_target_a(env, start_response): + start_response('200', [('Content-Length', '1')]) + return [b'1'] + + +def wsgi_target_b(env, start_response): + start_response('200', [('Content-Length', '1')]) + return [b'2'] diff --git a/test/python/threading/asgi.py b/test/python/threading/asgi.py index 3c978e50..c4169a24 100644 --- a/test/python/threading/asgi.py +++ b/test/python/threading/asgi.py @@ -33,10 +33,10 @@ async def application(scope, receive, send): Foo(Foo.num).start() Foo.num += 10 - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', b'0'), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [(b'content-length', b'0')], + } + ) diff --git a/test/python/threads/asgi.py b/test/python/threads/asgi.py index d51ae431..ff4e52ad 100644 --- a/test/python/threads/asgi.py +++ b/test/python/threads/asgi.py @@ -2,6 +2,7 @@ import asyncio import time import threading + async def application(scope, receive, send): assert scope['type'] == 'http' @@ -17,11 +18,13 @@ async def application(scope, receive, send): time.sleep(delay) - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-length', b'0'), - (b'x-thread', str(threading.currentThread().ident).encode()), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'x-thread', str(threading.currentThread().ident).encode()), + ], + } + ) diff --git a/test/python/threads/wsgi.py b/test/python/threads/wsgi.py index 1cc8ffe2..cc283cfe 100644 --- a/test/python/threads/wsgi.py +++ b/test/python/threads/wsgi.py @@ -1,15 +1,19 @@ import time import threading + def application(environ, start_response): delay = float(environ.get('HTTP_X_DELAY', 0)) time.sleep(delay) - start_response('200', [ - ('Content-Length', '0'), - ('Wsgi-Multithread', str(environ['wsgi.multithread'])), - ('X-Thread', str(threading.currentThread().ident)) - ]) + start_response( + '200', + [ + ('Content-Length', '0'), + ('Wsgi-Multithread', str(environ['wsgi.multithread'])), + ('X-Thread', str(threading.currentThread().ident)), + ], + ) return [] diff --git a/test/python/upload/wsgi.py b/test/python/upload/wsgi.py index 37ee89eb..953c5ecc 100644 --- a/test/python/upload/wsgi.py +++ b/test/python/upload/wsgi.py @@ -1,6 +1,7 @@ from tempfile import TemporaryFile import os, cgi + def read(environ): length = int(environ.get('CONTENT_LENGTH', 0)) @@ -11,6 +12,7 @@ def read(environ): environ['wsgi.input'] = body return body + def application(environ, start_response): file = read(environ) @@ -19,9 +21,9 @@ def application(environ, start_response): filename = form['file'].filename data = filename.encode() + form['file'].file.read() - start_response('200 OK', [ - ('Content-Type', 'text/plain'), - ('Content-Length', str(len(data))), - ]) + start_response( + '200 OK', + [('Content-Type', 'text/plain'), ('Content-Length', str(len(data)))], + ) return data diff --git a/test/python/user_group/wsgi.py b/test/python/user_group/wsgi.py index f5deb87d..4003c064 100644 --- a/test/python/user_group/wsgi.py +++ b/test/python/user_group/wsgi.py @@ -1,18 +1,19 @@ import json import os + def application(environ, start_response): uid = os.geteuid() gid = os.getegid() - out = json.dumps({ - 'UID': uid, - 'GID': gid, - }).encode('utf-8') + out = json.dumps({'UID': uid, 'GID': gid,}).encode('utf-8') - start_response('200 OK', [ - ('Content-Length', str(len(out))), - ('Content-Type', 'application/json') - ]) + start_response( + '200 OK', + [ + ('Content-Length', str(len(out))), + ('Content-Type', 'application/json'), + ], + ) return [out] diff --git a/test/python/variables/asgi.py b/test/python/variables/asgi.py index dd1cca72..5a4f55e8 100644 --- a/test/python/variables/asgi.py +++ b/test/python/variables/asgi.py @@ -17,24 +17,23 @@ async def application(scope, receive, send): res.append(h[1]) return b', '.join(res) - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - (b'content-type', get_header(b'content-type')), - (b'content-length', str(len(body)).encode()), - (b'request-method', scope['method'].encode()), - (b'request-uri', scope['path'].encode()), - (b'http-host', get_header(b'host')), - (b'http-version', scope['http_version'].encode()), - (b'asgi-version', scope['asgi']['version'].encode()), - (b'asgi-spec-version', scope['asgi']['spec_version'].encode()), - (b'scheme', scope['scheme'].encode()), - (b'custom-header', get_header(b'custom-header')), - ] - }) + await send( + { + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-type', get_header(b'content-type')), + (b'content-length', str(len(body)).encode()), + (b'request-method', scope['method'].encode()), + (b'request-uri', scope['path'].encode()), + (b'http-host', get_header(b'host')), + (b'http-version', scope['http_version'].encode()), + (b'asgi-version', scope['asgi']['version'].encode()), + (b'asgi-spec-version', scope['asgi']['spec_version'].encode()), + (b'scheme', scope['scheme'].encode()), + (b'custom-header', get_header(b'custom-header')), + ], + } + ) - await send({ - 'type': 'http.response.body', - 'body': body, - }) + await send({'type': 'http.response.body', 'body': body}) diff --git a/test/python/variables/wsgi.py b/test/python/variables/wsgi.py index 53991e5e..5d77902d 100644 --- a/test/python/variables/wsgi.py +++ b/test/python/variables/wsgi.py @@ -3,19 +3,22 @@ def application(environ, start_response): content_length = int(environ.get('CONTENT_LENGTH', 0)) body = bytes(environ['wsgi.input'].read(content_length)) - start_response('200', [ - ('Content-Type', environ.get('CONTENT_TYPE')), - ('Content-Length', str(len(body))), - ('Request-Method', environ.get('REQUEST_METHOD')), - ('Request-Uri', environ.get('REQUEST_URI')), - ('Http-Host', environ.get('HTTP_HOST')), - ('Server-Protocol', environ.get('SERVER_PROTOCOL')), - ('Server-Software', environ.get('SERVER_SOFTWARE')), - ('Custom-Header', environ.get('HTTP_CUSTOM_HEADER')), - ('Wsgi-Version', str(environ['wsgi.version'])), - ('Wsgi-Url-Scheme', environ['wsgi.url_scheme']), - ('Wsgi-Multithread', str(environ['wsgi.multithread'])), - ('Wsgi-Multiprocess', str(environ['wsgi.multiprocess'])), - ('Wsgi-Run-Once', str(environ['wsgi.run_once'])) - ]) + start_response( + '200', + [ + ('Content-Type', environ.get('CONTENT_TYPE')), + ('Content-Length', str(len(body))), + ('Request-Method', environ.get('REQUEST_METHOD')), + ('Request-Uri', environ.get('REQUEST_URI')), + ('Http-Host', environ.get('HTTP_HOST')), + ('Server-Protocol', environ.get('SERVER_PROTOCOL')), + ('Server-Software', environ.get('SERVER_SOFTWARE')), + ('Custom-Header', environ.get('HTTP_CUSTOM_HEADER')), + ('Wsgi-Version', str(environ['wsgi.version'])), + ('Wsgi-Url-Scheme', environ['wsgi.url_scheme']), + ('Wsgi-Multithread', str(environ['wsgi.multithread'])), + ('Wsgi-Multiprocess', str(environ['wsgi.multiprocess'])), + ('Wsgi-Run-Once', str(environ['wsgi.run_once'])), + ], + ) return [body] diff --git a/test/python/websockets/mirror/asgi.py b/test/python/websockets/mirror/asgi.py index 0f1d9953..72a32d67 100644 --- a/test/python/websockets/mirror/asgi.py +++ b/test/python/websockets/mirror/asgi.py @@ -3,16 +3,16 @@ async def application(scope, receive, send): while True: m = await receive() if m['type'] == 'websocket.connect': - await send({ - 'type': 'websocket.accept', - }) + await send({'type': 'websocket.accept'}) if m['type'] == 'websocket.receive': - await send({ - 'type': 'websocket.send', - 'bytes': m.get('bytes', None), - 'text': m.get('text', None), - }) + await send( + { + 'type': 'websocket.send', + 'bytes': m.get('bytes', None), + 'text': m.get('text', None), + } + ) if m['type'] == 'websocket.disconnect': - break; + break diff --git a/test/python/websockets/subprotocol/asgi.py b/test/python/websockets/subprotocol/asgi.py index 92263dd7..0385bb9d 100644 --- a/test/python/websockets/subprotocol/asgi.py +++ b/test/python/websockets/subprotocol/asgi.py @@ -6,20 +6,24 @@ async def application(scope, receive, send): if m['type'] == 'websocket.connect': subprotocols = scope['subprotocols'] - await send({ - 'type': 'websocket.accept', - 'headers': [ - (b'x-subprotocols', str(subprotocols).encode()), - ], - 'subprotocol': subprotocols[0], - }) + await send( + { + 'type': 'websocket.accept', + 'headers': [ + (b'x-subprotocols', str(subprotocols).encode()), + ], + 'subprotocol': subprotocols[0], + } + ) if m['type'] == 'websocket.receive': - await send({ - 'type': 'websocket.send', - 'bytes': m.get('bytes', None), - 'text': m.get('text', None), - }) + await send( + { + 'type': 'websocket.send', + 'bytes': m.get('bytes', None), + 'text': m.get('text', None), + } + ) if m['type'] == 'websocket.disconnect': - break; + break diff --git a/test/ruby/encoding/config.ru b/test/ruby/encoding/config.ru new file mode 100644 index 00000000..60bf0efa --- /dev/null +++ b/test/ruby/encoding/config.ru @@ -0,0 +1,8 @@ +app = Proc.new do |env| + ['200', { + 'Content-Length' => '0', + 'X-Enc' => Encoding.default_external.to_s, + }, ['']] +end + +run app diff --git a/test/test_access_log.py b/test/test_access_log.py index 65d5e50a..ba254c5e 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -1,6 +1,7 @@ import time import pytest + from unit.applications.lang.python import TestApplicationPython from unit.option import option @@ -248,7 +249,7 @@ Connection: close assert self.search_in_log(r'/delete', 'access.log') is None, 'delete' - def test_access_log_change(self, temp_dir): + def test_access_log_change(self): self.load('empty') self.get() diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py index 886a160b..f503fa82 100644 --- a/test/test_asgi_application.py +++ b/test/test_asgi_application.py @@ -3,19 +3,17 @@ import time from distutils.version import LooseVersion import pytest + from unit.applications.lang.python import TestApplicationPython from unit.option import option class TestASGIApplication(TestApplicationPython): - prerequisites = {'modules': {'python': - lambda v: LooseVersion(v) >= LooseVersion('3.5')}} + prerequisites = { + 'modules': {'python': lambda v: LooseVersion(v) >= LooseVersion('3.5')} + } load_module = 'asgi' - def findall(self, pattern): - with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f: - return re.findall(pattern, f.read()) - def test_asgi_application_variables(self): self.load('variables') @@ -31,7 +29,8 @@ Content-Type: text/html Connection: close custom-header: BLAH -%s""" % (len(body), body.encode()), +%s""" + % (len(body), body.encode()), raw=True, ) @@ -145,7 +144,7 @@ custom-header: BLAH assert 'success' in self.conf( '{"http":{"max_body_size": ' + str(max_body_size) + ' }}', - 'settings' + 'settings', ) assert self.get()['status'] == 200, 'init' @@ -398,7 +397,7 @@ Connection: close socks.append(sock) - time.sleep(1.0) # required to avoid greedy request reading + time.sleep(1.0) # required to avoid greedy request reading threads = set() diff --git a/test/test_asgi_lifespan.py b/test/test_asgi_lifespan.py index 3ecece43..90866ec3 100644 --- a/test/test_asgi_lifespan.py +++ b/test/test_asgi_lifespan.py @@ -2,6 +2,7 @@ import os from distutils.version import LooseVersion import pytest + from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython from unit.option import option @@ -13,45 +14,88 @@ class TestASGILifespan(TestApplicationPython): } load_module = 'asgi' - def test_asgi_lifespan(self): - self.load('lifespan/empty') + def setup_cookies(self, prefix): + base_dir = option.test_dir + '/python/lifespan/empty' - startup_path = option.test_dir + '/python/lifespan/empty/startup' - shutdown_path = option.test_dir + '/python/lifespan/empty/shutdown' - version_path = option.test_dir + '/python/lifespan/empty/version' + os.chmod(base_dir, 0o777) - os.chmod(option.test_dir + '/python/lifespan/empty', 0o777) + for name in ['startup', 'shutdown', 'version']: + path = option.test_dir + '/python/lifespan/empty/' + prefix + name + open(path, 'a').close() + os.chmod(path, 0o777) - open(startup_path, 'a').close() - os.chmod(startup_path, 0o777) + def assert_cookies(self, prefix): + for name in ['startup', 'shutdown']: + path = option.test_dir + '/python/lifespan/empty/' + prefix + name + exists = os.path.isfile(path) + if exists: + os.remove(path) - open(shutdown_path, 'a').close() - os.chmod(shutdown_path, 0o777) + assert not exists, name - open(version_path, 'a').close() - os.chmod(version_path, 0o777) + path = option.test_dir + '/python/lifespan/empty/' + prefix + 'version' - assert self.get()['status'] == 204 + with open(path, 'r') as f: + version = f.read() - unit_stop() + os.remove(path) + + assert version == '3.0 2.0', 'version' - is_startup = os.path.isfile(startup_path) - is_shutdown = os.path.isfile(shutdown_path) + def test_asgi_lifespan(self): + self.load('lifespan/empty') - if is_startup: - os.remove(startup_path) + self.setup_cookies('') - if is_shutdown: - os.remove(shutdown_path) + assert self.get()['status'] == 204 - with open(version_path, 'r') as f: - version = f.read() + unit_stop() - os.remove(version_path) + self.assert_cookies('') + + def test_asgi_lifespan_targets(self): + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "match": {"uri": "/1"}, + "action": {"pass": "applications/targets/1"}, + }, + { + "match": {"uri": "/2"}, + "action": {"pass": "applications/targets/2"}, + }, + ], + "applications": { + "targets": { + "type": "python", + "processes": {"spare": 0}, + "working_directory": option.test_dir + + "/python/lifespan/empty", + "path": option.test_dir + '/python/lifespan/empty', + "targets": { + "1": {"module": "asgi", "callable": "application"}, + "2": { + "module": "asgi", + "callable": "application2", + }, + }, + } + }, + } + ) + + self.setup_cookies('') + self.setup_cookies('app2_') + + assert self.get(url="/1")['status'] == 204 + assert self.get(url="/2")['status'] == 204 - assert not is_startup, 'startup' - assert not is_shutdown, 'shutdown' - assert version == '3.0 2.0', 'version' + unit_stop() + + self.assert_cookies('') + self.assert_cookies('app2_') def test_asgi_lifespan_failed(self): self.load('lifespan/failed') diff --git a/test/test_asgi_targets.py b/test/test_asgi_targets.py new file mode 100644 index 00000000..a0eb1f84 --- /dev/null +++ b/test/test_asgi_targets.py @@ -0,0 +1,92 @@ +from distutils.version import LooseVersion + +import pytest + +from unit.applications.lang.python import TestApplicationPython +from unit.option import option + + +class TestASGITargets(TestApplicationPython): + prerequisites = { + 'modules': {'python': lambda v: LooseVersion(v) >= LooseVersion('3.5')} + } + load_module = 'asgi' + + @pytest.fixture(autouse=True) + def setup_method_fixture(self): + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "match": {"uri": "/1"}, + "action": {"pass": "applications/targets/1"}, + }, + { + "match": {"uri": "/2"}, + "action": {"pass": "applications/targets/2"}, + }, + ], + "applications": { + "targets": { + "type": "python", + "processes": {"spare": 0}, + "working_directory": option.test_dir + + "/python/targets/", + "path": option.test_dir + '/python/targets/', + "protocol": "asgi", + "targets": { + "1": { + "module": "asgi", + "callable": "application_200", + }, + "2": { + "module": "asgi", + "callable": "application_201", + }, + }, + } + }, + } + ) + + def conf_targets(self, targets): + assert 'success' in self.conf(targets, 'applications/targets/targets') + + def test_asgi_targets(self): + assert self.get(url='/1')['status'] == 200 + assert self.get(url='/2')['status'] == 201 + + def test_asgi_targets_legacy(self): + self.conf_targets( + { + "1": {"module": "asgi", "callable": "legacy_application_200"}, + "2": {"module": "asgi", "callable": "legacy_application_201"}, + } + ) + + assert self.get(url='/1')['status'] == 200 + assert self.get(url='/2')['status'] == 201 + + def test_asgi_targets_mix(self): + self.conf_targets( + { + "1": {"module": "asgi", "callable": "application_200"}, + "2": {"module": "asgi", "callable": "legacy_application_201"}, + } + ) + + assert self.get(url='/1')['status'] == 200 + assert self.get(url='/2')['status'] == 201 + + def test_asgi_targets_broken(self, skip_alert): + skip_alert(r'Python failed to get "blah" from module') + + self.conf_targets( + { + "1": {"module": "asgi", "callable": "application_200"}, + "2": {"module": "asgi", "callable": "blah"}, + } + ) + + assert self.get(url='/1')['status'] != 200 diff --git a/test/test_asgi_websockets.py b/test/test_asgi_websockets.py index 7218d526..140bcb9a 100644 --- a/test/test_asgi_websockets.py +++ b/test/test_asgi_websockets.py @@ -3,14 +3,16 @@ import time from distutils.version import LooseVersion import pytest + from unit.applications.lang.python import TestApplicationPython from unit.applications.websockets import TestApplicationWebsocket from unit.option import option class TestASGIWebsockets(TestApplicationPython): - prerequisites = {'modules': {'python': - lambda v: LooseVersion(v) >= LooseVersion('3.5')}} + prerequisites = { + 'modules': {'python': lambda v: LooseVersion(v) >= LooseVersion('3.5')} + } load_module = 'asgi' ws = TestApplicationWebsocket() @@ -74,7 +76,9 @@ class TestASGIWebsockets(TestApplicationPython): sock.close() assert resp['status'] == 101, 'status' - assert resp['headers']['x-subprotocols'] == "('chat', 'phone', 'video')", 'subprotocols' + assert ( + resp['headers']['x-subprotocols'] == "('chat', 'phone', 'video')" + ), 'subprotocols' assert resp['headers']['sec-websocket-protocol'] == 'chat', 'key' def test_asgi_websockets_mirror(self): @@ -159,7 +163,7 @@ class TestASGIWebsockets(TestApplicationPython): self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) self.ws.frame_write( - sock, self.ws.OP_CONT, 'fragment2', length=2**64 - 1 + sock, self.ws.OP_CONT, 'fragment2', length=2 ** 64 - 1 ) self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE @@ -231,7 +235,7 @@ class TestASGIWebsockets(TestApplicationPython): @pytest.mark.skip('not yet') def test_asgi_websockets_handshake_upgrade_absent( - self + self, ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 self.load('websockets/mirror') @@ -324,7 +328,9 @@ class TestASGIWebsockets(TestApplicationPython): }, ) - assert resp['status'] == 400, 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 + assert ( + resp['status'] == 400 + ), 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 def test_asgi_websockets_handshake_method_invalid(self): self.load('websockets/mirror') @@ -419,14 +425,14 @@ class TestASGIWebsockets(TestApplicationPython): frame = self.ws.frame_read(sock) self.check_frame(frame, True, opcode, payload) - check_length(0) # 1_1_1 - check_length(125) # 1_1_2 - check_length(126) # 1_1_3 - check_length(127) # 1_1_4 - check_length(128) # 1_1_5 - check_length(65535) # 1_1_6 - check_length(65536) # 1_1_7 - check_length(65536, chopsize = 997) # 1_1_8 + check_length(0) # 1_1_1 + check_length(125) # 1_1_2 + check_length(126) # 1_1_3 + check_length(127) # 1_1_4 + check_length(128) # 1_1_5 + check_length(65535) # 1_1_6 + check_length(65536) # 1_1_7 + check_length(65536, chopsize=997) # 1_1_8 self.close_connection(sock) @@ -445,14 +451,14 @@ class TestASGIWebsockets(TestApplicationPython): self.check_frame(frame, True, opcode, payload) - check_length(0) # 1_2_1 - check_length(125) # 1_2_2 - check_length(126) # 1_2_3 - check_length(127) # 1_2_4 - check_length(128) # 1_2_5 - check_length(65535) # 1_2_6 - check_length(65536) # 1_2_7 - check_length(65536, chopsize = 997) # 1_2_8 + check_length(0) # 1_2_1 + check_length(125) # 1_2_2 + check_length(126) # 1_2_3 + check_length(127) # 1_2_4 + check_length(128) # 1_2_5 + check_length(65535) # 1_2_6 + check_length(65536) # 1_2_7 + check_length(65536, chopsize=997) # 1_2_8 self.close_connection(sock) @@ -470,11 +476,11 @@ class TestASGIWebsockets(TestApplicationPython): self.check_frame(frame, True, op_pong, payload, decode=decode) - check_ping('') # 2_1 - check_ping('Hello, world!') # 2_2 + check_ping('') # 2_1 + check_ping('Hello, world!') # 2_2 check_ping(b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', decode=False) # 2_3 - check_ping(b'\xfe' * 125, decode=False) # 2_4 - check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6 + check_ping(b'\xfe' * 125, decode=False) # 2_4 + check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6 self.close_connection(sock) @@ -935,7 +941,9 @@ class TestASGIWebsockets(TestApplicationPython): frame = self.ws.frame_read(sock) if frame['opcode'] == self.ws.OP_TEXT: - self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + self.check_frame( + frame, True, self.ws.OP_TEXT, 'fragment1fragment2' + ) frame = None self.check_close(sock, 1002, frame=frame) @@ -1092,27 +1100,27 @@ class TestASGIWebsockets(TestApplicationPython): self.close_connection(sock) -# Unit does not support UTF-8 validation -# -# # 6_3_1 FAIL -# -# payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' -# payload_2 = '\xed\xa0\x80' -# payload_3 = '\x65\x64\x69\x74\x65\x64' -# -# payload = payload_1 + payload_2 + payload_3 -# -# self.ws.message(sock, self.ws.OP_TEXT, payload) -# self.check_close(sock, 1007) -# -# # 6_3_2 FAIL -# -# _, sock, _ = self.ws.upgrade() -# -# self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) -# self.check_close(sock, 1007) -# -# # 6_4_1 ... 6_4_4 FAIL + # Unit does not support UTF-8 validation + # + # # 6_3_1 FAIL + # + # payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' + # payload_2 = '\xed\xa0\x80' + # payload_3 = '\x65\x64\x69\x74\x65\x64' + # + # payload = payload_1 + payload_2 + payload_3 + # + # self.ws.message(sock, self.ws.OP_TEXT, payload) + # self.check_close(sock, 1007) + # + # # 6_3_2 FAIL + # + # _, sock, _ = self.ws.upgrade() + # + # self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) + # self.check_close(sock, 1007) + # + # # 6_4_1 ... 6_4_4 FAIL def test_asgi_websockets_7_1_1__7_5_1(self): self.load('websockets/mirror') @@ -1239,15 +1247,15 @@ class TestASGIWebsockets(TestApplicationPython): self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) self.check_close(sock, 1002) -# # 7_5_1 FAIL Unit does not support UTF-8 validation -# -# _, sock, _ = self.ws.upgrade() -# -# payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \ -# '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64') -# -# self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) -# self.check_close(sock, 1007) + # # 7_5_1 FAIL Unit does not support UTF-8 validation + # + # _, sock, _ = self.ws.upgrade() + # + # payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \ + # '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64') + # + # self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + # self.check_close(sock, 1007) def test_asgi_websockets_7_7_X__7_9_X(self): self.load('websockets/mirror') @@ -1350,52 +1358,52 @@ class TestASGIWebsockets(TestApplicationPython): frame = self.ws.frame_read(sock, read_timeout=5) self.check_frame(frame, True, opcode, payload) - check_payload(op_text, 64 * 2 ** 10) # 9_1_1 - check_payload(op_text, 256 * 2 ** 10) # 9_1_2 - check_payload(op_text, 2 ** 20) # 9_1_3 - check_payload(op_text, 4 * 2 ** 20) # 9_1_4 - check_payload(op_text, 8 * 2 ** 20) # 9_1_5 - check_payload(op_text, 16 * 2 ** 20) # 9_1_6 + check_payload(op_text, 64 * 2 ** 10) # 9_1_1 + check_payload(op_text, 256 * 2 ** 10) # 9_1_2 + check_payload(op_text, 2 ** 20) # 9_1_3 + check_payload(op_text, 4 * 2 ** 20) # 9_1_4 + check_payload(op_text, 8 * 2 ** 20) # 9_1_5 + check_payload(op_text, 16 * 2 ** 20) # 9_1_6 - check_payload(op_binary, 64 * 2 ** 10) # 9_2_1 - check_payload(op_binary, 256 * 2 ** 10) # 9_2_2 - check_payload(op_binary, 2 ** 20) # 9_2_3 - check_payload(op_binary, 4 * 2 ** 20) # 9_2_4 - check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 - check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 + check_payload(op_binary, 64 * 2 ** 10) # 9_2_1 + check_payload(op_binary, 256 * 2 ** 10) # 9_2_2 + check_payload(op_binary, 2 ** 20) # 9_2_3 + check_payload(op_binary, 4 * 2 ** 20) # 9_2_4 + check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 + check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 if option.system != 'Darwin' and option.system != 'FreeBSD': - check_message(op_text, 64) # 9_3_1 - check_message(op_text, 256) # 9_3_2 - check_message(op_text, 2 ** 10) # 9_3_3 - check_message(op_text, 4 * 2 ** 10) # 9_3_4 - check_message(op_text, 16 * 2 ** 10) # 9_3_5 - check_message(op_text, 64 * 2 ** 10) # 9_3_6 - check_message(op_text, 256 * 2 ** 10) # 9_3_7 - check_message(op_text, 2 ** 20) # 9_3_8 - check_message(op_text, 4 * 2 ** 20) # 9_3_9 - - check_message(op_binary, 64) # 9_4_1 - check_message(op_binary, 256) # 9_4_2 - check_message(op_binary, 2 ** 10) # 9_4_3 - check_message(op_binary, 4 * 2 ** 10) # 9_4_4 - check_message(op_binary, 16 * 2 ** 10) # 9_4_5 - check_message(op_binary, 64 * 2 ** 10) # 9_4_6 - check_message(op_binary, 256 * 2 ** 10) # 9_4_7 - check_message(op_binary, 2 ** 20) # 9_4_8 - check_message(op_binary, 4 * 2 ** 20) # 9_4_9 - - check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1 - check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2 - check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3 - check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4 - check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5 - check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6 - - check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1 - check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2 - check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3 - check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4 + check_message(op_text, 64) # 9_3_1 + check_message(op_text, 256) # 9_3_2 + check_message(op_text, 2 ** 10) # 9_3_3 + check_message(op_text, 4 * 2 ** 10) # 9_3_4 + check_message(op_text, 16 * 2 ** 10) # 9_3_5 + check_message(op_text, 64 * 2 ** 10) # 9_3_6 + check_message(op_text, 256 * 2 ** 10) # 9_3_7 + check_message(op_text, 2 ** 20) # 9_3_8 + check_message(op_text, 4 * 2 ** 20) # 9_3_9 + + check_message(op_binary, 64) # 9_4_1 + check_message(op_binary, 256) # 9_4_2 + check_message(op_binary, 2 ** 10) # 9_4_3 + check_message(op_binary, 4 * 2 ** 10) # 9_4_4 + check_message(op_binary, 16 * 2 ** 10) # 9_4_5 + check_message(op_binary, 64 * 2 ** 10) # 9_4_6 + check_message(op_binary, 256 * 2 ** 10) # 9_4_7 + check_message(op_binary, 2 ** 20) # 9_4_8 + check_message(op_binary, 4 * 2 ** 20) # 9_4_9 + + check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1 + check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2 + check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3 + check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4 + check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5 + check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6 + + check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1 + check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2 + check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3 + check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4 check_payload(op_binary, 2 ** 20, chopsize=1024) # 9_6_5 check_payload(op_binary, 2 ** 20, chopsize=2048) # 9_6_6 diff --git a/test/test_configuration.py b/test/test_configuration.py index b7417264..880aef6c 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,4 +1,6 @@ import pytest + +import socket from unit.control import TestControl @@ -260,6 +262,33 @@ class TestConfiguration(TestControl): } ), 'explicit ipv6' + def test_listeners_port_release(self): + for i in range(10): + fail = False + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + self.conf( + { + "listeners": {"127.0.0.1:7080": {"pass": "routes"}}, + "routes": [], + } + ) + + resp = self.conf({"listeners": {}, "applications": {}}) + + try: + s.bind(('127.0.0.1', 7080)) + s.listen() + + except OSError: + fail = True + + if fail: + pytest.fail('cannot bind or listen to the address') + + assert 'success' in resp, 'port release' + @pytest.mark.skip('not yet, unsafe') def test_listeners_no_port(self): assert 'error' in self.conf( diff --git a/test/test_go_application.py b/test/test_go_application.py index e833d190..438ce2e0 100644 --- a/test/test_go_application.py +++ b/test/test_go_application.py @@ -129,11 +129,9 @@ class TestGoApplication(TestApplicationGo): def test_go_application_command_line_arguments_type(self): self.load('command_line_arguments') - assert 'error' in \ - self.conf( - '' "a b c", 'applications/command_line_arguments/arguments' - ), \ - 'arguments type' + assert 'error' in self.conf( + '' "a b c", 'applications/command_line_arguments/arguments' + ), 'arguments type' def test_go_application_command_line_arguments_0(self): self.load('command_line_arguments') diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py index e8fa26c6..e02ef1cf 100644 --- a/test/test_go_isolation.py +++ b/test/test_go_isolation.py @@ -3,10 +3,12 @@ import os import pwd import pytest + from unit.applications.lang.go import TestApplicationGo from unit.option import option from unit.utils import getns + class TestGoIsolation(TestApplicationGo): prerequisites = {'modules': {'go': 'any'}, 'features': ['isolation']} @@ -279,7 +281,7 @@ class TestGoIsolation(TestApplicationGo): isolation['namespaces'] = { 'mount': True, 'credential': True, - 'pid': True + 'pid': True, } self.load('ns_inspect', isolation=isolation) @@ -337,12 +339,10 @@ class TestGoIsolation(TestApplicationGo): isolation['namespaces'] = { 'mount': True, 'credential': True, - 'pid': True + 'pid': True, } - isolation['automount'] = { - 'tmpfs': False - } + isolation['automount'] = {'tmpfs': False} self.load('ns_inspect', isolation=isolation) @@ -352,9 +352,7 @@ class TestGoIsolation(TestApplicationGo): "/ /tmp" not in obj['Mounts'] and "tmpfs" not in obj['Mounts'] ), 'app has no /tmp mounted' - isolation['automount'] = { - 'tmpfs': True - } + isolation['automount'] = {'tmpfs': True} self.load('ns_inspect', isolation=isolation) diff --git a/test/test_go_isolation_rootfs.py b/test/test_go_isolation_rootfs.py index 2bded5ec..1cc59c67 100644 --- a/test/test_go_isolation_rootfs.py +++ b/test/test_go_isolation_rootfs.py @@ -1,6 +1,7 @@ import os import pytest + from unit.applications.lang.go import TestApplicationGo diff --git a/test/test_http_header.py b/test/test_http_header.py index ca355eb7..fdb557cf 100644 --- a/test/test_http_header.py +++ b/test/test_http_header.py @@ -1,4 +1,5 @@ import pytest + from unit.applications.lang.python import TestApplicationPython diff --git a/test/test_java_application.py b/test/test_java_application.py index 4a67f291..3fd5c26e 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -7,6 +7,7 @@ from unit.applications.lang.java import TestApplicationJava from unit.option import option from unit.utils import public_dir + class TestJavaApplication(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} @@ -712,9 +713,9 @@ class TestJavaApplication(TestApplicationJava): assert ( 'javax.servlet.include.request_uri: /data/test' in body ) == True, 'include request uri' - #assert ( + # assert ( # 'javax.servlet.include.context_path: ' in body - #) == True, 'include request context path' + # ) == True, 'include request context path' assert ( 'javax.servlet.include.servlet_path: /data' in body ) == True, 'include request servlet path' @@ -754,9 +755,9 @@ class TestJavaApplication(TestApplicationJava): assert ( 'javax.servlet.include.request_uri: null' in body ) == True, 'include request uri' - #assert ( + # assert ( # 'javax.servlet.include.context_path: null' in body - #) == True, 'include request context path' + # ) == True, 'include request context path' assert ( 'javax.servlet.include.servlet_path: null' in body ) == True, 'include request servlet path' @@ -1034,7 +1035,7 @@ class TestJavaApplication(TestApplicationJava): socks.append(sock) - time.sleep(0.25) # required to avoid greedy request reading + time.sleep(0.25) # required to avoid greedy request reading threads = set() diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py index 91773981..a401e23b 100644 --- a/test/test_java_isolation_rootfs.py +++ b/test/test_java_isolation_rootfs.py @@ -2,6 +2,7 @@ import os import subprocess import pytest + from unit.applications.lang.java import TestApplicationJava from unit.option import option diff --git a/test/test_java_websockets.py b/test/test_java_websockets.py index df9e0885..df0f76e8 100644 --- a/test/test_java_websockets.py +++ b/test/test_java_websockets.py @@ -2,6 +2,7 @@ import struct import time import pytest + from unit.applications.lang.java import TestApplicationJava from unit.applications.websockets import TestApplicationWebsocket from unit.option import option @@ -163,7 +164,7 @@ class TestJavaWebsockets(TestApplicationJava): @pytest.mark.skip('not yet') def test_java_websockets_handshake_upgrade_absent( - self + self, ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 self.load('websockets_mirror') @@ -256,7 +257,9 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - assert resp['status'] == 400, 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 + assert ( + resp['status'] == 400 + ), 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 def test_java_websockets_handshake_method_invalid(self): self.load('websockets_mirror') @@ -351,14 +354,14 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.message_read(sock) self.check_frame(frame, True, opcode, payload) - check_length(0) # 1_1_1 - check_length(125) # 1_1_2 - check_length(126) # 1_1_3 - check_length(127) # 1_1_4 - check_length(128) # 1_1_5 - check_length(65535) # 1_1_6 - check_length(65536) # 1_1_7 - check_length(65536, chopsize = 997) # 1_1_8 + check_length(0) # 1_1_1 + check_length(125) # 1_1_2 + check_length(126) # 1_1_3 + check_length(127) # 1_1_4 + check_length(128) # 1_1_5 + check_length(65535) # 1_1_6 + check_length(65536) # 1_1_7 + check_length(65536, chopsize=997) # 1_1_8 self.close_connection(sock) @@ -377,14 +380,14 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.message_read(sock) self.check_frame(frame, True, opcode, payload) - check_length(0) # 1_2_1 - check_length(125) # 1_2_2 - check_length(126) # 1_2_3 - check_length(127) # 1_2_4 - check_length(128) # 1_2_5 - check_length(65535) # 1_2_6 - check_length(65536) # 1_2_7 - check_length(65536, chopsize = 997) # 1_2_8 + check_length(0) # 1_2_1 + check_length(125) # 1_2_2 + check_length(126) # 1_2_3 + check_length(127) # 1_2_4 + check_length(128) # 1_2_5 + check_length(65535) # 1_2_6 + check_length(65536) # 1_2_7 + check_length(65536, chopsize=997) # 1_2_8 self.close_connection(sock) @@ -402,11 +405,11 @@ class TestJavaWebsockets(TestApplicationJava): self.check_frame(frame, True, op_pong, payload, decode=decode) - check_ping('') # 2_1 - check_ping('Hello, world!') # 2_2 + check_ping('') # 2_1 + check_ping('Hello, world!') # 2_2 check_ping(b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', decode=False) # 2_3 - check_ping(b'\xfe' * 125, decode=False) # 2_4 - check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6 + check_ping(b'\xfe' * 125, decode=False) # 2_4 + check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6 self.close_connection(sock) @@ -867,7 +870,9 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) if frame['opcode'] == self.ws.OP_TEXT: - self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + self.check_frame( + frame, True, self.ws.OP_TEXT, 'fragment1fragment2' + ) frame = None self.check_close(sock, 1002, frame=frame) @@ -1024,27 +1029,27 @@ class TestJavaWebsockets(TestApplicationJava): self.close_connection(sock) -# Unit does not support UTF-8 validation -# -# # 6_3_1 FAIL -# -# payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' -# payload_2 = '\xed\xa0\x80' -# payload_3 = '\x65\x64\x69\x74\x65\x64' -# -# payload = payload_1 + payload_2 + payload_3 -# -# self.ws.message(sock, self.ws.OP_TEXT, payload) -# self.check_close(sock, 1007) -# -# # 6_3_2 FAIL -# -# _, sock, _ = self.ws.upgrade() -# -# self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) -# self.check_close(sock, 1007) -# -# # 6_4_1 ... 6_4_4 FAIL + # Unit does not support UTF-8 validation + # + # # 6_3_1 FAIL + # + # payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' + # payload_2 = '\xed\xa0\x80' + # payload_3 = '\x65\x64\x69\x74\x65\x64' + # + # payload = payload_1 + payload_2 + payload_3 + # + # self.ws.message(sock, self.ws.OP_TEXT, payload) + # self.check_close(sock, 1007) + # + # # 6_3_2 FAIL + # + # _, sock, _ = self.ws.upgrade() + # + # self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) + # self.check_close(sock, 1007) + # + # # 6_4_1 ... 6_4_4 FAIL def test_java_websockets_7_1_1__7_5_1(self): self.load('websockets_mirror') @@ -1171,15 +1176,15 @@ class TestJavaWebsockets(TestApplicationJava): self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) self.check_close(sock, 1002) -# # 7_5_1 FAIL Unit does not support UTF-8 validation -# -# _, sock, _ = self.ws.upgrade() -# -# payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \ -# '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64') -# -# self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) -# self.check_close(sock, 1007) + # # 7_5_1 FAIL Unit does not support UTF-8 validation + # + # _, sock, _ = self.ws.upgrade() + # + # payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \ + # '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64') + # + # self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + # self.check_close(sock, 1007) def test_java_websockets_7_7_X__7_9_X(self): self.load('websockets_mirror') @@ -1282,52 +1287,52 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock, read_timeout=5) self.check_frame(frame, True, opcode, payload) - check_payload(op_text, 64 * 2 ** 10) # 9_1_1 - check_payload(op_text, 256 * 2 ** 10) # 9_1_2 - check_payload(op_text, 2 ** 20) # 9_1_3 - check_payload(op_text, 4 * 2 ** 20) # 9_1_4 - check_payload(op_text, 8 * 2 ** 20) # 9_1_5 - check_payload(op_text, 16 * 2 ** 20) # 9_1_6 + check_payload(op_text, 64 * 2 ** 10) # 9_1_1 + check_payload(op_text, 256 * 2 ** 10) # 9_1_2 + check_payload(op_text, 2 ** 20) # 9_1_3 + check_payload(op_text, 4 * 2 ** 20) # 9_1_4 + check_payload(op_text, 8 * 2 ** 20) # 9_1_5 + check_payload(op_text, 16 * 2 ** 20) # 9_1_6 - check_payload(op_binary, 64 * 2 ** 10) # 9_2_1 - check_payload(op_binary, 256 * 2 ** 10) # 9_2_2 - check_payload(op_binary, 2 ** 20) # 9_2_3 - check_payload(op_binary, 4 * 2 ** 20) # 9_2_4 - check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 - check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 + check_payload(op_binary, 64 * 2 ** 10) # 9_2_1 + check_payload(op_binary, 256 * 2 ** 10) # 9_2_2 + check_payload(op_binary, 2 ** 20) # 9_2_3 + check_payload(op_binary, 4 * 2 ** 20) # 9_2_4 + check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 + check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 if option.system != 'Darwin' and option.system != 'FreeBSD': - check_message(op_text, 64) # 9_3_1 - check_message(op_text, 256) # 9_3_2 - check_message(op_text, 2 ** 10) # 9_3_3 - check_message(op_text, 4 * 2 ** 10) # 9_3_4 - check_message(op_text, 16 * 2 ** 10) # 9_3_5 - check_message(op_text, 64 * 2 ** 10) # 9_3_6 - check_message(op_text, 256 * 2 ** 10) # 9_3_7 - check_message(op_text, 2 ** 20) # 9_3_8 - check_message(op_text, 4 * 2 ** 20) # 9_3_9 - - check_message(op_binary, 64) # 9_4_1 - check_message(op_binary, 256) # 9_4_2 - check_message(op_binary, 2 ** 10) # 9_4_3 - check_message(op_binary, 4 * 2 ** 10) # 9_4_4 - check_message(op_binary, 16 * 2 ** 10) # 9_4_5 - check_message(op_binary, 64 * 2 ** 10) # 9_4_6 - check_message(op_binary, 256 * 2 ** 10) # 9_4_7 - check_message(op_binary, 2 ** 20) # 9_4_8 - check_message(op_binary, 4 * 2 ** 20) # 9_4_9 - - check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1 - check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2 - check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3 - check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4 - check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5 - check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6 - - check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1 - check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2 - check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3 - check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4 + check_message(op_text, 64) # 9_3_1 + check_message(op_text, 256) # 9_3_2 + check_message(op_text, 2 ** 10) # 9_3_3 + check_message(op_text, 4 * 2 ** 10) # 9_3_4 + check_message(op_text, 16 * 2 ** 10) # 9_3_5 + check_message(op_text, 64 * 2 ** 10) # 9_3_6 + check_message(op_text, 256 * 2 ** 10) # 9_3_7 + check_message(op_text, 2 ** 20) # 9_3_8 + check_message(op_text, 4 * 2 ** 20) # 9_3_9 + + check_message(op_binary, 64) # 9_4_1 + check_message(op_binary, 256) # 9_4_2 + check_message(op_binary, 2 ** 10) # 9_4_3 + check_message(op_binary, 4 * 2 ** 10) # 9_4_4 + check_message(op_binary, 16 * 2 ** 10) # 9_4_5 + check_message(op_binary, 64 * 2 ** 10) # 9_4_6 + check_message(op_binary, 256 * 2 ** 10) # 9_4_7 + check_message(op_binary, 2 ** 20) # 9_4_8 + check_message(op_binary, 4 * 2 ** 20) # 9_4_9 + + check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1 + check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2 + check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3 + check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4 + check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5 + check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6 + + check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1 + check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2 + check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3 + check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4 check_payload(op_binary, 2 ** 20, chopsize=1024) # 9_6_5 check_payload(op_binary, 2 ** 20, chopsize=2048) # 9_6_6 diff --git a/test/test_node_application.py b/test/test_node_application.py index b277dc3a..48ed8d3d 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -1,6 +1,7 @@ import re import pytest + from unit.applications.lang.node import TestApplicationNode from unit.utils import waitforfiles @@ -8,13 +9,26 @@ from unit.utils import waitforfiles class TestNodeApplication(TestApplicationNode): prerequisites = {'modules': {'node': 'all'}} - def test_node_application_basic(self): - self.load('basic') - + def assert_basic_application(self): resp = self.get() assert resp['headers']['Content-Type'] == 'text/plain', 'basic header' assert resp['body'] == 'Hello World\n', 'basic body' + def test_node_application_basic(self): + self.load('basic') + + self.assert_basic_application() + + def test_node_application_loader_unit_http(self): + self.load('loader/unit_http') + + self.assert_basic_application() + + def test_node_application_loader_transitive_dependency(self): + self.load('loader/transitive_dependency') + + self.assert_basic_application() + def test_node_application_seq(self): self.load('basic') @@ -205,7 +219,9 @@ class TestNodeApplication(TestApplicationNode): def test_node_application_status_message(self): self.load('status_message') - assert re.search(r'200 blah', self.get(raw_resp=True)), 'status message' + assert re.search( + r'200 blah', self.get(raw_resp=True) + ), 'status message' def test_node_application_get_header_type(self): self.load('get_header_type') diff --git a/test/test_node_es_modules.py b/test/test_node_es_modules.py new file mode 100644 index 00000000..0945a967 --- /dev/null +++ b/test/test_node_es_modules.py @@ -0,0 +1,50 @@ +import pytest + +from distutils.version import LooseVersion +from unit.applications.lang.node import TestApplicationNode +from unit.applications.websockets import TestApplicationWebsocket + + +class TestNodeESModules(TestApplicationNode): + prerequisites = { + 'modules': { + 'node': lambda v: LooseVersion(v) >= LooseVersion("14.16.0") + } + } + + es_modules = True + ws = TestApplicationWebsocket() + + def assert_basic_application(self): + resp = self.get() + assert resp['headers']['Content-Type'] == 'text/plain', 'basic header' + assert resp['body'] == 'Hello World\n', 'basic body' + + def test_node_es_modules_loader_http(self): + self.load('loader/es_modules_http', name="app.mjs") + + self.assert_basic_application() + + def test_node_es_modules_loader_http_indirect(self): + self.load('loader/es_modules_http_indirect', name="app.js") + + self.assert_basic_application() + + def test_node_es_modules_loader_websockets(self): + self.load('loader/es_modules_websocket', name="app.mjs") + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + frame = self.ws.frame_read(sock) + + assert message == frame['data'].decode('utf-8'), 'mirror' + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + frame = self.ws.frame_read(sock) + + assert message == frame['data'].decode('utf-8'), 'mirror 2' + + sock.close() diff --git a/test/test_node_websockets.py b/test/test_node_websockets.py index d5f79811..51515f4e 100644 --- a/test/test_node_websockets.py +++ b/test/test_node_websockets.py @@ -2,6 +2,7 @@ import struct import time import pytest + from unit.applications.lang.node import TestApplicationNode from unit.applications.websockets import TestApplicationWebsocket from unit.option import option @@ -182,7 +183,7 @@ class TestNodeWebsockets(TestApplicationNode): @pytest.mark.skip('not yet') def test_node_websockets_handshake_upgrade_absent( - self + self, ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 self.load('websockets/mirror') @@ -275,7 +276,9 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - assert resp['status'] == 400, 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 + assert ( + resp['status'] == 400 + ), 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 def test_node_websockets_handshake_method_invalid(self): self.load('websockets/mirror') @@ -370,14 +373,14 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) self.check_frame(frame, True, opcode, payload) - check_length(0) # 1_1_1 - check_length(125) # 1_1_2 - check_length(126) # 1_1_3 - check_length(127) # 1_1_4 - check_length(128) # 1_1_5 - check_length(65535) # 1_1_6 - check_length(65536) # 1_1_7 - check_length(65536, chopsize = 997) # 1_1_8 + check_length(0) # 1_1_1 + check_length(125) # 1_1_2 + check_length(126) # 1_1_3 + check_length(127) # 1_1_4 + check_length(128) # 1_1_5 + check_length(65535) # 1_1_6 + check_length(65536) # 1_1_7 + check_length(65536, chopsize=997) # 1_1_8 self.close_connection(sock) @@ -396,14 +399,14 @@ class TestNodeWebsockets(TestApplicationNode): self.check_frame(frame, True, opcode, payload) - check_length(0) # 1_2_1 - check_length(125) # 1_2_2 - check_length(126) # 1_2_3 - check_length(127) # 1_2_4 - check_length(128) # 1_2_5 - check_length(65535) # 1_2_6 - check_length(65536) # 1_2_7 - check_length(65536, chopsize = 997) # 1_2_8 + check_length(0) # 1_2_1 + check_length(125) # 1_2_2 + check_length(126) # 1_2_3 + check_length(127) # 1_2_4 + check_length(128) # 1_2_5 + check_length(65535) # 1_2_6 + check_length(65536) # 1_2_7 + check_length(65536, chopsize=997) # 1_2_8 self.close_connection(sock) @@ -421,11 +424,11 @@ class TestNodeWebsockets(TestApplicationNode): self.check_frame(frame, True, op_pong, payload, decode=decode) - check_ping('') # 2_1 - check_ping('Hello, world!') # 2_2 + check_ping('') # 2_1 + check_ping('Hello, world!') # 2_2 check_ping(b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', decode=False) # 2_3 - check_ping(b'\xfe' * 125, decode=False) # 2_4 - check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6 + check_ping(b'\xfe' * 125, decode=False) # 2_4 + check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6 self.close_connection(sock) @@ -886,7 +889,9 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) if frame['opcode'] == self.ws.OP_TEXT: - self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + self.check_frame( + frame, True, self.ws.OP_TEXT, 'fragment1fragment2' + ) frame = None self.check_close(sock, 1002, frame=frame) @@ -1043,27 +1048,27 @@ class TestNodeWebsockets(TestApplicationNode): self.close_connection(sock) -# Unit does not support UTF-8 validation -# -# # 6_3_1 FAIL -# -# payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' -# payload_2 = '\xed\xa0\x80' -# payload_3 = '\x65\x64\x69\x74\x65\x64' -# -# payload = payload_1 + payload_2 + payload_3 -# -# self.ws.message(sock, self.ws.OP_TEXT, payload) -# self.check_close(sock, 1007) -# -# # 6_3_2 FAIL -# -# _, sock, _ = self.ws.upgrade() -# -# self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) -# self.check_close(sock, 1007) -# -# # 6_4_1 ... 6_4_4 FAIL + # Unit does not support UTF-8 validation + # + # # 6_3_1 FAIL + # + # payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' + # payload_2 = '\xed\xa0\x80' + # payload_3 = '\x65\x64\x69\x74\x65\x64' + # + # payload = payload_1 + payload_2 + payload_3 + # + # self.ws.message(sock, self.ws.OP_TEXT, payload) + # self.check_close(sock, 1007) + # + # # 6_3_2 FAIL + # + # _, sock, _ = self.ws.upgrade() + # + # self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) + # self.check_close(sock, 1007) + # + # # 6_4_1 ... 6_4_4 FAIL def test_node_websockets_7_1_1__7_5_1(self): self.load('websockets/mirror') @@ -1190,15 +1195,15 @@ class TestNodeWebsockets(TestApplicationNode): self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) self.check_close(sock, 1002) -# # 7_5_1 FAIL Unit does not support UTF-8 validation -# -# _, sock, _ = self.ws.upgrade() -# -# payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \ -# '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64') -# -# self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) -# self.check_close(sock, 1007) + # # 7_5_1 FAIL Unit does not support UTF-8 validation + # + # _, sock, _ = self.ws.upgrade() + # + # payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \ + # '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64') + # + # self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + # self.check_close(sock, 1007) def test_node_websockets_7_7_X__7_9_X(self): self.load('websockets/mirror') @@ -1301,52 +1306,52 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock, read_timeout=5) self.check_frame(frame, True, opcode, payload) - check_payload(op_text, 64 * 2 ** 10) # 9_1_1 - check_payload(op_text, 256 * 2 ** 10) # 9_1_2 - check_payload(op_text, 2 ** 20) # 9_1_3 - check_payload(op_text, 4 * 2 ** 20) # 9_1_4 - check_payload(op_text, 8 * 2 ** 20) # 9_1_5 - check_payload(op_text, 16 * 2 ** 20) # 9_1_6 + check_payload(op_text, 64 * 2 ** 10) # 9_1_1 + check_payload(op_text, 256 * 2 ** 10) # 9_1_2 + check_payload(op_text, 2 ** 20) # 9_1_3 + check_payload(op_text, 4 * 2 ** 20) # 9_1_4 + check_payload(op_text, 8 * 2 ** 20) # 9_1_5 + check_payload(op_text, 16 * 2 ** 20) # 9_1_6 - check_payload(op_binary, 64 * 2 ** 10) # 9_2_1 - check_payload(op_binary, 256 * 2 ** 10) # 9_2_2 - check_payload(op_binary, 2 ** 20) # 9_2_3 - check_payload(op_binary, 4 * 2 ** 20) # 9_2_4 - check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 - check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 + check_payload(op_binary, 64 * 2 ** 10) # 9_2_1 + check_payload(op_binary, 256 * 2 ** 10) # 9_2_2 + check_payload(op_binary, 2 ** 20) # 9_2_3 + check_payload(op_binary, 4 * 2 ** 20) # 9_2_4 + check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 + check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 if option.system != 'Darwin' and option.system != 'FreeBSD': - check_message(op_text, 64) # 9_3_1 - check_message(op_text, 256) # 9_3_2 - check_message(op_text, 2 ** 10) # 9_3_3 - check_message(op_text, 4 * 2 ** 10) # 9_3_4 - check_message(op_text, 16 * 2 ** 10) # 9_3_5 - check_message(op_text, 64 * 2 ** 10) # 9_3_6 - check_message(op_text, 256 * 2 ** 10) # 9_3_7 - check_message(op_text, 2 ** 20) # 9_3_8 - check_message(op_text, 4 * 2 ** 20) # 9_3_9 - - check_message(op_binary, 64) # 9_4_1 - check_message(op_binary, 256) # 9_4_2 - check_message(op_binary, 2 ** 10) # 9_4_3 - check_message(op_binary, 4 * 2 ** 10) # 9_4_4 - check_message(op_binary, 16 * 2 ** 10) # 9_4_5 - check_message(op_binary, 64 * 2 ** 10) # 9_4_6 - check_message(op_binary, 256 * 2 ** 10) # 9_4_7 - check_message(op_binary, 2 ** 20) # 9_4_8 - check_message(op_binary, 4 * 2 ** 20) # 9_4_9 - - check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1 - check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2 - check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3 - check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4 - check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5 - check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6 - - check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1 - check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2 - check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3 - check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4 + check_message(op_text, 64) # 9_3_1 + check_message(op_text, 256) # 9_3_2 + check_message(op_text, 2 ** 10) # 9_3_3 + check_message(op_text, 4 * 2 ** 10) # 9_3_4 + check_message(op_text, 16 * 2 ** 10) # 9_3_5 + check_message(op_text, 64 * 2 ** 10) # 9_3_6 + check_message(op_text, 256 * 2 ** 10) # 9_3_7 + check_message(op_text, 2 ** 20) # 9_3_8 + check_message(op_text, 4 * 2 ** 20) # 9_3_9 + + check_message(op_binary, 64) # 9_4_1 + check_message(op_binary, 256) # 9_4_2 + check_message(op_binary, 2 ** 10) # 9_4_3 + check_message(op_binary, 4 * 2 ** 10) # 9_4_4 + check_message(op_binary, 16 * 2 ** 10) # 9_4_5 + check_message(op_binary, 64 * 2 ** 10) # 9_4_6 + check_message(op_binary, 256 * 2 ** 10) # 9_4_7 + check_message(op_binary, 2 ** 20) # 9_4_8 + check_message(op_binary, 4 * 2 ** 20) # 9_4_9 + + check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1 + check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2 + check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3 + check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4 + check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5 + check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6 + + check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1 + check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2 + check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3 + check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4 check_payload(op_binary, 2 ** 20, chopsize=1024) # 9_6_5 check_payload(op_binary, 2 ** 20, chopsize=2048) # 9_6_6 diff --git a/test/test_perl_application.py b/test/test_perl_application.py index dfd8be6c..e906aaca 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -1,6 +1,7 @@ import re import pytest + from unit.applications.lang.perl import TestApplicationPerl diff --git a/test/test_php_application.py b/test/test_php_application.py index e73c67ba..66e2ef7d 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -1,13 +1,15 @@ import os import re import shutil +import signal import time -from subprocess import call import pytest + from unit.applications.lang.php import TestApplicationPHP from unit.option import option + class TestPHPApplication(TestApplicationPHP): prerequisites = {'modules': {'php': 'all'}} @@ -93,37 +95,39 @@ class TestPHPApplication(TestApplicationPHP): assert resp['status'] == 200, 'query string empty status' assert resp['headers']['Query-String'] == '', 'query string empty' - def test_php_application_fastcgi_finish_request(self, temp_dir): + def test_php_application_fastcgi_finish_request(self, unit_pid): self.load('fastcgi_finish_request') - assert self.get()['body'] == '0123' + assert 'success' in self.conf( + {"admin": {"auto_globals_jit": "1"}}, + 'applications/fastcgi_finish_request/options', + ) - with open(temp_dir + '/unit.pid', 'r') as f: - pid = f.read().rstrip() + assert self.get()['body'] == '0123' - call(['kill', '-s', 'USR1', pid]) + os.kill(unit_pid, signal.SIGUSR1) - with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: - errs = re.findall(r'Error in fastcgi_finish_request', f.read()) + errs = self.findall(r'Error in fastcgi_finish_request') - assert len(errs) == 0, 'no error' + assert len(errs) == 0, 'no error' - def test_php_application_fastcgi_finish_request_2(self, temp_dir): + def test_php_application_fastcgi_finish_request_2(self, unit_pid): self.load('fastcgi_finish_request') + assert 'success' in self.conf( + {"admin": {"auto_globals_jit": "1"}}, + 'applications/fastcgi_finish_request/options', + ) + resp = self.get(url='/?skip') assert resp['status'] == 200 assert resp['body'] == '' - with open(temp_dir + '/unit.pid', 'r') as f: - pid = f.read().rstrip() - - call(['kill', '-s', 'USR1', pid]) + os.kill(unit_pid, signal.SIGUSR1) - with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: - errs = re.findall(r'Error in fastcgi_finish_request', f.read()) + errs = self.findall(r'Error in fastcgi_finish_request') - assert len(errs) == 0, 'no error' + assert len(errs) == 0, 'no error' def test_php_application_query_string_absent(self): self.load('query_string') @@ -428,11 +432,13 @@ class TestPHPApplication(TestApplicationPHP): self.load('auth') def check_auth(auth): - resp = self.get(headers={ - 'Host': 'localhost', - 'Authorization': auth, - 'Connection': 'close', - }) + resp = self.get( + headers={ + 'Host': 'localhost', + 'Authorization': auth, + 'Connection': 'close', + } + ) assert resp['status'] == 200, 'status' assert resp['headers']['X-Digest'] == 'not set', 'Digest' @@ -534,7 +540,7 @@ class TestPHPApplication(TestApplicationPHP): r'012345', self.get()['body'] ), 'disable_classes before' - def test_php_application_error_log(self, temp_dir): + def test_php_application_error_log(self): self.load('error_log') assert self.get()['status'] == 200, 'status' @@ -547,14 +553,13 @@ class TestPHPApplication(TestApplicationPHP): assert self.wait_for_record(pattern) is not None, 'errors print' - with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: - errs = re.findall(pattern, f.read()) + errs = self.findall(pattern) - assert len(errs) == 2, 'error_log count' + assert len(errs) == 2, 'error_log count' - date = errs[0].split('[')[0] - date2 = errs[1].split('[')[0] - assert date != date2, 'date diff' + date = errs[0].split('[')[0] + date2 = errs[1].split('[')[0] + assert date != date2, 'date diff' def test_php_application_script(self): assert 'success' in self.conf( diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index 2e458023..8db6b590 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -1,4 +1,5 @@ import pytest + from unit.applications.lang.php import TestApplicationPHP from unit.option import option @@ -28,7 +29,7 @@ class TestPHPIsolation(TestApplicationPHP): isolation['namespaces'] = { 'mount': True, 'credential': True, - 'pid': True + 'pid': True, } self.load('phpinfo', isolation=isolation) @@ -64,7 +65,7 @@ class TestPHPIsolation(TestApplicationPHP): isolation['namespaces'] = { 'mount': True, 'credential': True, - 'pid': True + 'pid': True, } self.load('list-extensions', isolation=isolation) diff --git a/test/test_proxy.py b/test/test_proxy.py index 7e7c7246..3d59cf24 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -3,6 +3,7 @@ import socket import time import pytest + from conftest import run_process from unit.applications.lang.python import TestApplicationPython from unit.option import option @@ -464,9 +465,9 @@ Content-Length: 10 def test_proxy_invalid(self): def check_proxy(proxy): - assert 'error' in \ - self.conf([{"action": {"proxy": proxy}}], 'routes'), \ - 'proxy invalid' + assert 'error' in self.conf( + [{"action": {"proxy": proxy}}], 'routes' + ), 'proxy invalid' check_proxy('blah') check_proxy('/blah') @@ -502,7 +503,8 @@ Content-Length: 10 "type": "python", "processes": {"spare": 0}, "path": option.test_dir + "/python/mirror", - "working_directory": option.test_dir + "/python/mirror", + "working_directory": option.test_dir + + "/python/mirror", "module": "wsgi", }, }, diff --git a/test/test_python_application.py b/test/test_python_application.py index 41f6f538..48c3d603 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -5,17 +5,13 @@ import re import time import pytest + from unit.applications.lang.python import TestApplicationPython -from unit.option import option class TestPythonApplication(TestApplicationPython): prerequisites = {'modules': {'python': 'all'}} - def findall(self, pattern): - with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f: - return re.findall(pattern, f.read()) - def test_python_application_variables(self): self.load('variables') @@ -31,7 +27,8 @@ Content-Type: text/html Connection: close custom-header: BLAH -%s""" % (len(body), body.encode()), +%s""" + % (len(body), body.encode()), raw=True, ) @@ -816,7 +813,11 @@ last line: 987654321 assert ['/new', *sys_path] == get_path(), 'check path update' set_path('["/blah1", "/blah2"]') - assert ['/blah1', '/blah2', *sys_path] == get_path(), 'check path array' + assert [ + '/blah1', + '/blah2', + *sys_path, + ] == get_path(), 'check path array' def test_python_application_path_invalid(self): self.load('path') diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 680f2c03..93f85264 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -1,5 +1,5 @@ - import pytest + from unit.applications.lang.python import TestApplicationPython from unit.option import option from unit.utils import findmnt @@ -32,7 +32,7 @@ class TestPythonIsolation(TestApplicationPython): isolation['namespaces'] = { 'mount': True, 'credential': True, - 'pid': True + 'pid': True, } self.load('ns_inspect', isolation=isolation) @@ -43,8 +43,7 @@ class TestPythonIsolation(TestApplicationPython): ), 'temp_dir does not exists in rootfs' assert ( - self.getjson(url='/?path=/proc/self')['body']['FileExists'] - == True + self.getjson(url='/?path=/proc/self')['body']['FileExists'] == True ), 'no /proc/self' assert ( @@ -66,15 +65,12 @@ class TestPythonIsolation(TestApplicationPython): if not is_su: pytest.skip('requires root') - isolation = { - 'rootfs': temp_dir, - 'automount': {'language_deps': False} - } + isolation = {'rootfs': temp_dir, 'automount': {'language_deps': False}} self.load('empty', isolation=isolation) assert findmnt().find(temp_dir) == -1 - assert (self.get()['status'] != 200), 'disabled language_deps' + assert self.get()['status'] != 200, 'disabled language_deps' assert findmnt().find(temp_dir) == -1 isolation['automount']['language_deps'] = True @@ -82,7 +78,7 @@ class TestPythonIsolation(TestApplicationPython): self.load('empty', isolation=isolation) assert findmnt().find(temp_dir) == -1 - assert (self.get()['status'] == 200), 'enabled language_deps' + assert self.get()['status'] == 200, 'enabled language_deps' assert waitformount(temp_dir), 'language_deps mount' self.conf({"listeners": {}, "applications": {}}) @@ -90,8 +86,6 @@ class TestPythonIsolation(TestApplicationPython): assert waitforunmount(temp_dir), 'language_deps unmount' def test_python_isolation_procfs(self, is_su, temp_dir): - isolation_features = option.available['features']['isolation'].keys() - if not is_su: pytest.skip('requires root') diff --git a/test/test_python_isolation_chroot.py b/test/test_python_isolation_chroot.py index 7281f5f6..54fbad12 100644 --- a/test/test_python_isolation_chroot.py +++ b/test/test_python_isolation_chroot.py @@ -1,4 +1,5 @@ import pytest + from unit.applications.lang.python import TestApplicationPython @@ -21,8 +22,7 @@ class TestPythonIsolation(TestApplicationPython): ), 'temp_dir does not exists in rootfs' assert ( - self.getjson(url='/?path=/proc/self')['body']['FileExists'] - == True + self.getjson(url='/?path=/proc/self')['body']['FileExists'] == True ), 'no /proc/self' assert ( diff --git a/test/test_python_procman.py b/test/test_python_procman.py index ac403ce4..b0d0f5af 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -3,6 +3,7 @@ import subprocess import time import pytest + from unit.applications.lang.python import TestApplicationPython from unit.option import option diff --git a/test/test_python_targets.py b/test/test_python_targets.py new file mode 100644 index 00000000..ca736c0d --- /dev/null +++ b/test/test_python_targets.py @@ -0,0 +1,51 @@ +import pytest + +from unit.applications.lang.python import TestApplicationPython +from unit.option import option + + +class TestPythonTargets(TestApplicationPython): + prerequisites = {'modules': {'python': 'all'}} + + def test_python_targets(self): + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "match": {"uri": "/1"}, + "action": {"pass": "applications/targets/1"}, + }, + { + "match": {"uri": "/2"}, + "action": {"pass": "applications/targets/2"}, + }, + ], + "applications": { + "targets": { + "type": "python", + "working_directory": option.test_dir + + "/python/targets/", + "path": option.test_dir + '/python/targets/', + "targets": { + "1": { + "module": "wsgi", + "callable": "wsgi_target_a", + }, + "2": { + "module": "wsgi", + "callable": "wsgi_target_b", + }, + }, + } + }, + } + ) + + resp = self.get(url='/1') + assert resp['status'] == 200 + assert resp['body'] == '1' + + resp = self.get(url='/2') + assert resp['status'] == 200 + assert resp['body'] == '2' diff --git a/test/test_respawn.py b/test/test_respawn.py index ed85ee95..edbfa2a8 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -58,7 +58,8 @@ class TestRespawn(TestApplicationPython): assert len(self.find_proc(self.PATTERN_CONTROLLER, unit_pid, out)) == 1 assert len(self.find_proc(self.app_name, unit_pid, out)) == 1 - def test_respawn_router(self, skip_alert, unit_pid): + def test_respawn_router(self, skip_alert, unit_pid, skip_fds_check): + skip_fds_check(router=True) pid = self.pid_by_name(self.PATTERN_ROUTER, unit_pid) self.kill_pids(pid) @@ -68,15 +69,17 @@ class TestRespawn(TestApplicationPython): self.smoke_test(unit_pid) - def test_respawn_controller(self, skip_alert, unit_pid): + def test_respawn_controller(self, skip_alert, unit_pid, skip_fds_check): + skip_fds_check(controller=True) pid = self.pid_by_name(self.PATTERN_CONTROLLER, unit_pid) self.kill_pids(pid) skip_alert(r'process %s exited on signal 9' % pid) - assert self.wait_for_process( - self.PATTERN_CONTROLLER, unit_pid - ) is not None + assert ( + self.wait_for_process(self.PATTERN_CONTROLLER, unit_pid) + is not None + ) assert self.get()['status'] == 200 diff --git a/test/test_routing.py b/test/test_routing.py index be9a1faf..eaa0a134 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import pytest + from unit.applications.proto import TestApplicationProto from unit.option import option @@ -232,11 +233,11 @@ class TestRouting(TestApplicationProto): if not option.available['modules']['regex']: pytest.skip('requires regex') - self.route_match({"uri":"~"}) + self.route_match({"uri": "~"}) assert self.get(url='/')['status'] == 200, 'empty regexp' assert self.get(url='/anything')['status'] == 200, '/anything' - self.route_match({"uri":"!~"}) + self.route_match({"uri": "!~"}) assert self.get(url='/')['status'] == 404, 'empty regexp 2' assert self.get(url='/nothing')['status'] == 404, '/nothing' @@ -336,8 +337,7 @@ class TestRouting(TestApplicationProto): "type": "python", "processes": {"spare": 0}, "path": option.test_dir + '/python/empty', - "working_directory": option.test_dir - + '/python/empty', + "working_directory": option.test_dir + '/python/empty', "module": "wsgi", } }, @@ -495,8 +495,7 @@ class TestRouting(TestApplicationProto): 'routes/0/action', ), 'proxy pass' assert 'error' in self.conf( - {"share": temp_dir, "pass": "applications/app"}, - 'routes/0/action', + {"share": temp_dir, "pass": "applications/app"}, 'routes/0/action', ), 'share pass' def test_routes_rules_two(self): @@ -1042,83 +1041,31 @@ class TestRouting(TestApplicationProto): } ) + def check_headers(hds): + hds = dict({"Host": "localhost", "Connection": "close"}, **hds) + assert ( + self.get(headers=hds)['status'] == 200 + ), 'headers array match' + + def check_headers_404(hds): + hds = dict({"Host": "localhost", "Connection": "close"}, **hds) + assert ( + self.get(headers=hds)['status'] == 404 + ), 'headers array no match' + assert self.get()['status'] == 404, 'match headers array' - assert ( - self.get( - headers={ - "Host": "localhost", - "x-header1": "foo123", - "Connection": "close", - } - )['status'] - == 200 - ), 'match headers array 2' - assert ( - self.get( - headers={ - "Host": "localhost", - "x-header2": "bar", - "Connection": "close", - } - )['status'] - == 200 - ), 'match headers array 3' - assert ( - self.get( - headers={ - "Host": "localhost", - "x-header3": "bar", - "Connection": "close", - } - )['status'] - == 200 - ), 'match headers array 4' - assert ( - self.get( - headers={ - "Host": "localhost", - "x-header1": "bar", - "Connection": "close", - } - )['status'] - == 404 - ), 'match headers array 5' - assert ( - self.get( - headers={ - "Host": "localhost", - "x-header1": "bar", - "x-header4": "foo", - "Connection": "close", - } - )['status'] - == 200 - ), 'match headers array 6' + check_headers({"x-header1": "foo123"}) + check_headers({"x-header2": "bar"}) + check_headers({"x-header3": "bar"}) + check_headers_404({"x-header1": "bar"}) + check_headers({"x-header1": "bar", "x-header4": "foo"}) assert 'success' in self.conf_delete( 'routes/0/match/headers/1' ), 'match headers array configure 2' - assert ( - self.get( - headers={ - "Host": "localhost", - "x-header2": "bar", - "Connection": "close", - } - )['status'] - == 404 - ), 'match headers array 7' - assert ( - self.get( - headers={ - "Host": "localhost", - "x-header3": "foo", - "Connection": "close", - } - )['status'] - == 200 - ), 'match headers array 8' + check_headers_404({"x-header2": "bar"}) + check_headers({"x-header3": "foo"}) def test_routes_match_arguments(self): self.route_match({"arguments": {"foo": "bar"}}) @@ -1362,10 +1309,7 @@ class TestRouting(TestApplicationProto): assert self.get(url='/?var2=val2')['status'] == 404, 'arr 7' assert self.get(url='/?var3=foo')['status'] == 200, 'arr 8' - def test_routes_match_arguments_invalid(self, skip_alert): - # TODO remove it after controller fixed - skip_alert(r'failed to apply new conf') - + def test_routes_match_arguments_invalid(self): self.route_match_invalid({"arguments": ["var"]}) self.route_match_invalid({"arguments": [{"var1": {}}]}) self.route_match_invalid({"arguments": {"": "bar"}}) diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index b18a6cee..ddd31f59 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -1,6 +1,8 @@ import re +import subprocess import pytest + from unit.applications.lang.ruby import TestApplicationRuby @@ -207,7 +209,6 @@ class TestRubyApplication(TestApplicationRuby): self.get() - assert ( self.wait_for_record(r'\[error\].+1234567890') is not None ), 'errors write int' @@ -223,6 +224,57 @@ class TestRubyApplication(TestApplicationRuby): self.wait_for_record(r'\[error\].+At exit called\.') is not None ), 'at exit' + def test_ruby_application_encoding(self): + self.load('encoding') + + try: + locales = ( + subprocess.check_output( + ['locale', '-a'], stderr=subprocess.STDOUT, + ) + .decode() + .split('\n') + ) + + except (FileNotFoundError, subprocess.CalledProcessError): + pytest.skip('require locale') + + def get_locale(pattern): + return next( + ( + l + for l in locales + if re.match(pattern, l.upper()) is not None + ), + None, + ) + + utf8 = get_locale(r'.*UTF[-_]?8') + iso88591 = get_locale(r'.*ISO[-_]?8859[-_]?1') + + def check_locale(enc): + assert 'success' in self.conf( + {"LC_CTYPE": enc, "LC_ALL": ""}, + '/config/applications/encoding/environment', + ) + + resp = self.get() + assert resp['status'] == 200, 'status' + + enc_default = re.sub(r'[-_]', '', resp['headers']['X-Enc']).upper() + assert ( + enc_default == re.sub(r'[-_]', '', enc.split('.')[-1]).upper() + ) + + if utf8: + check_locale(utf8) + + if iso88591: + check_locale(iso88591) + + if not utf8 and not iso88591: + pytest.skip('no available locales') + def test_ruby_application_header_custom(self): self.load('header_custom') diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index c49b6732..8443d857 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -2,6 +2,7 @@ import os import shutil import pytest + from unit.applications.lang.ruby import TestApplicationRuby from unit.option import option diff --git a/test/test_settings.py b/test/test_settings.py index c59ca8b9..49041b62 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -3,7 +3,9 @@ import socket import time import pytest + from unit.applications.lang.python import TestApplicationPython +from unit.utils import sysctl class TestSettings(TestApplicationPython): @@ -12,33 +14,42 @@ class TestSettings(TestApplicationPython): def test_settings_header_read_timeout(self): self.load('empty') - self.conf({'http': {'header_read_timeout': 2}}, 'settings') - - (resp, sock) = self.http( - b"""GET / HTTP/1.1 + def req(): + (resp, sock) = self.http( + b"""GET / HTTP/1.1 """, - start=True, - read_timeout=1, - raw=True, - ) + start=True, + read_timeout=1, + raw=True, + ) - time.sleep(3) + time.sleep(3) - resp = self.http( - b"""Host: localhost + return self.http( + b"""Host: localhost Connection: close -""", - sock=sock, - raw=True, + """, + sock=sock, + raw=True, + ) + + assert 'success' in self.conf( + {'http': {'header_read_timeout': 2}}, 'settings' ) + assert req()['status'] == 408, 'status header read timeout' - assert resp['status'] == 408, 'status header read timeout' + assert 'success' in self.conf( + {'http': {'header_read_timeout': 7}}, 'settings' + ) + assert req()['status'] == 200, 'status header read timeout 2' def test_settings_header_read_timeout_update(self): self.load('empty') - self.conf({'http': {'header_read_timeout': 4}}, 'settings') + assert 'success' in self.conf( + {'http': {'header_read_timeout': 4}}, 'settings' + ) (resp, sock) = self.http( b"""GET / HTTP/1.1 @@ -89,31 +100,40 @@ Connection: close def test_settings_body_read_timeout(self): self.load('empty') - self.conf({'http': {'body_read_timeout': 2}}, 'settings') - - (resp, sock) = self.http( - b"""POST / HTTP/1.1 + def req(): + (resp, sock) = self.http( + b"""POST / HTTP/1.1 Host: localhost Content-Length: 10 Connection: close """, - start=True, - raw_resp=True, - read_timeout=1, - raw=True, - ) + start=True, + raw_resp=True, + read_timeout=1, + raw=True, + ) - time.sleep(3) + time.sleep(3) - resp = self.http(b"""0123456789""", sock=sock, raw=True) + return self.http(b"""0123456789""", sock=sock, raw=True) - assert resp['status'] == 408, 'status body read timeout' + assert 'success' in self.conf( + {'http': {'body_read_timeout': 2}}, 'settings' + ) + assert req()['status'] == 408, 'status body read timeout' + + assert 'success' in self.conf( + {'http': {'body_read_timeout': 7}}, 'settings' + ) + assert req()['status'] == 200, 'status body read timeout 2' def test_settings_body_read_timeout_update(self): self.load('empty') - self.conf({'http': {'body_read_timeout': 4}}, 'settings') + assert 'success' in self.conf( + {'http': {'body_read_timeout': 4}}, 'settings' + ) (resp, sock) = self.http( b"""POST / HTTP/1.1 @@ -146,85 +166,116 @@ Connection: close assert resp['status'] == 200, 'status body read timeout update' def test_settings_send_timeout(self, temp_dir): - self.load('mirror') + self.load('body_generate') - data_len = 1048576 + def req(addr, data_len): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(addr) - self.conf({'http': {'send_timeout': 1}}, 'settings') + req = ( + """GET / HTTP/1.1 +Host: localhost +X-Length: %d +Connection: close - addr = temp_dir + '/sock' +""" + % data_len + ) - self.conf({"unix:" + addr: {'application': 'mirror'}}, 'listeners') + sock.sendall(req.encode()) - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(addr) + data = sock.recv(16).decode() - req = """POST / HTTP/1.1 -Host: localhost -Content-Type: text/html -Content-Length: %d -Connection: close + time.sleep(3) -""" % data_len + ( - 'X' * data_len + data += self.recvall(sock).decode() + + sock.close() + + return data + + sysctl_out = sysctl() + values = re.findall( + r'net.core.[rw]mem_(?:max|default).*?(\d+)', sysctl_out ) + values = [int(v) for v in values] - sock.sendall(req.encode()) + data_len = 1048576 if len(values) == 0 else 10 * max(values) - data = sock.recv(16).decode() + addr = temp_dir + '/sock' - time.sleep(3) + assert 'success' in self.conf( + {"unix:" + addr: {'application': 'body_generate'}}, 'listeners' + ) - data += self.recvall(sock).decode() + assert 'success' in self.conf( + {'http': {'send_timeout': 1}}, 'settings' + ) - sock.close() + data = req(addr, data_len) + assert re.search(r'200 OK', data), 'send timeout status' + assert len(data) < data_len, 'send timeout data ' - assert re.search(r'200 OK', data), 'status send timeout' - assert len(data) < data_len, 'data send timeout' + self.conf({'http': {'send_timeout': 7}}, 'settings') + + data = req(addr, data_len) + assert re.search(r'200 OK', data), 'send timeout status 2' + assert len(data) > data_len, 'send timeout data 2' def test_settings_idle_timeout(self): self.load('empty') - assert self.get()['status'] == 200, 'init' + def req(): + (resp, sock) = self.get( + headers={'Host': 'localhost', 'Connection': 'keep-alive'}, + start=True, + read_timeout=1, + ) - self.conf({'http': {'idle_timeout': 2}}, 'settings') + time.sleep(3) - (resp, sock) = self.get( - headers={'Host': 'localhost', 'Connection': 'keep-alive'}, - start=True, - read_timeout=1, - ) + return self.get(sock=sock) - time.sleep(3) + assert self.get()['status'] == 200, 'init' - resp = self.get( - headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock + assert 'success' in self.conf( + {'http': {'idle_timeout': 2}}, 'settings' ) + assert req()['status'] == 408, 'status idle timeout' - assert resp['status'] == 408, 'status idle timeout' + assert 'success' in self.conf( + {'http': {'idle_timeout': 7}}, 'settings' + ) + assert req()['status'] == 200, 'status idle timeout 2' def test_settings_idle_timeout_2(self): self.load('empty') - assert self.get()['status'] == 200, 'init' + def req(): + _, sock = self.http(b'', start=True, raw=True, no_recv=True) - self.conf({'http': {'idle_timeout': 1}}, 'settings') + time.sleep(3) - _, sock = self.http(b'', start=True, raw=True, no_recv=True) + return self.get(sock=sock) - time.sleep(3) + assert self.get()['status'] == 200, 'init' - assert ( - self.get( - headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock - )['status'] - == 408 - ), 'status idle timeout' + assert 'success' in self.conf( + {'http': {'idle_timeout': 1}}, 'settings' + ) + assert req()['status'] == 408, 'status idle timeout' + + assert 'success' in self.conf( + {'http': {'idle_timeout': 7}}, 'settings' + ) + assert req()['status'] == 200, 'status idle timeout 2' def test_settings_max_body_size(self): self.load('empty') - self.conf({'http': {'max_body_size': 5}}, 'settings') + assert 'success' in self.conf( + {'http': {'max_body_size': 5}}, 'settings' + ) assert self.post(body='01234')['status'] == 200, 'status size' assert self.post(body='012345')['status'] == 413, 'status size max' @@ -232,7 +283,9 @@ Connection: close def test_settings_max_body_size_large(self): self.load('mirror') - self.conf({'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings') + assert 'success' in self.conf( + {'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings' + ) body = '0123456789abcdef' * 4 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) diff --git a/test/test_share_chroot.py b/test/test_share_chroot.py new file mode 100644 index 00000000..7e53d3f7 --- /dev/null +++ b/test/test_share_chroot.py @@ -0,0 +1,108 @@ +import os +from pathlib import Path + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestShareChroot(TestApplicationProto): + prerequisites = {'features': ['chroot']} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, temp_dir): + os.makedirs(temp_dir + '/assets/dir') + with open(temp_dir + '/assets/index.html', 'w') as index, open( + temp_dir + '/assets/dir/file', 'w' + ) as file: + index.write('0123456789') + file.write('blah') + + test = Path(__file__) + self.test_path = '/' + test.parent.name + '/' + test.name + + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + } + ) + + def test_share_chroot(self, temp_dir): + assert self.get(url='/dir/file')['status'] == 200, 'default chroot' + assert self.get(url='/index.html')['status'] == 200, 'default chroot 2' + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets", + "chroot": temp_dir + "/assets/dir", + }, + 'routes/0/action', + ), 'configure chroot' + + assert self.get(url='/dir/file')['status'] == 200, 'chroot' + assert self.get(url='/index.html')['status'] == 403, 'chroot 403 2' + assert self.get(url='/file')['status'] == 403, 'chroot 403' + + def test_share_chroot_permission(self, temp_dir): + os.chmod(temp_dir + '/assets/dir', 0o100) + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets", + "chroot": temp_dir + "/assets/dir", + }, + 'routes/0/action', + ), 'configure chroot' + + assert self.get(url='/dir/file')['status'] == 200, 'chroot' + + def test_share_chroot_empty(self, temp_dir): + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "chroot": ""}, 'routes/0/action', + ), 'configure chroot empty absolute' + + assert ( + self.get(url='/dir/file')['status'] == 200 + ), 'chroot empty absolute' + + assert 'success' in self.conf( + {"share": ".", "chroot": ""}, 'routes/0/action', + ), 'configure chroot empty relative' + + assert ( + self.get(url=self.test_path)['status'] == 200 + ), 'chroot empty relative' + + def test_share_chroot_relative(self, is_su, temp_dir): + if is_su: + pytest.skip('does\'t work under root') + + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "chroot": "."}, 'routes/0/action', + ), 'configure relative chroot' + + assert self.get(url='/dir/file')['status'] == 403, 'relative chroot' + + assert 'success' in self.conf( + {"share": "."}, 'routes/0/action', + ), 'configure relative share' + + assert self.get(url=self.test_path)['status'] == 200, 'relative share' + + assert 'success' in self.conf( + {"share": ".", "chroot": "."}, 'routes/0/action', + ), 'configure relative' + + assert self.get(url=self.test_path)['status'] == 200, 'relative' + + def test_share_chroot_invalid(self, temp_dir): + assert 'error' in self.conf( + {"share": temp_dir, "chroot": True}, 'routes/0/action', + ), 'configure chroot error' + assert 'error' in self.conf( + {"share": temp_dir, "symlinks": "True"}, 'routes/0/action', + ), 'configure symlink error' + assert 'error' in self.conf( + {"share": temp_dir, "mount": "True"}, 'routes/0/action', + ), 'configure mount error' diff --git a/test/test_share_fallback.py b/test/test_share_fallback.py index 46464670..0b1c270e 100644 --- a/test/test_share_fallback.py +++ b/test/test_share_fallback.py @@ -1,6 +1,7 @@ import os import pytest + from unit.applications.proto import TestApplicationProto from unit.option import option @@ -81,10 +82,7 @@ class TestStatic(TestApplicationProto): def test_fallback_share(self, temp_dir): self.action_update( - { - "share": "/blah", - "fallback": {"share": temp_dir + "/assets"}, - } + {"share": "/blah", "fallback": {"share": temp_dir + "/assets"},} ) resp = self.get() diff --git a/test/test_share_mount.py b/test/test_share_mount.py new file mode 100644 index 00000000..f22fbe75 --- /dev/null +++ b/test/test_share_mount.py @@ -0,0 +1,142 @@ +import os +import subprocess + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestShareMount(TestApplicationProto): + prerequisites = {'features': ['chroot']} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, is_su, temp_dir): + if not is_su: + pytest.skip('requires root') + + os.makedirs(temp_dir + '/assets/dir/mount') + os.makedirs(temp_dir + '/assets/dir/dir') + os.makedirs(temp_dir + '/assets/mount') + with open(temp_dir + '/assets/index.html', 'w') as index, open( + temp_dir + '/assets/dir/dir/file', 'w' + ) as file, open(temp_dir + '/assets/mount/index.html', 'w') as mount: + index.write('index') + file.write('file') + mount.write('mount') + + try: + process = subprocess.Popen( + [ + "mount", + "--bind", + temp_dir + "/assets/mount", + temp_dir + "/assets/dir/mount", + ], + stderr=subprocess.STDOUT, + ) + + process.communicate() + + except KeyboardInterrupt: + raise + + except: + pytest.fail('Can\'t run mount process.') + + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets/dir"}}], + } + ) + + yield + + try: + process = subprocess.Popen( + ["umount", "--lazy", temp_dir + "/assets/dir/mount"], + stderr=subprocess.STDOUT, + ) + + process.communicate() + + except KeyboardInterrupt: + raise + + except: + pytest.fail('Can\'t run umount process.') + + def test_share_mount(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + resp = self.get(url='/mount/') + assert resp['status'] == 200 + assert resp['body'] == 'mount' + + assert 'success' in self.conf( + {"share": temp_dir + "/assets/dir", "traverse_mounts": False}, + 'routes/0/action', + ), 'configure mount disable' + + assert self.get(url='/mount/')['status'] == 403 + + assert 'success' in self.conf( + {"share": temp_dir + "/assets/dir", "traverse_mounts": True}, + 'routes/0/action', + ), 'configure mount enable' + + resp = self.get(url='/mount/') + assert resp['status'] == 200 + assert resp['body'] == 'mount' + + def test_share_mount_two_blocks(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') + + assert 'success' in self.conf( + [ + { + "match": {"method": "HEAD"}, + "action": { + "share": temp_dir + "/assets/dir", + "traverse_mounts": False, + }, + }, + { + "match": {"method": "GET"}, + "action": { + "share": temp_dir + "/assets/dir", + "traverse_mounts": True, + }, + }, + ], + 'routes', + ), 'configure two options' + + assert self.get(url='/mount/')['status'] == 200, 'block enabled' + assert self.head(url='/mount/')['status'] == 403, 'block disabled' + + def test_share_mount_chroot(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets/dir", + "chroot": temp_dir + "/assets", + }, + 'routes/0/action', + ), 'configure chroot mount default' + + assert self.get(url='/mount/')['status'] == 200, 'chroot' + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets/dir", + "chroot": temp_dir + "/assets", + "traverse_mounts": False, + }, + 'routes/0/action', + ), 'configure chroot mount disable' + + assert self.get(url='/mount/')['status'] == 403, 'chroot mount' diff --git a/test/test_share_symlink.py b/test/test_share_symlink.py new file mode 100644 index 00000000..3970b605 --- /dev/null +++ b/test/test_share_symlink.py @@ -0,0 +1,96 @@ +import os + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestShareSymlink(TestApplicationProto): + prerequisites = {'features': ['chroot']} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, temp_dir): + os.makedirs(temp_dir + '/assets/dir/dir') + with open(temp_dir + '/assets/index.html', 'w') as index, open( + temp_dir + '/assets/dir/file', 'w' + ) as file: + index.write('0123456789') + file.write('blah') + + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + } + ) + + def test_share_symlink(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') + + assert self.get(url='/dir')['status'] == 301, 'dir' + assert self.get(url='/dir/file')['status'] == 200, 'file' + assert self.get(url='/link')['status'] == 301, 'symlink dir' + assert self.get(url='/link/file')['status'] == 200, 'symlink file' + + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "follow_symlinks": False}, + 'routes/0/action', + ), 'configure symlink disable' + + assert self.get(url='/link/file')['status'] == 403, 'symlink disabled' + + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "follow_symlinks": True}, + 'routes/0/action', + ), 'configure symlink enable' + + assert self.get(url='/link/file')['status'] == 200, 'symlink enabled' + + def test_share_symlink_two_blocks(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') + + assert 'success' in self.conf( + [ + { + "match": {"method": "HEAD"}, + "action": { + "share": temp_dir + "/assets", + "follow_symlinks": False, + }, + }, + { + "match": {"method": "GET"}, + "action": { + "share": temp_dir + "/assets", + "follow_symlinks": True, + }, + }, + ], + 'routes', + ), 'configure two options' + + assert self.get(url='/link/file')['status'] == 200, 'block enabled' + assert self.head(url='/link/file')['status'] == 403, 'block disabled' + + def test_share_symlink_chroot(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink( + temp_dir + '/assets/dir/file', temp_dir + '/assets/dir/dir/link' + ) + + assert self.get(url='/dir/dir/link')['status'] == 200, 'default chroot' + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets", + "chroot": temp_dir + "/assets/dir/dir", + }, + 'routes/0/action', + ), 'configure chroot' + + assert self.get(url='/dir/dir/link')['status'] == 404, 'chroot' diff --git a/test/test_share_types.py b/test/test_share_types.py new file mode 100644 index 00000000..b5ed97a0 --- /dev/null +++ b/test/test_share_types.py @@ -0,0 +1,170 @@ +import os +from pathlib import Path + +import pytest +from unit.applications.proto import TestApplicationProto +from unit.option import option + + +class TestShareTypes(TestApplicationProto): + prerequisites = {} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, temp_dir): + Path(temp_dir + '/assets').mkdir() + for ext in ['.xml', '.mp4', '.php', '', '.txt', '.html', '.png']: + Path(temp_dir + '/assets/file' + ext).write_text(ext) + + Path(temp_dir + '/assets/index.html').write_text('index') + + self._load_conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "routes"}, + }, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + "applications": {}, + } + ) + + def action_update(self, conf): + assert 'success' in self.conf(conf, 'routes/0/action') + + def check_body(self, http_url, body): + resp = self.get(url=http_url) + assert resp['status'] == 200, 'status' + assert resp['body'] == body, 'body' + + def test_share_types_basic(self, temp_dir): + self.action_update({"share": temp_dir + "/assets"}) + self.check_body('/index.html', 'index') + self.check_body('/file.xml', '.xml') + + self.action_update( + {"share": temp_dir + "/assets", "types": "application/xml"} + ) + self.check_body('/file.xml', '.xml') + + self.action_update( + {"share": temp_dir + "/assets", "types": ["application/xml"]} + ) + self.check_body('/file.xml', '.xml') + + self.action_update({"share": temp_dir + "/assets", "types": [""]}) + assert self.get(url='/file.xml')['status'] == 403, 'no mtype' + + def test_share_types_wildcard(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["application/*"]} + ) + self.check_body('/file.xml', '.xml') + assert self.get(url='/file.mp4')['status'] == 403, 'app * mtype mp4' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["video/*"]} + ) + assert self.get(url='/file.xml')['status'] == 403, 'video * mtype xml' + self.check_body('/file.mp4', '.mp4') + + def test_share_types_negation(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["!application/xml"]} + ) + assert self.get(url='/file.xml')['status'] == 403, 'forbidden negation' + self.check_body('/file.mp4', '.mp4') + + # sorting negation + self.action_update( + { + "share": temp_dir + "/assets", + "types": ["!video/*", "image/png", "!image/jpg"], + } + ) + assert self.get(url='/file.mp4')['status'] == 403, 'negation sort mp4' + self.check_body('/file.png', '.png') + assert self.get(url='/file.jpg')['status'] == 403, 'negation sort jpg' + + def test_share_types_regex(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["~text/(html|plain)"]} + ) + assert self.get(url='/file.php')['status'] == 403, 'regex fail' + self.check_body('/file.html', '.html') + self.check_body('/file.txt', '.txt') + + def test_share_types_case(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["!APpliCaTiOn/xMl"]} + ) + self.check_body('/file.mp4', '.mp4') + assert ( + self.get(url='/file.xml')['status'] == 403 + ), 'mixed case xml negation' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["vIdEo/mp4"]} + ) + assert self.get(url='/file.mp4')['status'] == 200, 'mixed case' + assert ( + self.get(url='/file.xml')['status'] == 403 + ), 'mixed case video negation' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["vIdEo/*"]} + ) + self.check_body('/file.mp4', '.mp4') + assert ( + self.get(url='/file.xml')['status'] == 403 + ), 'mixed case video * negation' + + def test_share_types_fallback(self, temp_dir): + assert 'success' in self.conf( + [ + { + "match": {"destination": "*:7081"}, + "action": {"return": 200}, + }, + { + "action": { + "share": temp_dir + "/assets", + "types": ["!application/x-httpd-php"], + "fallback": {"proxy": "http://127.0.0.1:7081"}, + } + }, + ], + 'routes', + ), 'configure fallback proxy route' + + self.check_body('/file.php', '') + self.check_body('/file.mp4', '.mp4') + + def test_share_types_index(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": "application/xml"} + ) + self.check_body('/', 'index') + self.check_body('/file.xml', '.xml') + assert self.get(url='/file.mp4')['status'] == 403, 'forbidden mtype' + + def test_share_types_custom_mime(self, temp_dir): + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + "applications": {}, + "settings": { + "http": { + "static": {"mime_types": {"test/mime-type": ["file"]}} + } + }, + } + ) + + self.action_update({"share": temp_dir + "/assets", "types": [""]}) + assert self.get(url='/file')['status'] == 403, 'forbidden custom mime' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["test/mime-type"]} + ) + self.check_body('/file', '') diff --git a/test/test_static.py b/test/test_static.py index 3e85b435..669e265d 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -2,6 +2,7 @@ import os import socket import pytest + from unit.applications.proto import TestApplicationProto from unit.option import option from unit.utils import waitforfiles @@ -85,8 +86,7 @@ class TestStatic(TestApplicationProto): def test_static_space_in_name(self, temp_dir): os.rename( - temp_dir + '/assets/dir/file', - temp_dir + '/assets/dir/fi le', + temp_dir + '/assets/dir/file', temp_dir + '/assets/dir/fi le', ) assert waitforfiles(temp_dir + '/assets/dir/fi le') assert self.get(url='/dir/fi le')['body'] == 'blah', 'file name' @@ -95,9 +95,7 @@ class TestStatic(TestApplicationProto): assert waitforfiles(temp_dir + '/assets/di r/fi le') assert self.get(url='/di r/fi le')['body'] == 'blah', 'dir name' - os.rename( - temp_dir + '/assets/di r', temp_dir + '/assets/ di r ' - ) + os.rename(temp_dir + '/assets/di r', temp_dir + '/assets/ di r ') assert waitforfiles(temp_dir + '/assets/ di r /fi le') assert ( self.get(url='/ di r /fi le')['body'] == 'blah' @@ -150,8 +148,7 @@ class TestStatic(TestApplicationProto): ), 'file name 2' os.rename( - temp_dir + '/assets/ di r ', - temp_dir + '/assets/ди ректория', + temp_dir + '/assets/ di r ', temp_dir + '/assets/ди ректория', ) assert waitforfiles(temp_dir + '/assets/ди ректория/фа йл') assert ( @@ -171,14 +168,6 @@ class TestStatic(TestApplicationProto): assert self.get(url='/fifo')['status'] == 404, 'fifo' - def test_static_symlink(self, temp_dir): - os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') - - assert self.get(url='/dir')['status'] == 301, 'dir' - assert self.get(url='/dir/file')['status'] == 200, 'file' - assert self.get(url='/link')['status'] == 301, 'symlink dir' - assert self.get(url='/link/file')['status'] == 200, 'symlink file' - def test_static_method(self): resp = self.head() assert resp['status'] == 200, 'HEAD status' diff --git a/test/test_tls.py b/test/test_tls.py index 206ea828..0cfeaded 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -2,8 +2,10 @@ import io import re import ssl import subprocess +import time import pytest + from unit.applications.tls import TestApplicationTLS from unit.option import option @@ -11,10 +13,6 @@ from unit.option import option class TestTLS(TestApplicationTLS): prerequisites = {'modules': {'python': 'any', 'openssl': 'any'}} - def findall(self, pattern): - with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f: - return re.findall(pattern, f.read()) - def openssl_date_to_sec_epoch(self, date): return self.date_to_sec_epoch(date, '%b %d %H:%M:%S %Y %Z') @@ -22,7 +20,7 @@ class TestTLS(TestApplicationTLS): assert 'success' in self.conf( { "pass": "applications/" + application, - "tls": {"certificate": cert} + "tls": {"certificate": cert}, }, 'listeners/*:' + str(port), ) @@ -32,6 +30,91 @@ class TestTLS(TestApplicationTLS): {"pass": "applications/" + application}, 'listeners/*:' + str(port) ) + def req(self, name='localhost', subject=None, x509=False): + subj = subject if subject is not None else '/CN=' + name + '/' + + subprocess.call( + [ + 'openssl', + 'req', + '-new', + '-subj', + subj, + '-config', + option.temp_dir + '/openssl.conf', + '-out', + option.temp_dir + '/' + name + '.csr', + '-keyout', + option.temp_dir + '/' + name + '.key', + ], + stderr=subprocess.STDOUT, + ) + + def generate_ca_conf(self): + with open(option.temp_dir + '/ca.conf', 'w') as f: + f.write( + """[ ca ] +default_ca = myca + +[ myca ] +new_certs_dir = %(dir)s +database = %(database)s +default_md = sha256 +policy = myca_policy +serial = %(certserial)s +default_days = 1 +x509_extensions = myca_extensions +copy_extensions = copy + +[ myca_policy ] +commonName = optional + +[ myca_extensions ] +basicConstraints = critical,CA:TRUE""" + % { + 'dir': option.temp_dir, + 'database': option.temp_dir + '/certindex', + 'certserial': option.temp_dir + '/certserial', + } + ) + + with open(option.temp_dir + '/certserial', 'w') as f: + f.write('1000') + + with open(option.temp_dir + '/certindex', 'w') as f: + f.write('') + + with open(option.temp_dir + '/certindex.attr', 'w') as f: + f.write('') + + def ca(self, cert='root', out='localhost'): + subprocess.call( + [ + 'openssl', + 'ca', + '-batch', + '-config', + option.temp_dir + '/ca.conf', + '-keyfile', + option.temp_dir + '/' + cert + '.key', + '-cert', + option.temp_dir + '/' + cert + '.crt', + '-in', + option.temp_dir + '/' + out + '.csr', + '-out', + option.temp_dir + '/' + out + '.crt', + ], + stderr=subprocess.STDOUT, + ) + + def set_certificate_req_context(self, cert='root'): + self.context = ssl.create_default_context() + self.context.check_hostname = False + self.context.verify_mode = ssl.CERT_REQUIRED + self.context.load_verify_locations( + option.temp_dir + '/' + cert + '.crt' + ) + def test_tls_listener_option_add(self): self.load('empty') @@ -212,113 +295,13 @@ class TestTLS(TestApplicationTLS): self.certificate('root', False) - subprocess.call( - [ - 'openssl', - 'req', - '-new', - '-subj', - '/CN=int/', - '-config', - temp_dir + '/openssl.conf', - '-out', - temp_dir + '/int.csr', - '-keyout', - temp_dir + '/int.key', - ], - stderr=subprocess.STDOUT, - ) + self.req('int') + self.req('end') - subprocess.call( - [ - 'openssl', - 'req', - '-new', - '-subj', - '/CN=end/', - '-config', - temp_dir + '/openssl.conf', - '-out', - temp_dir + '/end.csr', - '-keyout', - temp_dir + '/end.key', - ], - stderr=subprocess.STDOUT, - ) + self.generate_ca_conf() - with open(temp_dir + '/ca.conf', 'w') as f: - f.write( - """[ ca ] -default_ca = myca - -[ myca ] -new_certs_dir = %(dir)s -database = %(database)s -default_md = sha256 -policy = myca_policy -serial = %(certserial)s -default_days = 1 -x509_extensions = myca_extensions - -[ myca_policy ] -commonName = supplied - -[ myca_extensions ] -basicConstraints = critical,CA:TRUE""" - % { - 'dir': temp_dir, - 'database': temp_dir + '/certindex', - 'certserial': temp_dir + '/certserial', - } - ) - - with open(temp_dir + '/certserial', 'w') as f: - f.write('1000') - - with open(temp_dir + '/certindex', 'w') as f: - f.write('') - - subprocess.call( - [ - 'openssl', - 'ca', - '-batch', - '-subj', - '/CN=int/', - '-config', - temp_dir + '/ca.conf', - '-keyfile', - temp_dir + '/root.key', - '-cert', - temp_dir + '/root.crt', - '-in', - temp_dir + '/int.csr', - '-out', - temp_dir + '/int.crt', - ], - stderr=subprocess.STDOUT, - ) - - subprocess.call( - [ - 'openssl', - 'ca', - '-batch', - '-subj', - '/CN=end/', - '-config', - temp_dir + '/ca.conf', - '-keyfile', - temp_dir + '/int.key', - '-cert', - temp_dir + '/int.crt', - '-in', - temp_dir + '/end.csr', - '-out', - temp_dir + '/end.crt', - ], - stderr=subprocess.STDOUT, - ) + self.ca(cert='root', out='int') + self.ca(cert='int', out='end') crt_path = temp_dir + '/end-int.crt' end_path = temp_dir + '/end.crt' @@ -329,10 +312,7 @@ basicConstraints = critical,CA:TRUE""" ) as int: crt.write(end.read() + int.read()) - self.context = ssl.create_default_context() - self.context.check_hostname = False - self.context.verify_mode = ssl.CERT_REQUIRED - self.context.load_verify_locations(temp_dir + '/root.crt') + self.set_certificate_req_context() # incomplete chain @@ -406,6 +386,67 @@ basicConstraints = critical,CA:TRUE""" self.get_ssl()['status'] == 200 ), 'certificate chain intermediate server' + def test_tls_certificate_empty_cn(self, temp_dir): + self.certificate('root', False) + + self.req(subject='/') + + self.generate_ca_conf() + self.ca() + + self.set_certificate_req_context() + + assert 'success' in self.certificate_load('localhost', 'localhost') + + cert = self.conf_get('/certificates/localhost') + assert cert['chain'][0]['subject'] == {}, 'empty subject' + assert cert['chain'][0]['issuer']['common_name'] == 'root', 'issuer' + + def test_tls_certificate_empty_cn_san(self, temp_dir): + self.certificate('root', False) + + self.openssl_conf( + rewrite=True, alt_names=["example.com", "www.example.net"] + ) + + self.req(subject='/') + + self.generate_ca_conf() + self.ca() + + self.set_certificate_req_context() + + assert 'success' in self.certificate_load('localhost', 'localhost') + + cert = self.conf_get('/certificates/localhost') + assert cert['chain'][0]['subject'] == { + 'alt_names': ['example.com', 'www.example.net'] + }, 'subject alt_names' + assert cert['chain'][0]['issuer']['common_name'] == 'root', 'issuer' + + def test_tls_certificate_empty_cn_san_ip(self): + self.certificate('root', False) + + self.openssl_conf( + rewrite=True, + alt_names=['example.com', 'www.example.net', 'IP|10.0.0.1'], + ) + + self.req(subject='/') + + self.generate_ca_conf() + self.ca() + + self.set_certificate_req_context() + + assert 'success' in self.certificate_load('localhost', 'localhost') + + cert = self.conf_get('/certificates/localhost') + assert cert['chain'][0]['subject'] == { + 'alt_names': ['example.com', 'www.example.net'] + }, 'subject alt_names' + assert cert['chain'][0]['issuer']['common_name'] == 'root', 'issuer' + @pytest.mark.skip('not yet') def test_tls_reconfigure(self): self.load('empty') @@ -461,6 +502,28 @@ basicConstraints = critical,CA:TRUE""" assert resp['body'] == '0123456789', 'keepalive 2' + def test_tls_no_close_notify(self): + self.certificate() + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": { + "pass": "routes", + "tls": {"certificate": "default"}, + } + }, + "routes": [{"action": {"return": 200}}], + "applications": {}, + } + ), 'load application configuration' + + (resp, sock) = self.get_ssl(start=True) + + time.sleep(5) + + sock.close() + @pytest.mark.skip('not yet') def test_tls_keepalive_certificate_remove(self): self.load('empty') diff --git a/test/test_tls_conf_command.py b/test/test_tls_conf_command.py new file mode 100644 index 00000000..ccae09ad --- /dev/null +++ b/test/test_tls_conf_command.py @@ -0,0 +1,112 @@ +import ssl + +import pytest + +from unit.applications.tls import TestApplicationTLS + + +class TestTLSConfCommand(TestApplicationTLS): + prerequisites = {'modules': {'openssl': 'any'}} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, request): + self.certificate() + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": { + "pass": "routes", + "tls": {"certificate": "default"}, + } + }, + "routes": [{"action": {"return": 200}}], + "applications": {}, + } + ), 'load application configuration' + + def test_tls_conf_command(self): + def check_no_connection(): + try: + self.get_ssl() + pytest.fail('Unexpected connection.') + + except (ssl.SSLError, ConnectionRefusedError): + pass + + # Set one conf_commands (disable protocol). + + (resp, sock) = self.get_ssl(start=True) + + shared_ciphers = sock.shared_ciphers() + protocols = list(set(c[1] for c in shared_ciphers)) + protocol = sock.cipher()[1] + + if '/' in protocol: + pytest.skip('Complex protocol format.') + + assert 'success' in self.conf( + { + "certificate": "default", + "conf_commands": {"protocol": '-' + protocol}, + }, + 'listeners/*:7080/tls', + ), 'protocol disabled' + + sock.close() + + if len(protocols) > 1: + (resp, sock) = self.get_ssl(start=True) + + cipher = sock.cipher() + assert cipher[1] != protocol, 'new protocol used' + + shared_ciphers = sock.shared_ciphers() + ciphers = list(set(c for c in shared_ciphers if c[1] == cipher[1])) + + sock.close() + else: + check_no_connection() + pytest.skip('One TLS protocol available only.') + + # Set two conf_commands (disable protocol and cipher). + + assert 'success' in self.conf( + { + "certificate": "default", + "conf_commands": { + "protocol": '-' + protocol, + "cipherstring": cipher[1] + ":!" + cipher[0], + }, + }, + 'listeners/*:7080/tls', + ), 'cipher disabled' + + if len(ciphers) > 1: + (resp, sock) = self.get_ssl(start=True) + + cipher_new = sock.cipher() + assert cipher_new[1] == cipher[1], 'previous protocol used' + assert cipher_new[0] != cipher[0], 'new cipher used' + + sock.close() + + else: + check_no_connection() + + def test_tls_conf_command_invalid(self, skip_alert): + skip_alert(r'SSL_CONF_cmd', r'failed to apply new conf') + + def check_conf_commands(conf_commands): + assert 'error' in self.conf( + {"certificate": "default", "conf_commands": conf_commands}, + 'listeners/*:7080/tls', + ), 'ivalid conf_commands' + + check_conf_commands([]) + check_conf_commands("blah") + check_conf_commands({"": ""}) + check_conf_commands({"blah": ""}) + check_conf_commands({"protocol": {}}) + check_conf_commands({"protocol": "blah"}) + check_conf_commands({"protocol": "TLSv1.2", "blah": ""}) diff --git a/test/test_tls_sni.py b/test/test_tls_sni.py new file mode 100644 index 00000000..2e5424e2 --- /dev/null +++ b/test/test_tls_sni.py @@ -0,0 +1,276 @@ +import ssl +import subprocess + +import pytest + +from unit.applications.tls import TestApplicationTLS +from unit.option import option + + +class TestTLSSNI(TestApplicationTLS): + prerequisites = {'modules': {'openssl': 'any'}} + + def setup_method(self): + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"return": 200}}], + "applications": {}, + } + ) + + def openssl_date_to_sec_epoch(self, date): + return self.date_to_sec_epoch(date, '%b %d %H:%M:%S %Y %Z') + + def add_tls(self, cert='default'): + assert 'success' in self.conf( + {"pass": "routes", "tls": {"certificate": cert}}, + 'listeners/*:7080', + ) + + def remove_tls(self): + assert 'success' in self.conf({"pass": "routes"}, 'listeners/*:7080') + + def generate_ca_conf(self): + with open(option.temp_dir + '/ca.conf', 'w') as f: + f.write( + """[ ca ] +default_ca = myca + +[ myca ] +new_certs_dir = %(dir)s +database = %(database)s +default_md = sha256 +policy = myca_policy +serial = %(certserial)s +default_days = 1 +x509_extensions = myca_extensions +copy_extensions = copy + +[ myca_policy ] +commonName = optional + +[ myca_extensions ] +basicConstraints = critical,CA:TRUE""" + % { + 'dir': option.temp_dir, + 'database': option.temp_dir + '/certindex', + 'certserial': option.temp_dir + '/certserial', + } + ) + + with open(option.temp_dir + '/certserial', 'w') as f: + f.write('1000') + + with open(option.temp_dir + '/certindex', 'w') as f: + f.write('') + + def config_bundles(self, bundles): + self.certificate('root', False) + + for b in bundles: + self.openssl_conf(rewrite=True, alt_names=bundles[b]['alt_names']) + subj = ( + '/CN={}/'.format(bundles[b]['subj']) + if 'subj' in bundles[b] + else '/' + ) + + subprocess.call( + [ + 'openssl', + 'req', + '-new', + '-subj', + subj, + '-config', + option.temp_dir + '/openssl.conf', + '-out', + option.temp_dir + '/{}.csr'.format(b), + '-keyout', + option.temp_dir + '/{}.key'.format(b), + ], + stderr=subprocess.STDOUT, + ) + + self.generate_ca_conf() + + for b in bundles: + subj = ( + '/CN={}/'.format(bundles[b]['subj']) + if 'subj' in bundles[b] + else '/' + ) + + subprocess.call( + [ + 'openssl', + 'ca', + '-batch', + '-subj', + subj, + '-config', + option.temp_dir + '/ca.conf', + '-keyfile', + option.temp_dir + '/root.key', + '-cert', + option.temp_dir + '/root.crt', + '-in', + option.temp_dir + '/{}.csr'.format(b), + '-out', + option.temp_dir + '/{}.crt'.format(b), + ], + stderr=subprocess.STDOUT, + ) + + self.context = ssl.create_default_context() + self.context.check_hostname = False + self.context.verify_mode = ssl.CERT_REQUIRED + self.context.load_verify_locations(option.temp_dir + '/root.crt') + + self.load_certs(bundles) + + def load_certs(self, bundles): + for bname, bvalue in bundles.items(): + assert 'success' in self.certificate_load( + bname, bname + ), 'certificate {} upload'.format(bvalue['subj']) + + def check_cert(self, host, expect): + resp, sock = self.get_ssl( + headers={ + 'Host': host, + 'Content-Length': '0', + 'Connection': 'close', + }, + start=True, + ) + + assert resp['status'] == 200 + assert sock.getpeercert()['subject'][0][0][1] == expect + + def test_tls_sni(self): + bundles = { + "default": {"subj": "default", "alt_names": ["default"]}, + "localhost.com": { + "subj": "localhost.com", + "alt_names": ["alt1.localhost.com"], + }, + "example.com": { + "subj": "example.com", + "alt_names": ["alt1.example.com", "alt2.example.com"], + }, + } + self.config_bundles(bundles) + self.add_tls(["default", "localhost.com", "example.com"]) + + self.check_cert('alt1.localhost.com', bundles['localhost.com']['subj']) + self.check_cert('alt2.example.com', bundles['example.com']['subj']) + self.check_cert('blah', bundles['default']['subj']) + + def test_tls_sni_upper_case(self): + bundles = { + "localhost.com": {"subj": "LOCALHOST.COM", "alt_names": []}, + "example.com": { + "subj": "example.com", + "alt_names": ["ALT1.EXAMPLE.COM", "*.ALT2.EXAMPLE.COM"], + }, + } + self.config_bundles(bundles) + self.add_tls(["localhost.com", "example.com"]) + + self.check_cert('localhost.com', bundles['localhost.com']['subj']) + self.check_cert('LOCALHOST.COM', bundles['localhost.com']['subj']) + self.check_cert('EXAMPLE.COM', bundles['localhost.com']['subj']) + self.check_cert('ALT1.EXAMPLE.COM', bundles['example.com']['subj']) + self.check_cert('WWW.ALT2.EXAMPLE.COM', bundles['example.com']['subj']) + + def test_tls_sni_only_bundle(self): + bundles = { + "localhost.com": { + "subj": "localhost.com", + "alt_names": ["alt1.localhost.com", "alt2.localhost.com"], + } + } + self.config_bundles(bundles) + self.add_tls(["localhost.com"]) + + self.check_cert('domain.com', bundles['localhost.com']['subj']) + self.check_cert('alt1.domain.com', bundles['localhost.com']['subj']) + + def test_tls_sni_wildcard(self): + bundles = { + "localhost.com": {"subj": "localhost.com", "alt_names": []}, + "example.com": { + "subj": "example.com", + "alt_names": ["*.example.com", "*.alt.example.com"], + }, + } + self.config_bundles(bundles) + self.add_tls(["localhost.com", "example.com"]) + + self.check_cert('example.com', bundles['localhost.com']['subj']) + self.check_cert('www.example.com', bundles['example.com']['subj']) + self.check_cert('alt.example.com', bundles['example.com']['subj']) + self.check_cert('www.alt.example.com', bundles['example.com']['subj']) + self.check_cert('www.alt.example.ru', bundles['localhost.com']['subj']) + + def test_tls_sni_duplicated_bundle(self): + bundles = { + "localhost.com": { + "subj": "localhost.com", + "alt_names": ["localhost.com", "alt2.localhost.com"], + } + } + self.config_bundles(bundles) + self.add_tls(["localhost.com", "localhost.com"]) + + self.check_cert('localhost.com', bundles['localhost.com']['subj']) + self.check_cert('alt2.localhost.com', bundles['localhost.com']['subj']) + + def test_tls_sni_same_alt(self): + bundles = { + "localhost": {"subj": "subj1", "alt_names": "same.altname.com"}, + "example": {"subj": "subj2", "alt_names": "same.altname.com"}, + } + self.config_bundles(bundles) + self.add_tls(["localhost", "example"]) + + self.check_cert('localhost', bundles['localhost']['subj']) + self.check_cert('example', bundles['localhost']['subj']) + + def test_tls_sni_empty_cn(self): + bundles = {"localhost": {"alt_names": ["alt.localhost.com"]}} + self.config_bundles(bundles) + self.add_tls(["localhost"]) + + resp, sock = self.get_ssl( + headers={ + 'Host': 'domain.com', + 'Content-Length': '0', + 'Connection': 'close', + }, + start=True, + ) + + assert resp['status'] == 200 + assert ( + sock.getpeercert()['subjectAltName'][0][1] == 'alt.localhost.com' + ) + + def test_tls_sni_invalid(self): + self.config_bundles({"localhost": {"subj": "subj1", "alt_names": ''}}) + self.add_tls(["localhost"]) + + def check_certificate(cert): + assert 'error' in self.conf( + {"pass": "routes", "tls": {"certificate": cert}}, + 'listeners/*:7080', + ) + + check_certificate('') + check_certificate('blah') + check_certificate([]) + check_certificate(['blah']) + check_certificate(['localhost', 'blah']) + check_certificate(['localhost', []]) diff --git a/test/test_usr1.py b/test/test_usr1.py index dbb5265c..9a12b747 100644 --- a/test/test_usr1.py +++ b/test/test_usr1.py @@ -1,14 +1,15 @@ import os -from subprocess import call +import signal from unit.applications.lang.python import TestApplicationPython +from unit.log import Log from unit.utils import waitforfiles class TestUSR1(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def test_usr1_access_log(self, temp_dir): + def test_usr1_access_log(self, temp_dir, unit_pid): self.load('empty') log = 'access.log' @@ -31,10 +32,7 @@ class TestUSR1(TestApplicationPython): ), 'rename new' assert not os.path.isfile(log_path), 'rename old' - with open(temp_dir + '/unit.pid', 'r') as f: - pid = f.read().rstrip() - - call(['kill', '-s', 'USR1', pid]) + os.kill(unit_pid, signal.SIGUSR1) assert waitforfiles(log_path), 'reopen' @@ -46,7 +44,7 @@ class TestUSR1(TestApplicationPython): ), 'reopen 2' assert self.search_in_log(r'/usr1', log_new) is None, 'rename new 2' - def test_usr1_unit_log(self, temp_dir): + def test_usr1_unit_log(self, temp_dir, unit_pid): self.load('log_body') log_new = 'new.log' @@ -55,28 +53,37 @@ class TestUSR1(TestApplicationPython): os.rename(log_path, log_path_new) - body = 'body_for_a_log_new' - assert self.post(body=body)['status'] == 200 + Log.swap(log_new) - assert self.wait_for_record(body, log_new) is not None, 'rename new' - assert not os.path.isfile(log_path), 'rename old' + try: + body = 'body_for_a_log_new\n' + assert self.post(body=body)['status'] == 200 - with open(temp_dir + '/unit.pid', 'r') as f: - pid = f.read().rstrip() + assert ( + self.wait_for_record(body, log_new) is not None + ), 'rename new' + assert not os.path.isfile(log_path), 'rename old' - call(['kill', '-s', 'USR1', pid]) + os.kill(unit_pid, signal.SIGUSR1) - assert waitforfiles(log_path), 'reopen' + assert waitforfiles(log_path), 'reopen' + + body = 'body_for_a_log_unit\n' + assert self.post(body=body)['status'] == 200 + + assert self.wait_for_record(body) is not None, 'rename new' + assert self.search_in_log(body, log_new) is None, 'rename new 2' - body = 'body_for_a_log_unit' - assert self.post(body=body)['status'] == 200 + finally: + # merge two log files into unit.log to check alerts - assert self.wait_for_record(body) is not None, 'rename new' - assert self.search_in_log(body, log_new) is None, 'rename new 2' + with open(log_path, 'r', errors='ignore') as unit_log: + log = unit_log.read() - # merge two log files into unit.log to check alerts + with open(log_path, 'w') as unit_log, open( + log_path_new, 'r', errors='ignore' + ) as unit_log_new: + unit_log.write(unit_log_new.read()) + unit_log.write(log) - with open(log_path, 'w') as unit_log, open( - log_path_new, 'r' - ) as unit_log_new: - unit_log.write(unit_log_new.read()) + Log.swap(log_new) diff --git a/test/test_variables.py b/test/test_variables.py index bbb8f769..139d867e 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -92,16 +92,8 @@ class TestVariables(TestApplicationProto): "*:7080": {"pass": "upstreams$uri"}, "*:7081": {"pass": "routes/one"}, }, - "upstreams": { - "1": { - "servers": { - "127.0.0.1:7081": {}, - }, - }, - }, - "routes": { - "one": [{"action": {"return": 200}}], - }, + "upstreams": {"1": {"servers": {"127.0.0.1:7081": {}}}}, + "routes": {"one": [{"action": {"return": 200}}]}, }, ), 'upstreams initial configuration' diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index a17b1af4..6be1667b 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -13,6 +13,7 @@ class TestApplicationGo(TestApplicationProto): env = os.environ.copy() env['GOPATH'] = option.current_dir + '/build/go' env['GOCACHE'] = option.cache_dir + '/go' + env['GO111MODULE'] = 'auto' if static: args = [ diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index cc6d06ef..5d05c70c 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -7,15 +7,16 @@ from unit.utils import public_dir class TestApplicationNode(TestApplicationProto): + application_type = "node" + es_modules = False + def prepare_env(self, script): # copy application - shutil.copytree( option.test_dir + '/node/' + script, option.temp_dir + '/node' ) # copy modules - shutil.copytree( option.current_dir + '/node/node_modules', option.temp_dir + '/node/node_modules', @@ -26,6 +27,19 @@ class TestApplicationNode(TestApplicationProto): def load(self, script, name='app.js', **kwargs): self.prepare_env(script) + if self.es_modules: + arguments = [ + "node", + "--loader", + "unit-http/loader.mjs", + "--require", + "unit-http/loader", + name, + ] + + else: + arguments = ["node", "--require", "unit-http/loader", name] + self._load_conf( { "listeners": { @@ -36,7 +50,8 @@ class TestApplicationNode(TestApplicationProto): "type": "external", "processes": {"spare": 0}, "working_directory": option.temp_dir + '/node', - "executable": name, + "executable": '/usr/bin/env', + "arguments": arguments, } }, }, diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index 287d23f0..b399dffd 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -42,8 +42,15 @@ class TestApplicationPython(TestApplicationProto): "module": module, } - for attr in ('callable', 'home', 'limits', 'path', 'protocol', - 'threads'): + for attr in ( + 'callable', + 'home', + 'limits', + 'path', + 'protocol', + 'targets', + 'threads', + ): if attr in kwargs: app[attr] = kwargs.pop(attr) diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py index 5c400621..92754c03 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -4,6 +4,7 @@ import time from unit.control import TestControl from unit.option import option +from unit.log import Log class TestApplicationProto(TestControl): @@ -15,18 +16,23 @@ class TestApplicationProto(TestControl): def date_to_sec_epoch(self, date, template='%a, %d %b %Y %H:%M:%S %Z'): return time.mktime(time.strptime(date, template)) + def findall(self, pattern, name='unit.log'): + with Log.open(name) as f: + return re.findall(pattern, f.read()) + def search_in_log(self, pattern, name='unit.log'): - with open(option.temp_dir + '/' + name, 'r', errors='ignore') as f: + with Log.open(name) as f: return re.search(pattern, f.read()) def wait_for_record(self, pattern, name='unit.log', wait=150): - for i in range(wait): - found = self.search_in_log(pattern, name) + with Log.open(name) as f: + for i in range(wait): + found = re.search(pattern, f.read()) - if found is not None: - break + if found is not None: + break - time.sleep(0.1) + time.sleep(0.1) return found diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index b0cd5abb..583b618f 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -21,10 +21,14 @@ class TestApplicationTLS(TestApplicationProto): 'req', '-x509', '-new', - '-subj', '/CN=' + name + '/', - '-config', option.temp_dir + '/openssl.conf', - '-out', option.temp_dir + '/' + name + '.crt', - '-keyout', option.temp_dir + '/' + name + '.key', + '-subj', + '/CN=' + name + '/', + '-config', + option.temp_dir + '/openssl.conf', + '-out', + option.temp_dir + '/' + name + '.crt', + '-keyout', + option.temp_dir + '/' + name + '.key', ], stderr=subprocess.STDOUT, ) @@ -63,19 +67,43 @@ class TestApplicationTLS(TestApplicationProto): return ssl.get_server_certificate(addr, ssl_version=ssl_version) - def openssl_conf(self): + def openssl_conf(self, rewrite=False, alt_names=[]): conf_path = option.temp_dir + '/openssl.conf' - if os.path.exists(conf_path): + if not rewrite and os.path.exists(conf_path): return + # Generates alt_names section with dns names + a_names = "[alt_names]\n" + for i, k in enumerate(alt_names, 1): + k = k.split('|') + + if k[0] == 'IP': + a_names += "IP.%d = %s\n" % (i, k[1]) + else: + a_names += "DNS.%d = %s\n" % (i, k[0]) + + # Generates section for sign request extension + a_sec = """req_extensions = myca_req_extensions + +[ myca_req_extensions ] +subjectAltName = @alt_names + +{a_names}""".format( + a_names=a_names + ) + with open(conf_path, 'w') as f: f.write( """[ req ] default_bits = 2048 encrypt_key = no distinguished_name = req_distinguished_name -[ req_distinguished_name ]""" + +{a_sec} +[ req_distinguished_name ]""".format( + a_sec=a_sec if alt_names else "" + ) ) def load(self, script, name=None): diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index cc720a98..aa83339c 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -43,11 +43,7 @@ class TestApplicationWebsocket(TestApplicationProto): 'Sec-WebSocket-Version': 13, } - _, sock = self.get( - headers=headers, - no_recv=True, - start=True, - ) + _, sock = self.get(headers=headers, no_recv=True, start=True,) resp = '' while True: @@ -57,7 +53,7 @@ class TestApplicationWebsocket(TestApplicationProto): resp += sock.recv(4096).decode() - if (resp.startswith('HTTP/') and '\r\n\r\n' in resp): + if resp.startswith('HTTP/') and '\r\n\r\n' in resp: resp = self._resp_to_dict(resp) break @@ -90,8 +86,8 @@ class TestApplicationWebsocket(TestApplicationProto): frame = {} - head1, = struct.unpack('!B', recv_bytes(sock, 1)) - head2, = struct.unpack('!B', recv_bytes(sock, 1)) + (head1,) = struct.unpack('!B', recv_bytes(sock, 1)) + (head2,) = struct.unpack('!B', recv_bytes(sock, 1)) frame['fin'] = bool(head1 & 0b10000000) frame['rsv1'] = bool(head1 & 0b01000000) @@ -103,10 +99,10 @@ class TestApplicationWebsocket(TestApplicationProto): length = head2 & 0b01111111 if length == 126: data = recv_bytes(sock, 2) - length, = struct.unpack('!H', data) + (length,) = struct.unpack('!H', data) elif length == 127: data = recv_bytes(sock, 8) - length, = struct.unpack('!Q', data) + (length,) = struct.unpack('!Q', data) if frame['mask']: mask_bits = recv_bytes(sock, 4) @@ -121,7 +117,7 @@ class TestApplicationWebsocket(TestApplicationProto): if frame['opcode'] == self.OP_CLOSE: if length >= 2: - code, = struct.unpack('!H', data[:2]) + (code,) = struct.unpack('!H', data[:2]) reason = data[2:].decode('utf-8') if not (code in self.CLOSE_CODES or 3000 <= code < 5000): pytest.fail('Invalid status code') diff --git a/test/unit/check/chroot.py b/test/unit/check/chroot.py new file mode 100644 index 00000000..40b75058 --- /dev/null +++ b/test/unit/check/chroot.py @@ -0,0 +1,32 @@ +import json + +from unit.http import TestHTTP +from unit.option import option + +http = TestHTTP() + + +def check_chroot(): + available = option.available + + resp = http.put( + url='/config', + sock_type='unix', + addr=option.temp_dir + '/control.unit.sock', + body=json.dumps( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "action": { + "share": option.temp_dir, + "chroot": option.temp_dir, + } + } + ], + } + ), + ) + + if 'success' in resp['body']: + available['features']['chroot'] = True diff --git a/test/unit/check/go.py b/test/unit/check/go.py index 35b0c2d5..309091c0 100644 --- a/test/unit/check/go.py +++ b/test/unit/check/go.py @@ -8,6 +8,7 @@ def check_go(current_dir, temp_dir, test_dir): env = os.environ.copy() env['GOPATH'] = current_dir + '/build/go' + env['GO111MODULE'] = 'auto' try: process = subprocess.Popen( diff --git a/test/unit/check/isolation.py b/test/unit/check/isolation.py index fe5a41f8..7c83ae35 100644 --- a/test/unit/check/isolation.py +++ b/test/unit/check/isolation.py @@ -12,6 +12,7 @@ from unit.utils import getns allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net'] http = TestHTTP() + def check_isolation(): test_conf = {"namespaces": {"credential": True}} available = option.available @@ -117,8 +118,7 @@ def check_isolation(): "body_empty": { "type": "perl", "processes": {"spare": 0}, - "working_directory": option.test_dir - + "/perl/body_empty", + "working_directory": option.test_dir + "/perl/body_empty", "script": option.test_dir + "/perl/body_empty/psgi.pl", "isolation": {"namespaces": {"credential": True}}, } diff --git a/test/unit/check/node.py b/test/unit/check/node.py index 236ba7b5..e053a749 100644 --- a/test/unit/check/node.py +++ b/test/unit/check/node.py @@ -1,6 +1,15 @@ import os +import subprocess def check_node(current_dir): - if os.path.exists(current_dir + '/node/node_modules'): - return True + if not os.path.exists(current_dir + '/node/node_modules'): + return None + + try: + v_bytes = subprocess.check_output(['/usr/bin/env', 'node', '-v']) + + return [str(v_bytes, 'utf-8').lstrip('v').rstrip()] + + except subprocess.CalledProcessError: + return None diff --git a/test/unit/http.py b/test/unit/http.py index 57e6ed3a..797b7681 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -10,15 +10,16 @@ import pytest from unit.option import option -class TestHTTP(): +class TestHTTP: def http(self, start_str, **kwargs): sock_type = kwargs.get('sock_type', 'ipv4') port = kwargs.get('port', 7080) url = kwargs.get('url', '/') http = 'HTTP/1.0' if 'http_10' in kwargs else 'HTTP/1.1' - headers = kwargs.get('headers', - {'Host': 'localhost', 'Connection': 'close'}) + headers = kwargs.get( + 'headers', {'Host': 'localhost', 'Connection': 'close'} + ) body = kwargs.get('body', b'') crlf = '\r\n' @@ -44,7 +45,8 @@ class TestHTTP(): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if 'wrapper' in kwargs: - sock = kwargs['wrapper'](sock) + server_hostname = headers.get('Host', 'localhost') + sock = kwargs['wrapper'](sock, server_hostname=server_hostname) connect_args = addr if sock_type == 'unix' else (addr, port) try: @@ -304,8 +306,9 @@ class TestHTTP(): return body, content_type def form_url_encode(self, fields): - data = "&".join("%s=%s" % (name, value) - for name, value in fields.items()).encode() + data = "&".join( + "%s=%s" % (name, value) for name, value in fields.items() + ).encode() return data, 'application/x-www-form-urlencoded' def multipart_encode(self, fields): @@ -325,7 +328,9 @@ class TestHTTP(): datatype = value['type'] if not isinstance(value['data'], io.IOBase): - pytest.fail('multipart encoding of file requires a stream.') + pytest.fail( + 'multipart encoding of file requires a stream.' + ) data = value['data'].read() @@ -335,9 +340,10 @@ class TestHTTP(): else: pytest.fail('multipart requires a string or stream data') - body += ( - "--%s\r\nContent-Disposition: form-data; name=\"%s\"" - ) % (boundary, field) + body += ("--%s\r\nContent-Disposition: form-data; name=\"%s\"") % ( + boundary, + field, + ) if filename != '': body += "; filename=\"%s\"" % filename diff --git a/test/unit/log.py b/test/unit/log.py new file mode 100644 index 00000000..7263443d --- /dev/null +++ b/test/unit/log.py @@ -0,0 +1,23 @@ +UNIT_LOG = 'unit.log' + + +class Log: + temp_dir = None + pos = {} + + def open(name=UNIT_LOG, encoding=None): + f = open(Log.get_path(name), 'r', encoding=encoding, errors='ignore') + f.seek(Log.pos.get(name, 0)) + + return f + + def set_pos(pos, name=UNIT_LOG): + Log.pos[name] = pos + + def swap(name): + pos = Log.pos.get(UNIT_LOG, 0) + Log.pos[UNIT_LOG] = Log.pos.get(name, 0) + Log.pos[name] = pos + + def get_path(name=UNIT_LOG): + return Log.temp_dir + '/' + name diff --git a/test/unit/option.py b/test/unit/option.py index 677d806e..cb3803dc 100644 --- a/test/unit/option.py +++ b/test/unit/option.py @@ -1,4 +1,4 @@ -class Options(): +class Options: _options = { 'skip_alerts': [], 'skip_sanitizer': False, @@ -13,4 +13,5 @@ class Options(): raise AttributeError + option = Options() diff --git a/test/unit/utils.py b/test/unit/utils.py index e80fc469..a627e9f5 100644 --- a/test/unit/utils.py +++ b/test/unit/utils.py @@ -61,6 +61,17 @@ def findmnt(): return out +def sysctl(): + try: + out = subprocess.check_output( + ['sysctl', '-a'], stderr=subprocess.STDOUT + ).decode() + except FileNotFoundError: + pytest.skip('requires sysctl') + + return out + + def waitformount(template, wait=50): for i in range(wait): if findmnt().find(template) != -1: @@ -1,5 +1,5 @@ # Copyright (C) NGINX, Inc. -NXT_VERSION=1.23.0 -NXT_VERNUM=12300 +NXT_VERSION=1.24.0 +NXT_VERNUM=12400 |