From a13018fecb9fd2565b3db9762009927269e1c77d Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Mon, 17 Aug 2020 11:57:55 +0300 Subject: Version bump. --- version | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version b/version index 6b8e9c50..a3b9c907 100644 --- a/version +++ b/version @@ -1,5 +1,5 @@ # Copyright (C) NGINX, Inc. -NXT_VERSION=1.19.0 -NXT_VERNUM=11900 +NXT_VERSION=1.20.0 +NXT_VERNUM=12000 -- cgit From 4ac7a6f55fb628e67fec97568e9724719d8a9e0a Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 17 Aug 2020 12:28:40 +0300 Subject: Style: changing preprocessor directives. Using #if directives instead of #ifdef the same way as in other places. --- src/nxt_conn_write.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/nxt_conn_write.c b/src/nxt_conn_write.c index d7a6a8da..3e08e43f 100644 --- a/src/nxt_conn_write.c +++ b/src/nxt_conn_write.c @@ -246,24 +246,30 @@ nxt_sendfile(int fd, int s, off_t pos, size_t size) { ssize_t res; -#ifdef NXT_HAVE_MACOSX_SENDFILE +#if (NXT_HAVE_MACOSX_SENDFILE) + off_t sent = size; int rc = sendfile(fd, s, pos, &sent, NULL, 0); res = (rc == 0 || sent > 0) ? sent : -1; -#endif -#ifdef NXT_HAVE_FREEBSD_SENDFILE +#elif (NXT_HAVE_FREEBSD_SENDFILE) + off_t sent = 0; int rc = sendfile(fd, s, pos, size, NULL, &sent, 0); res = (rc == 0 || sent > 0) ? sent : -1; -#endif -#ifdef NXT_HAVE_LINUX_SENDFILE +#elif (NXT_HAVE_LINUX_SENDFILE) + res = sendfile(s, fd, &pos, size); + +#else + + res = -1; + #endif return res; -- cgit From 7ffc617ae89fe08b8a9a17bed41ef8941b8151fb Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 17 Aug 2020 12:28:48 +0300 Subject: Supporting platforms without sendfile() implementation. This is a quick and dirty sendfile() replacement. This closes #452 PR on GitHub. --- auto/sendfile | 6 ++---- src/nxt_conn_write.c | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/auto/sendfile b/auto/sendfile index a065f7b6..1c20db06 100644 --- a/auto/sendfile +++ b/auto/sendfile @@ -84,10 +84,8 @@ fi if [ $nxt_found = no ]; then - $echo - $echo "$0: error: no supported sendfile() found." - $echo - exit 1; + # No supported sendfile() found. Using our replacement. + nxt_found=yes fi diff --git a/src/nxt_conn_write.c b/src/nxt_conn_write.c index 3e08e43f..bcf9e8fa 100644 --- a/src/nxt_conn_write.c +++ b/src/nxt_conn_write.c @@ -268,7 +268,24 @@ nxt_sendfile(int fd, int s, off_t pos, size_t size) #else - res = -1; + int err; + void *map; + off_t page_off; + + page_off = pos % nxt_pagesize; + + map = nxt_mem_mmap(NULL, size + page_off, PROT_READ, MAP_SHARED, fd, + pos - page_off); + if (nxt_slow_path(map == MAP_FAILED)) { + return -1; + } + + res = write(s, nxt_pointer_to(map, page_off), size); + + /* Backup and restore errno to catch socket errors in the upper level. */ + err = errno; + nxt_mem_munmap(map, size + page_off); + errno = err; #endif -- cgit From 9bf6efc55ac9678ad386fd1a9d420a3b75e9ab70 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Wed, 19 Aug 2020 15:36:57 +0300 Subject: Configuration: improved error message of invalid listener address. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to 洪志道 (Hong Zhi Dao). This closes #466 issue on GitHub. --- src/nxt_conf_validation.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/nxt_conf_validation.c b/src/nxt_conf_validation.c index b5530b85..4c979782 100644 --- a/src/nxt_conf_validation.c +++ b/src/nxt_conf_validation.c @@ -1188,10 +1188,17 @@ static nxt_int_t nxt_conf_vldt_listener(nxt_conf_validation_t *vldt, nxt_str_t *name, nxt_conf_value_t *value) { - nxt_int_t ret; + nxt_int_t ret; + nxt_sockaddr_t *sa; - ret = nxt_conf_vldt_type(vldt, name, value, NXT_CONF_VLDT_OBJECT); + sa = nxt_sockaddr_parse(vldt->pool, name); + if (nxt_slow_path(sa == NULL)) { + return nxt_conf_vldt_error(vldt, + "The listener address \"%V\" is invalid.", + name); + } + ret = nxt_conf_vldt_type(vldt, name, value, NXT_CONF_VLDT_OBJECT); if (ret != NXT_OK) { return ret; } -- cgit From a8a7eeb1fc7aada17d0d8fe8e15d325525986937 Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Thu, 20 Aug 2020 15:22:58 +0100 Subject: Moved isolation related code to "nxt_isolation.c". --- auto/sources | 1 + src/nxt_application.c | 585 +----------------------------- src/nxt_isolation.c | 958 +++++++++++++++++++++++++++++++++++++++++++++++++ src/nxt_isolation.h | 18 + src/nxt_main_process.c | 6 +- src/nxt_process.c | 382 -------------------- src/nxt_process.h | 20 +- 7 files changed, 990 insertions(+), 980 deletions(-) create mode 100644 src/nxt_isolation.c create mode 100644 src/nxt_isolation.h diff --git a/auto/sources b/auto/sources index a61577dc..e44dc4bb 100644 --- a/auto/sources +++ b/auto/sources @@ -14,6 +14,7 @@ NXT_LIB_SRCS=" \ src/nxt_socket.c \ src/nxt_socketpair.c \ src/nxt_credential.c \ + src/nxt_isolation.c \ src/nxt_process.c \ src/nxt_process_title.c \ src/nxt_signal.c \ diff --git a/src/nxt_application.c b/src/nxt_application.c index 57e4615e..6935346c 100644 --- a/src/nxt_application.c +++ b/src/nxt_application.c @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -41,45 +42,10 @@ static void nxt_discovery_quit(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data); static nxt_app_module_t *nxt_app_module_load(nxt_task_t *task, const char *name); -static nxt_int_t nxt_app_main_prefork(nxt_task_t *task, nxt_process_t *process, - nxt_mp_t *mp); static nxt_int_t nxt_app_setup(nxt_task_t *task, nxt_process_t *process); static nxt_int_t nxt_app_set_environment(nxt_conf_value_t *environment); static u_char *nxt_cstr_dup(nxt_mp_t *mp, u_char *dst, u_char *src); -#if (NXT_HAVE_ISOLATION_ROOTFS) -static nxt_int_t nxt_app_set_isolation_mounts(nxt_task_t *task, - nxt_process_t *process, nxt_str_t *app_type); -static nxt_int_t nxt_app_set_lang_mounts(nxt_task_t *task, - nxt_process_t *process, nxt_array_t *syspaths); -static nxt_int_t nxt_app_set_isolation_rootfs(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -static nxt_int_t nxt_app_prepare_rootfs(nxt_task_t *task, - nxt_process_t *process); -#endif - -static nxt_int_t nxt_app_set_isolation(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); - -#if (NXT_HAVE_CLONE) -static nxt_int_t nxt_app_set_isolation_namespaces(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -static nxt_int_t nxt_app_clone_flags(nxt_task_t *task, - nxt_conf_value_t *namespaces, nxt_clone_t *clone); -#endif - -#if (NXT_HAVE_CLONE_NEWUSER) -static nxt_int_t nxt_app_set_isolation_creds(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -static nxt_int_t nxt_app_isolation_credential_map(nxt_task_t *task, - nxt_mp_t *mem_pool, nxt_conf_value_t *map_array, - nxt_clone_credential_map_t *map); -#endif - -#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) -static nxt_int_t nxt_app_set_isolation_new_privs(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -#endif nxt_str_t nxt_server = nxt_string(NXT_SERVER); @@ -126,7 +92,7 @@ const nxt_process_init_t nxt_discovery_process = { const nxt_process_init_t nxt_app_process = { .type = NXT_PROCESS_APP, .setup = nxt_app_setup, - .prefork = nxt_app_main_prefork, + .prefork = nxt_isolation_main_prefork, .restart = 0, .start = NULL, /* set to module->start */ .port_handlers = &nxt_app_process_port_handlers, @@ -473,81 +439,6 @@ nxt_discovery_quit(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data) } -static nxt_int_t -nxt_app_main_prefork(nxt_task_t *task, nxt_process_t *process, nxt_mp_t *mp) -{ - nxt_int_t cap_setid; - nxt_int_t ret; - nxt_runtime_t *rt; - nxt_common_app_conf_t *app_conf; - - rt = task->thread->runtime; - app_conf = process->data.app; - cap_setid = rt->capabilities.setid; - - if (app_conf->isolation != NULL) { - ret = nxt_app_set_isolation(task, app_conf->isolation, process); - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } - } - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { - cap_setid = 1; - } -#endif - -#if (NXT_HAVE_ISOLATION_ROOTFS) - if (process->isolation.rootfs != NULL) { - ret = nxt_app_set_isolation_mounts(task, process, &app_conf->type); - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } - } -#endif - - if (cap_setid) { - ret = nxt_process_creds_set(task, process, &app_conf->user, - &app_conf->group); - - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } - - } else { - if (!nxt_str_eq(&app_conf->user, (u_char *) rt->user_cred.user, - nxt_strlen(rt->user_cred.user))) - { - nxt_alert(task, "cannot set user \"%V\" for app \"%V\": " - "missing capabilities", &app_conf->user, &app_conf->name); - - return NXT_ERROR; - } - - if (app_conf->group.length > 0 - && !nxt_str_eq(&app_conf->group, (u_char *) rt->group, - nxt_strlen(rt->group))) - { - nxt_alert(task, "cannot set group \"%V\" for app \"%V\": " - "missing capabilities", &app_conf->group, - &app_conf->name); - - return NXT_ERROR; - } - } - -#if (NXT_HAVE_CLONE_NEWUSER) - ret = nxt_process_vldt_isolation_creds(task, process); - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } -#endif - - return NXT_OK; -} - - static nxt_int_t nxt_app_setup(nxt_task_t *task, nxt_process_t *process) { @@ -594,13 +485,13 @@ nxt_app_setup(nxt_task_t *task, nxt_process_t *process) #if (NXT_HAVE_ISOLATION_ROOTFS) if (process->isolation.rootfs != NULL) { if (process->isolation.mounts != NULL) { - ret = nxt_app_prepare_rootfs(task, process); + ret = nxt_isolation_prepare_rootfs(task, process); if (nxt_slow_path(ret != NXT_OK)) { return ret; } } - ret = nxt_process_change_root(task, process); + ret = nxt_isolation_change_root(task, process); if (nxt_slow_path(ret != NXT_OK)) { return NXT_ERROR; } @@ -686,474 +577,6 @@ nxt_app_set_environment(nxt_conf_value_t *environment) } -static nxt_int_t -nxt_app_set_isolation(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ -#if (NXT_HAVE_CLONE) - if (nxt_slow_path(nxt_app_set_isolation_namespaces(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_slow_path(nxt_app_set_isolation_creds(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - -#if (NXT_HAVE_ISOLATION_ROOTFS) - if (nxt_slow_path(nxt_app_set_isolation_rootfs(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - -#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) - if (nxt_slow_path(nxt_app_set_isolation_new_privs(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - - return NXT_OK; -} - - -#if (NXT_HAVE_CLONE) - -static nxt_int_t -nxt_app_set_isolation_namespaces(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_int_t ret; - nxt_conf_value_t *obj; - - static nxt_str_t nsname = nxt_string("namespaces"); - - obj = nxt_conf_get_object_member(isolation, &nsname, NULL); - if (obj != NULL) { - ret = nxt_app_clone_flags(task, obj, &process->isolation.clone); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - return NXT_OK; -} - -#endif - - -#if (NXT_HAVE_CLONE_NEWUSER) - -static nxt_int_t -nxt_app_set_isolation_creds(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_int_t ret; - nxt_clone_t *clone; - nxt_conf_value_t *array; - - static nxt_str_t uidname = nxt_string("uidmap"); - static nxt_str_t gidname = nxt_string("gidmap"); - - clone = &process->isolation.clone; - - array = nxt_conf_get_object_member(isolation, &uidname, NULL); - if (array != NULL) { - ret = nxt_app_isolation_credential_map(task, process->mem_pool, array, - &clone->uidmap); - - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - array = nxt_conf_get_object_member(isolation, &gidname, NULL); - if (array != NULL) { - ret = nxt_app_isolation_credential_map(task, process->mem_pool, array, - &clone->gidmap); - - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_isolation_credential_map(nxt_task_t *task, nxt_mp_t *mp, - nxt_conf_value_t *map_array, nxt_clone_credential_map_t *map) -{ - nxt_int_t ret; - nxt_uint_t i; - nxt_conf_value_t *obj; - - static nxt_conf_map_t nxt_clone_map_entry_conf[] = { - { - nxt_string("container"), - NXT_CONF_MAP_INT, - offsetof(nxt_clone_map_entry_t, container), - }, - - { - nxt_string("host"), - NXT_CONF_MAP_INT, - offsetof(nxt_clone_map_entry_t, host), - }, - - { - nxt_string("size"), - NXT_CONF_MAP_INT, - offsetof(nxt_clone_map_entry_t, size), - }, - }; - - map->size = nxt_conf_array_elements_count(map_array); - - if (map->size == 0) { - return NXT_OK; - } - - map->map = nxt_mp_alloc(mp, map->size * sizeof(nxt_clone_map_entry_t)); - if (nxt_slow_path(map->map == NULL)) { - return NXT_ERROR; - } - - for (i = 0; i < map->size; i++) { - obj = nxt_conf_get_array_element(map_array, i); - - ret = nxt_conf_map_object(mp, obj, nxt_clone_map_entry_conf, - nxt_nitems(nxt_clone_map_entry_conf), - map->map + i); - if (nxt_slow_path(ret != NXT_OK)) { - nxt_alert(task, "clone map entry map error"); - return NXT_ERROR; - } - } - - return NXT_OK; -} - -#endif - -#if (NXT_HAVE_CLONE) - -static nxt_int_t -nxt_app_clone_flags(nxt_task_t *task, nxt_conf_value_t *namespaces, - nxt_clone_t *clone) -{ - uint32_t index; - nxt_str_t name; - nxt_int_t flag; - nxt_conf_value_t *value; - - index = 0; - - for ( ;; ) { - value = nxt_conf_next_object_member(namespaces, &name, &index); - - if (value == NULL) { - break; - } - - flag = 0; - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_str_eq(&name, "credential", 10)) { - flag = CLONE_NEWUSER; - } -#endif - -#if (NXT_HAVE_CLONE_NEWPID) - if (nxt_str_eq(&name, "pid", 3)) { - flag = CLONE_NEWPID; - } -#endif - -#if (NXT_HAVE_CLONE_NEWNET) - if (nxt_str_eq(&name, "network", 7)) { - flag = CLONE_NEWNET; - } -#endif - -#if (NXT_HAVE_CLONE_NEWUTS) - if (nxt_str_eq(&name, "uname", 5)) { - flag = CLONE_NEWUTS; - } -#endif - -#if (NXT_HAVE_CLONE_NEWNS) - if (nxt_str_eq(&name, "mount", 5)) { - flag = CLONE_NEWNS; - } -#endif - -#if (NXT_HAVE_CLONE_NEWCGROUP) - if (nxt_str_eq(&name, "cgroup", 6)) { - flag = CLONE_NEWCGROUP; - } -#endif - - if (!flag) { - nxt_alert(task, "unknown namespace flag: \"%V\"", &name); - return NXT_ERROR; - } - - if (nxt_conf_get_boolean(value)) { - clone->flags |= flag; - } - } - - return NXT_OK; -} - -#endif - - -#if (NXT_HAVE_ISOLATION_ROOTFS) - -static nxt_int_t -nxt_app_set_isolation_rootfs(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_str_t str; - nxt_conf_value_t *obj; - - static nxt_str_t rootfs_name = nxt_string("rootfs"); - - obj = nxt_conf_get_object_member(isolation, &rootfs_name, NULL); - if (obj != NULL) { - nxt_conf_get_string(obj, &str); - - if (nxt_slow_path(str.length <= 1 || str.start[0] != '/')) { - nxt_log(task, NXT_LOG_ERR, "rootfs requires an absolute path other " - "than \"/\" but given \"%V\"", &str); - - return NXT_ERROR; - } - - if (str.start[str.length - 1] == '/') { - str.length--; - } - - process->isolation.rootfs = nxt_mp_alloc(process->mem_pool, - str.length + 1); - - if (nxt_slow_path(process->isolation.rootfs == NULL)) { - return NXT_ERROR; - } - - nxt_memcpy(process->isolation.rootfs, str.start, str.length); - - process->isolation.rootfs[str.length] = '\0'; - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_set_isolation_mounts(nxt_task_t *task, nxt_process_t *process, - nxt_str_t *app_type) -{ - nxt_int_t ret, cap_chroot; - nxt_runtime_t *rt; - nxt_app_lang_module_t *lang; - - rt = task->thread->runtime; - cap_chroot = rt->capabilities.chroot; - lang = nxt_app_lang_module(rt, app_type); - - nxt_assert(lang != NULL); - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { - cap_chroot = 1; - } -#endif - - if (!cap_chroot) { - nxt_log(task, NXT_LOG_ERR, "The \"rootfs\" field requires privileges"); - return NXT_ERROR; - } - - if (lang->mounts != NULL && lang->mounts->nelts > 0) { - ret = nxt_app_set_lang_mounts(task, process, lang->mounts); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, - nxt_array_t *lang_mounts) -{ - u_char *p; - size_t i, n, rootfs_len, len; - nxt_mp_t *mp; - nxt_array_t *mounts; - const u_char *rootfs; - nxt_fs_mount_t *mnt, *lang_mnt; - - rootfs = process->isolation.rootfs; - rootfs_len = nxt_strlen(rootfs); - mp = process->mem_pool; - - /* copy to init mem pool */ - mounts = nxt_array_copy(mp, NULL, lang_mounts); - if (mounts == NULL) { - return NXT_ERROR; - } - - n = mounts->nelts; - mnt = mounts->elts; - lang_mnt = lang_mounts->elts; - - for (i = 0; i < n; i++) { - len = nxt_strlen(lang_mnt[i].dst); - - mnt[i].dst = nxt_mp_alloc(mp, rootfs_len + len + 1); - if (mnt[i].dst == NULL) { - return NXT_ERROR; - } - - p = nxt_cpymem(mnt[i].dst, rootfs, rootfs_len); - p = nxt_cpymem(p, lang_mnt[i].dst, len); - *p = '\0'; - } - - process->isolation.mounts = mounts; - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) -{ - size_t i, n; - nxt_int_t ret, hasproc; - struct stat st; - nxt_array_t *mounts; - const u_char *dst; - nxt_fs_mount_t *mnt; - - hasproc = 0; - -#if (NXT_HAVE_CLONE_NEWPID) && (NXT_HAVE_CLONE_NEWNS) - nxt_fs_mount_t mount; - - if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWPID) - && nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) - { - /* - * This mount point will automatically be gone when the namespace is - * destroyed. - */ - - mount.fstype = (u_char *) "proc"; - mount.src = (u_char *) "proc"; - mount.dst = (u_char *) "/proc"; - mount.data = (u_char *) ""; - mount.flags = 0; - - ret = nxt_fs_mkdir_all(mount.dst, S_IRWXU | S_IRWXG | S_IRWXO); - if (nxt_fast_path(ret == NXT_OK)) { - ret = nxt_fs_mount(task, &mount); - if (nxt_fast_path(ret == NXT_OK)) { - hasproc = 1; - } - - } else { - nxt_log(task, NXT_LOG_WARN, "mkdir(%s) %E", mount.dst, nxt_errno); - } - } -#endif - - mounts = process->isolation.mounts; - - n = mounts->nelts; - mnt = mounts->elts; - - for (i = 0; i < n; i++) { - dst = mnt[i].dst; - - if (nxt_slow_path(nxt_memcmp(mnt[i].fstype, "bind", 4) == 0 - && stat((const char *) mnt[i].src, &st) != 0)) - { - nxt_log(task, NXT_LOG_WARN, "host path not found: %s", mnt[i].src); - continue; - } - - if (hasproc && nxt_memcmp(mnt[i].fstype, "proc", 4) == 0 - && nxt_memcmp(mnt[i].dst, "/proc", 5) == 0) - { - continue; - } - - ret = nxt_fs_mkdir_all(dst, S_IRWXU | S_IRWXG | S_IRWXO); - if (nxt_slow_path(ret != NXT_OK)) { - nxt_alert(task, "mkdir(%s) %E", dst, nxt_errno); - goto undo; - } - - ret = nxt_fs_mount(task, &mnt[i]); - if (nxt_slow_path(ret != NXT_OK)) { - goto undo; - } - } - - return NXT_OK; - -undo: - - n = i + 1; - - for (i = 0; i < n; i++) { - nxt_fs_unmount(mnt[i].dst); - } - - return NXT_ERROR; -} - -#endif - - -#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) - -static nxt_int_t -nxt_app_set_isolation_new_privs(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_conf_value_t *obj; - - static nxt_str_t new_privs_name = nxt_string("new_privs"); - - obj = nxt_conf_get_object_member(isolation, &new_privs_name, NULL); - if (obj != NULL) { - process->isolation.new_privs = nxt_conf_get_boolean(obj); - } - - return NXT_OK; -} - -#endif - - static u_char * nxt_cstr_dup(nxt_mp_t *mp, u_char *dst, u_char *src) { diff --git a/src/nxt_isolation.c b/src/nxt_isolation.c new file mode 100644 index 00000000..60de4324 --- /dev/null +++ b/src/nxt_isolation.c @@ -0,0 +1,958 @@ +/* + * Copyright (C) NGINX, Inc. + */ + +#include +#include +#include +#include + +#if (NXT_HAVE_PIVOT_ROOT) +#include +#endif + + +static nxt_int_t nxt_isolation_set(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); + +#if (NXT_HAVE_CLONE) +static nxt_int_t nxt_isolation_set_namespaces(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_clone_flags(nxt_task_t *task, + nxt_conf_value_t *namespaces, nxt_clone_t *clone); +#endif + +#if (NXT_HAVE_CLONE_NEWUSER) +static nxt_int_t nxt_isolation_set_creds(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_credential_map(nxt_task_t *task, + nxt_mp_t *mem_pool, nxt_conf_value_t *map_array, + nxt_clone_credential_map_t *map); +static nxt_int_t nxt_isolation_vldt_creds(nxt_task_t *task, + nxt_process_t *process); +#endif + +#if (NXT_HAVE_ISOLATION_ROOTFS) +static nxt_int_t nxt_isolation_set_rootfs(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_set_mounts(nxt_task_t *task, + nxt_process_t *process, nxt_str_t *app_type); +static nxt_int_t nxt_isolation_set_lang_mounts(nxt_task_t *task, + nxt_process_t *process, nxt_array_t *syspaths); +static void nxt_isolation_unmount_all(nxt_task_t *task, nxt_process_t *process); + +#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) +static nxt_int_t nxt_isolation_pivot_root(nxt_task_t *task, const char *rootfs); +static nxt_int_t nxt_isolation_make_private_mount(nxt_task_t *task, + const char *rootfs); +nxt_inline int nxt_pivot_root(const char *new_root, const char *old_root); +#endif + +static nxt_int_t nxt_isolation_chroot(nxt_task_t *task, const char *path); +#endif + +#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) +static nxt_int_t nxt_isolation_set_new_privs(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +#endif + + +nxt_int_t +nxt_isolation_main_prefork(nxt_task_t *task, nxt_process_t *process, + nxt_mp_t *mp) +{ + nxt_int_t cap_setid; + nxt_int_t ret; + nxt_runtime_t *rt; + nxt_common_app_conf_t *app_conf; + + rt = task->thread->runtime; + app_conf = process->data.app; + cap_setid = rt->capabilities.setid; + + if (app_conf->isolation != NULL) { + ret = nxt_isolation_set(task, app_conf->isolation, process); + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } + } + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { + cap_setid = 1; + } +#endif + +#if (NXT_HAVE_ISOLATION_ROOTFS) + if (process->isolation.rootfs != NULL) { + ret = nxt_isolation_set_mounts(task, process, &app_conf->type); + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } + } +#endif + + if (cap_setid) { + ret = nxt_process_creds_set(task, process, &app_conf->user, + &app_conf->group); + + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } + + } else { + if (!nxt_str_eq(&app_conf->user, (u_char *) rt->user_cred.user, + nxt_strlen(rt->user_cred.user))) + { + nxt_alert(task, "cannot set user \"%V\" for app \"%V\": " + "missing capabilities", &app_conf->user, &app_conf->name); + + return NXT_ERROR; + } + + if (app_conf->group.length > 0 + && !nxt_str_eq(&app_conf->group, (u_char *) rt->group, + nxt_strlen(rt->group))) + { + nxt_alert(task, "cannot set group \"%V\" for app \"%V\": " + "missing capabilities", &app_conf->group, + &app_conf->name); + + return NXT_ERROR; + } + } + +#if (NXT_HAVE_CLONE_NEWUSER) + ret = nxt_isolation_vldt_creds(task, process); + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } +#endif + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_set(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ +#if (NXT_HAVE_CLONE) + if (nxt_slow_path(nxt_isolation_set_namespaces(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_slow_path(nxt_isolation_set_creds(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + +#if (NXT_HAVE_ISOLATION_ROOTFS) + if (nxt_slow_path(nxt_isolation_set_rootfs(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + +#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) + if (nxt_slow_path(nxt_isolation_set_new_privs(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + + return NXT_OK; +} + + +#if (NXT_HAVE_CLONE) + +static nxt_int_t +nxt_isolation_set_namespaces(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_int_t ret; + nxt_conf_value_t *obj; + + static nxt_str_t nsname = nxt_string("namespaces"); + + obj = nxt_conf_get_object_member(isolation, &nsname, NULL); + if (obj != NULL) { + ret = nxt_isolation_clone_flags(task, obj, &process->isolation.clone); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + return NXT_OK; +} + +#endif + + +#if (NXT_HAVE_CLONE_NEWUSER) + +static nxt_int_t +nxt_isolation_set_creds(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_int_t ret; + nxt_clone_t *clone; + nxt_conf_value_t *array; + + static nxt_str_t uidname = nxt_string("uidmap"); + static nxt_str_t gidname = nxt_string("gidmap"); + + clone = &process->isolation.clone; + + array = nxt_conf_get_object_member(isolation, &uidname, NULL); + if (array != NULL) { + ret = nxt_isolation_credential_map(task, process->mem_pool, array, + &clone->uidmap); + + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + array = nxt_conf_get_object_member(isolation, &gidname, NULL); + if (array != NULL) { + ret = nxt_isolation_credential_map(task, process->mem_pool, array, + &clone->gidmap); + + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_credential_map(nxt_task_t *task, nxt_mp_t *mp, + nxt_conf_value_t *map_array, nxt_clone_credential_map_t *map) +{ + nxt_int_t ret; + nxt_uint_t i; + nxt_conf_value_t *obj; + + static nxt_conf_map_t nxt_clone_map_entry_conf[] = { + { + nxt_string("container"), + NXT_CONF_MAP_INT, + offsetof(nxt_clone_map_entry_t, container), + }, + + { + nxt_string("host"), + NXT_CONF_MAP_INT, + offsetof(nxt_clone_map_entry_t, host), + }, + + { + nxt_string("size"), + NXT_CONF_MAP_INT, + offsetof(nxt_clone_map_entry_t, size), + }, + }; + + map->size = nxt_conf_array_elements_count(map_array); + + if (map->size == 0) { + return NXT_OK; + } + + map->map = nxt_mp_alloc(mp, map->size * sizeof(nxt_clone_map_entry_t)); + if (nxt_slow_path(map->map == NULL)) { + return NXT_ERROR; + } + + for (i = 0; i < map->size; i++) { + obj = nxt_conf_get_array_element(map_array, i); + + ret = nxt_conf_map_object(mp, obj, nxt_clone_map_entry_conf, + nxt_nitems(nxt_clone_map_entry_conf), + map->map + i); + if (nxt_slow_path(ret != NXT_OK)) { + nxt_alert(task, "clone map entry map error"); + return NXT_ERROR; + } + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_vldt_creds(nxt_task_t *task, nxt_process_t *process) +{ + nxt_int_t ret; + nxt_clone_t *clone; + nxt_credential_t *creds; + + clone = &process->isolation.clone; + creds = process->user_cred; + + if (clone->uidmap.size == 0 && clone->gidmap.size == 0) { + return NXT_OK; + } + + if (!nxt_is_clone_flag_set(clone->flags, NEWUSER)) { + if (nxt_slow_path(clone->uidmap.size > 0)) { + nxt_log(task, NXT_LOG_ERR, "\"uidmap\" is set but " + "\"isolation.namespaces.credential\" is false or unset"); + + return NXT_ERROR; + } + + if (nxt_slow_path(clone->gidmap.size > 0)) { + nxt_log(task, NXT_LOG_ERR, "\"gidmap\" is set but " + "\"isolation.namespaces.credential\" is false or unset"); + + return NXT_ERROR; + } + + return NXT_OK; + } + + ret = nxt_clone_vldt_credential_uidmap(task, &clone->uidmap, creds); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + + return nxt_clone_vldt_credential_gidmap(task, &clone->gidmap, creds); +} + +#endif + + +#if (NXT_HAVE_CLONE) + +static nxt_int_t +nxt_isolation_clone_flags(nxt_task_t *task, nxt_conf_value_t *namespaces, + nxt_clone_t *clone) +{ + uint32_t index; + nxt_str_t name; + nxt_int_t flag; + nxt_conf_value_t *value; + + index = 0; + + for ( ;; ) { + value = nxt_conf_next_object_member(namespaces, &name, &index); + + if (value == NULL) { + break; + } + + flag = 0; + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_str_eq(&name, "credential", 10)) { + flag = CLONE_NEWUSER; + } +#endif + +#if (NXT_HAVE_CLONE_NEWPID) + if (nxt_str_eq(&name, "pid", 3)) { + flag = CLONE_NEWPID; + } +#endif + +#if (NXT_HAVE_CLONE_NEWNET) + if (nxt_str_eq(&name, "network", 7)) { + flag = CLONE_NEWNET; + } +#endif + +#if (NXT_HAVE_CLONE_NEWUTS) + if (nxt_str_eq(&name, "uname", 5)) { + flag = CLONE_NEWUTS; + } +#endif + +#if (NXT_HAVE_CLONE_NEWNS) + if (nxt_str_eq(&name, "mount", 5)) { + flag = CLONE_NEWNS; + } +#endif + +#if (NXT_HAVE_CLONE_NEWCGROUP) + if (nxt_str_eq(&name, "cgroup", 6)) { + flag = CLONE_NEWCGROUP; + } +#endif + + if (!flag) { + nxt_alert(task, "unknown namespace flag: \"%V\"", &name); + return NXT_ERROR; + } + + if (nxt_conf_get_boolean(value)) { + clone->flags |= flag; + } + } + + return NXT_OK; +} + +#endif + + +#if (NXT_HAVE_ISOLATION_ROOTFS) + +static nxt_int_t +nxt_isolation_set_rootfs(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_str_t str; + nxt_conf_value_t *obj; + + static nxt_str_t rootfs_name = nxt_string("rootfs"); + + obj = nxt_conf_get_object_member(isolation, &rootfs_name, NULL); + if (obj != NULL) { + nxt_conf_get_string(obj, &str); + + if (nxt_slow_path(str.length <= 1 || str.start[0] != '/')) { + nxt_log(task, NXT_LOG_ERR, "rootfs requires an absolute path other " + "than \"/\" but given \"%V\"", &str); + + return NXT_ERROR; + } + + if (str.start[str.length - 1] == '/') { + str.length--; + } + + process->isolation.rootfs = nxt_mp_alloc(process->mem_pool, + str.length + 1); + + if (nxt_slow_path(process->isolation.rootfs == NULL)) { + return NXT_ERROR; + } + + nxt_memcpy(process->isolation.rootfs, str.start, str.length); + + process->isolation.rootfs[str.length] = '\0'; + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_set_mounts(nxt_task_t *task, nxt_process_t *process, + nxt_str_t *app_type) +{ + nxt_int_t ret, cap_chroot; + nxt_runtime_t *rt; + nxt_app_lang_module_t *lang; + + rt = task->thread->runtime; + cap_chroot = rt->capabilities.chroot; + lang = nxt_app_lang_module(rt, app_type); + + nxt_assert(lang != NULL); + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { + cap_chroot = 1; + } +#endif + + if (!cap_chroot) { + nxt_log(task, NXT_LOG_ERR, "The \"rootfs\" field requires privileges"); + return NXT_ERROR; + } + + if (lang->mounts != NULL && lang->mounts->nelts > 0) { + ret = nxt_isolation_set_lang_mounts(task, process, lang->mounts); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + + process->isolation.cleanup = nxt_isolation_unmount_all; + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, + nxt_array_t *lang_mounts) +{ + u_char *p; + size_t i, n, rootfs_len, len; + nxt_mp_t *mp; + nxt_array_t *mounts; + const u_char *rootfs; + nxt_fs_mount_t *mnt, *lang_mnt; + + rootfs = process->isolation.rootfs; + rootfs_len = nxt_strlen(rootfs); + mp = process->mem_pool; + + /* copy to init mem pool */ + mounts = nxt_array_copy(mp, NULL, lang_mounts); + if (mounts == NULL) { + return NXT_ERROR; + } + + n = mounts->nelts; + mnt = mounts->elts; + lang_mnt = lang_mounts->elts; + + for (i = 0; i < n; i++) { + len = nxt_strlen(lang_mnt[i].dst); + + mnt[i].dst = nxt_mp_alloc(mp, rootfs_len + len + 1); + if (mnt[i].dst == NULL) { + return NXT_ERROR; + } + + p = nxt_cpymem(mnt[i].dst, rootfs, rootfs_len); + p = nxt_cpymem(p, lang_mnt[i].dst, len); + *p = '\0'; + } + + process->isolation.mounts = mounts; + + return NXT_OK; +} + + +void +nxt_isolation_unmount_all(nxt_task_t *task, nxt_process_t *process) +{ + size_t i, n; + nxt_array_t *mounts; + nxt_fs_mount_t *mnt; + + nxt_debug(task, "unmount all (%s)", process->name); + + mounts = process->isolation.mounts; + n = mounts->nelts; + mnt = mounts->elts; + + for (i = 0; i < n; i++) { + nxt_fs_unmount(mnt[i].dst); + } +} + + +nxt_int_t +nxt_isolation_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) +{ + size_t i, n; + nxt_int_t ret, hasproc; + struct stat st; + nxt_array_t *mounts; + const u_char *dst; + nxt_fs_mount_t *mnt; + + hasproc = 0; + +#if (NXT_HAVE_CLONE_NEWPID) && (NXT_HAVE_CLONE_NEWNS) + nxt_fs_mount_t mount; + + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWPID) + && nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) + { + /* + * This mount point will automatically be gone when the namespace is + * destroyed. + */ + + mount.fstype = (u_char *) "proc"; + mount.src = (u_char *) "proc"; + mount.dst = (u_char *) "/proc"; + mount.data = (u_char *) ""; + mount.flags = 0; + + ret = nxt_fs_mkdir_all(mount.dst, S_IRWXU | S_IRWXG | S_IRWXO); + if (nxt_fast_path(ret == NXT_OK)) { + ret = nxt_fs_mount(task, &mount); + if (nxt_fast_path(ret == NXT_OK)) { + hasproc = 1; + } + + } else { + nxt_log(task, NXT_LOG_WARN, "mkdir(%s) %E", mount.dst, nxt_errno); + } + } +#endif + + mounts = process->isolation.mounts; + + n = mounts->nelts; + mnt = mounts->elts; + + for (i = 0; i < n; i++) { + dst = mnt[i].dst; + + if (nxt_slow_path(nxt_memcmp(mnt[i].fstype, "bind", 4) == 0 + && stat((const char *) mnt[i].src, &st) != 0)) + { + nxt_log(task, NXT_LOG_WARN, "host path not found: %s", mnt[i].src); + continue; + } + + if (hasproc && nxt_memcmp(mnt[i].fstype, "proc", 4) == 0 + && nxt_memcmp(mnt[i].dst, "/proc", 5) == 0) + { + continue; + } + + ret = nxt_fs_mkdir_all(dst, S_IRWXU | S_IRWXG | S_IRWXO); + if (nxt_slow_path(ret != NXT_OK)) { + nxt_alert(task, "mkdir(%s) %E", dst, nxt_errno); + goto undo; + } + + ret = nxt_fs_mount(task, &mnt[i]); + if (nxt_slow_path(ret != NXT_OK)) { + goto undo; + } + } + + return NXT_OK; + +undo: + + n = i + 1; + + for (i = 0; i < n; i++) { + nxt_fs_unmount(mnt[i].dst); + } + + return NXT_ERROR; +} + + +#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) + +nxt_int_t +nxt_isolation_change_root(nxt_task_t *task, nxt_process_t *process) +{ + char *rootfs; + nxt_int_t ret; + + rootfs = (char *) process->isolation.rootfs; + + nxt_debug(task, "change root: %s", rootfs); + + if (NXT_CLONE_MNT(process->isolation.clone.flags)) { + ret = nxt_isolation_pivot_root(task, rootfs); + + } else { + ret = nxt_isolation_chroot(task, rootfs); + } + + if (nxt_fast_path(ret == NXT_OK)) { + if (nxt_slow_path(chdir("/") < 0)) { + nxt_alert(task, "chdir(\"/\") %E", nxt_errno); + return NXT_ERROR; + } + } + + return ret; +} + + +/* + * pivot_root(2) can only be safely used with containers, otherwise it can + * umount(2) the global root filesystem and screw up the machine. + */ + +static nxt_int_t +nxt_isolation_pivot_root(nxt_task_t *task, const char *path) +{ + /* + * This implementation makes use of a kernel trick that works for ages + * and now documented in Linux kernel 5. + * https://lore.kernel.org/linux-man/87r24piwhm.fsf@x220.int.ebiederm.org/T/ + */ + + if (nxt_slow_path(mount("", "/", "", MS_SLAVE|MS_REC, "") != 0)) { + nxt_alert(task, "failed to make / a slave mount %E", nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(nxt_isolation_make_private_mount(task, path) != NXT_OK)) { + return NXT_ERROR; + } + + if (nxt_slow_path(mount(path, path, "bind", MS_BIND|MS_REC, "") != 0)) { + nxt_alert(task, "error bind mounting rootfs %E", nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(chdir(path) != 0)) { + nxt_alert(task, "failed to chdir(%s) %E", path, nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(nxt_pivot_root(".", ".") != 0)) { + nxt_alert(task, "failed to pivot_root %E", nxt_errno); + return NXT_ERROR; + } + + /* + * Make oldroot a slave mount to avoid unmounts getting propagated to the + * host. + */ + if (nxt_slow_path(mount("", ".", "", MS_SLAVE | MS_REC, NULL) != 0)) { + nxt_alert(task, "failed to bind mount rootfs %E", nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(umount2(".", MNT_DETACH) != 0)) { + nxt_alert(task, "failed to umount old root directory %E", nxt_errno); + return NXT_ERROR; + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_make_private_mount(nxt_task_t *task, const char *rootfs) +{ + char *parent_mnt; + FILE *procfile; + u_char **mounts; + size_t len; + uint8_t *shared; + nxt_int_t ret, index, nmounts; + struct mntent *ent; + + static const char *mount_path = "/proc/self/mounts"; + + ret = NXT_ERROR; + ent = NULL; + shared = NULL; + procfile = NULL; + parent_mnt = NULL; + + nmounts = 256; + + mounts = nxt_malloc(nmounts * sizeof(uintptr_t)); + if (nxt_slow_path(mounts == NULL)) { + goto fail; + } + + shared = nxt_malloc(nmounts); + if (nxt_slow_path(shared == NULL)) { + goto fail; + } + + procfile = setmntent(mount_path, "r"); + if (nxt_slow_path(procfile == NULL)) { + nxt_alert(task, "failed to open %s %E", mount_path, nxt_errno); + + goto fail; + } + + index = 0; + +again: + + for ( ; index < nmounts; index++) { + ent = getmntent(procfile); + if (ent == NULL) { + nmounts = index; + break; + } + + mounts[index] = (u_char *) strdup(ent->mnt_dir); + shared[index] = hasmntopt(ent, "shared") != NULL; + } + + if (ent != NULL) { + /* there are still entries to be read */ + + nmounts *= 2; + mounts = nxt_realloc(mounts, nmounts); + if (nxt_slow_path(mounts == NULL)) { + goto fail; + } + + shared = nxt_realloc(shared, nmounts); + if (nxt_slow_path(shared == NULL)) { + goto fail; + } + + goto again; + } + + for (index = 0; index < nmounts; index++) { + if (nxt_strcmp(mounts[index], rootfs) == 0) { + parent_mnt = (char *) rootfs; + break; + } + } + + if (parent_mnt == NULL) { + len = nxt_strlen(rootfs); + + parent_mnt = nxt_malloc(len + 1); + if (parent_mnt == NULL) { + goto fail; + } + + nxt_memcpy(parent_mnt, rootfs, len); + parent_mnt[len] = '\0'; + + if (parent_mnt[len - 1] == '/') { + parent_mnt[len - 1] = '\0'; + len--; + } + + for ( ;; ) { + for (index = 0; index < nmounts; index++) { + if (nxt_strcmp(mounts[index], parent_mnt) == 0) { + goto found; + } + } + + if (len == 1 && parent_mnt[0] == '/') { + nxt_alert(task, "parent mount not found"); + goto fail; + } + + /* parent dir */ + while (parent_mnt[len - 1] != '/' && len > 0) { + len--; + } + + if (nxt_slow_path(len == 0)) { + nxt_alert(task, "parent mount not found"); + goto fail; + } + + if (len == 1) { + parent_mnt[len] = '\0'; /* / */ + } else { + parent_mnt[len - 1] = '\0'; /* / */ + } + } + } + +found: + + if (shared[index]) { + if (nxt_slow_path(mount("", parent_mnt, "", MS_PRIVATE, "") != 0)) { + nxt_alert(task, "mount(\"\", \"%s\", MS_PRIVATE) %E", parent_mnt, + nxt_errno); + + goto fail; + } + } + + ret = NXT_OK; + +fail: + + if (procfile != NULL) { + endmntent(procfile); + } + + if (mounts != NULL) { + for (index = 0; index < nmounts; index++) { + nxt_free(mounts[index]); + } + + nxt_free(mounts); + } + + if (shared != NULL) { + nxt_free(shared); + } + + if (parent_mnt != NULL && parent_mnt != rootfs) { + nxt_free(parent_mnt); + } + + return ret; +} + + +nxt_inline int +nxt_pivot_root(const char *new_root, const char *old_root) +{ + return syscall(__NR_pivot_root, new_root, old_root); +} + + +#else /* !(NXT_HAVE_PIVOT_ROOT) || !(NXT_HAVE_CLONE_NEWNS) */ + + +nxt_int_t +nxt_isolation_change_root(nxt_task_t *task, nxt_process_t *process) +{ + char *rootfs; + + rootfs = (char *) process->isolation.rootfs; + + nxt_debug(task, "change root: %s", rootfs); + + if (nxt_fast_path(nxt_isolation_chroot(task, rootfs) == NXT_OK)) { + if (nxt_slow_path(chdir("/") < 0)) { + nxt_alert(task, "chdir(\"/\") %E", nxt_errno); + return NXT_ERROR; + } + + return NXT_OK; + } + + return NXT_ERROR; +} + +#endif + + +static nxt_int_t +nxt_isolation_chroot(nxt_task_t *task, const char *path) +{ + if (nxt_slow_path(chroot(path) < 0)) { + nxt_alert(task, "chroot(%s) %E", path, nxt_errno); + return NXT_ERROR; + } + + return NXT_OK; +} + +#endif /* NXT_HAVE_ISOLATION_ROOTFS */ + + +#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) + +static nxt_int_t +nxt_isolation_set_new_privs(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_conf_value_t *obj; + + static nxt_str_t new_privs_name = nxt_string("new_privs"); + + obj = nxt_conf_get_object_member(isolation, &new_privs_name, NULL); + if (obj != NULL) { + process->isolation.new_privs = nxt_conf_get_boolean(obj); + } + + return NXT_OK; +} + +#endif diff --git a/src/nxt_isolation.h b/src/nxt_isolation.h new file mode 100644 index 00000000..88a5f9e1 --- /dev/null +++ b/src/nxt_isolation.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_ISOLATION_H_ +#define _NXT_ISOLATION_H_ + + +nxt_int_t nxt_isolation_main_prefork(nxt_task_t *task, nxt_process_t *process, + nxt_mp_t *mp); + +#if (NXT_HAVE_ISOLATION_ROOTFS) +nxt_int_t nxt_isolation_prepare_rootfs(nxt_task_t *task, + nxt_process_t *process); +nxt_int_t nxt_isolation_change_root(nxt_task_t *task, nxt_process_t *process); +#endif + +#endif /* _NXT_ISOLATION_H_ */ diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c index 48eb2abb..a7b84b73 100644 --- a/src/nxt_main_process.c +++ b/src/nxt_main_process.c @@ -878,11 +878,9 @@ nxt_main_cleanup_process(nxt_task_t *task, nxt_pid_t pid) return; } -#if (NXT_HAVE_ISOLATION_ROOTFS) - if (process->isolation.rootfs != NULL && process->isolation.mounts) { - (void) nxt_process_unmount_all(task, process); + if (process->isolation.cleanup != NULL) { + process->isolation.cleanup(task, process); } -#endif name = process->name; stream = process->stream; diff --git a/src/nxt_process.c b/src/nxt_process.c index 9bfae395..9be7974f 100644 --- a/src/nxt_process.c +++ b/src/nxt_process.c @@ -17,10 +17,6 @@ #include #endif -#if (NXT_HAVE_PIVOT_ROOT) -#include -#endif - static nxt_int_t nxt_process_setup(nxt_task_t *task, nxt_process_t *process); static nxt_int_t nxt_process_child_fixup(nxt_task_t *task, nxt_process_t *process); @@ -33,16 +29,6 @@ static void nxt_process_created_ok(nxt_task_t *task, nxt_port_recv_msg_t *msg, static void nxt_process_created_error(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data); -#if (NXT_HAVE_ISOLATION_ROOTFS) -static nxt_int_t nxt_process_chroot(nxt_task_t *task, const char *path); - -#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) -static nxt_int_t nxt_process_pivot_root(nxt_task_t *task, const char *rootfs); -static nxt_int_t nxt_process_private_mount(nxt_task_t *task, - const char *rootfs); -static int nxt_pivot_root(const char *new_root, const char *old_root); -#endif -#endif /* A cached process pid. */ nxt_pid_t nxt_pid; @@ -398,51 +384,6 @@ nxt_process_core_setup(nxt_task_t *task, nxt_process_t *process) } -#if (NXT_HAVE_CLONE_NEWUSER) - -nxt_int_t -nxt_process_vldt_isolation_creds(nxt_task_t *task, nxt_process_t *process) -{ - nxt_int_t ret; - nxt_clone_t *clone; - nxt_credential_t *creds; - - clone = &process->isolation.clone; - creds = process->user_cred; - - if (clone->uidmap.size == 0 && clone->gidmap.size == 0) { - return NXT_OK; - } - - if (!nxt_is_clone_flag_set(clone->flags, NEWUSER)) { - if (nxt_slow_path(clone->uidmap.size > 0)) { - nxt_log(task, NXT_LOG_ERR, "\"uidmap\" is set but " - "\"isolation.namespaces.credential\" is false or unset"); - - return NXT_ERROR; - } - - if (nxt_slow_path(clone->gidmap.size > 0)) { - nxt_log(task, NXT_LOG_ERR, "\"gidmap\" is set but " - "\"isolation.namespaces.credential\" is false or unset"); - - return NXT_ERROR; - } - - return NXT_OK; - } - - ret = nxt_clone_vldt_credential_uidmap(task, &clone->uidmap, creds); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - - return nxt_clone_vldt_credential_gidmap(task, &clone->gidmap, creds); -} - -#endif - - nxt_int_t nxt_process_creds_set(nxt_task_t *task, nxt_process_t *process, nxt_str_t *user, nxt_str_t *group) @@ -525,329 +466,6 @@ nxt_process_apply_creds(nxt_task_t *task, nxt_process_t *process) } -#if (NXT_HAVE_ISOLATION_ROOTFS) - - -#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) - - -nxt_int_t -nxt_process_change_root(nxt_task_t *task, nxt_process_t *process) -{ - char *rootfs; - nxt_int_t ret; - - rootfs = (char *) process->isolation.rootfs; - - nxt_debug(task, "change root: %s", rootfs); - - if (NXT_CLONE_MNT(process->isolation.clone.flags)) { - ret = nxt_process_pivot_root(task, rootfs); - } else { - ret = nxt_process_chroot(task, rootfs); - } - - if (nxt_fast_path(ret == NXT_OK)) { - if (nxt_slow_path(chdir("/") < 0)) { - nxt_alert(task, "chdir(\"/\") %E", nxt_errno); - return NXT_ERROR; - } - } - - return ret; -} - - -#else - - -nxt_int_t -nxt_process_change_root(nxt_task_t *task, nxt_process_t *process) -{ - char *rootfs; - - rootfs = (char *) process->isolation.rootfs; - - nxt_debug(task, "change root: %s", rootfs); - - if (nxt_fast_path(nxt_process_chroot(task, rootfs) == NXT_OK)) { - if (nxt_slow_path(chdir("/") < 0)) { - nxt_alert(task, "chdir(\"/\") %E", nxt_errno); - return NXT_ERROR; - } - - return NXT_OK; - } - - return NXT_ERROR; -} - - -#endif - - -static nxt_int_t -nxt_process_chroot(nxt_task_t *task, const char *path) -{ - if (nxt_slow_path(chroot(path) < 0)) { - nxt_alert(task, "chroot(%s) %E", path, nxt_errno); - return NXT_ERROR; - } - - return NXT_OK; -} - - -void -nxt_process_unmount_all(nxt_task_t *task, nxt_process_t *process) -{ - size_t i, n; - nxt_array_t *mounts; - nxt_fs_mount_t *mnt; - - nxt_debug(task, "unmount all (%s)", process->name); - - mounts = process->isolation.mounts; - n = mounts->nelts; - mnt = mounts->elts; - - for (i = 0; i < n; i++) { - nxt_fs_unmount(mnt[i].dst); - } -} - - -#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) - -/* - * pivot_root(2) can only be safely used with containers, otherwise it can - * umount(2) the global root filesystem and screw up the machine. - */ - -static nxt_int_t -nxt_process_pivot_root(nxt_task_t *task, const char *path) -{ - /* - * This implementation makes use of a kernel trick that works for ages - * and now documented in Linux kernel 5. - * https://lore.kernel.org/linux-man/87r24piwhm.fsf@x220.int.ebiederm.org/T/ - */ - - if (nxt_slow_path(mount("", "/", "", MS_SLAVE|MS_REC, "") != 0)) { - nxt_alert(task, "failed to make / a slave mount %E", nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(nxt_process_private_mount(task, path) != NXT_OK)) { - return NXT_ERROR; - } - - if (nxt_slow_path(mount(path, path, "bind", MS_BIND|MS_REC, "") != 0)) { - nxt_alert(task, "error bind mounting rootfs %E", nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(chdir(path) != 0)) { - nxt_alert(task, "failed to chdir(%s) %E", path, nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(nxt_pivot_root(".", ".") != 0)) { - nxt_alert(task, "failed to pivot_root %E", nxt_errno); - return NXT_ERROR; - } - - /* - * Make oldroot a slave mount to avoid unmounts getting propagated to the - * host. - */ - if (nxt_slow_path(mount("", ".", "", MS_SLAVE | MS_REC, NULL) != 0)) { - nxt_alert(task, "failed to bind mount rootfs %E", nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(umount2(".", MNT_DETACH) != 0)) { - nxt_alert(task, "failed to umount old root directory %E", nxt_errno); - return NXT_ERROR; - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_process_private_mount(nxt_task_t *task, const char *rootfs) -{ - char *parent_mnt; - FILE *procfile; - u_char **mounts; - size_t len; - uint8_t *shared; - nxt_int_t ret, index, nmounts; - struct mntent *ent; - - static const char *mount_path = "/proc/self/mounts"; - - ret = NXT_ERROR; - ent = NULL; - shared = NULL; - procfile = NULL; - parent_mnt = NULL; - - nmounts = 256; - - mounts = nxt_malloc(nmounts * sizeof(uintptr_t)); - if (nxt_slow_path(mounts == NULL)) { - goto fail; - } - - shared = nxt_malloc(nmounts); - if (nxt_slow_path(shared == NULL)) { - goto fail; - } - - procfile = setmntent(mount_path, "r"); - if (nxt_slow_path(procfile == NULL)) { - nxt_alert(task, "failed to open %s %E", mount_path, nxt_errno); - - goto fail; - } - - index = 0; - -again: - - for ( ; index < nmounts; index++) { - ent = getmntent(procfile); - if (ent == NULL) { - nmounts = index; - break; - } - - mounts[index] = (u_char *) strdup(ent->mnt_dir); - shared[index] = hasmntopt(ent, "shared") != NULL; - } - - if (ent != NULL) { - /* there are still entries to be read */ - - nmounts *= 2; - mounts = nxt_realloc(mounts, nmounts); - if (nxt_slow_path(mounts == NULL)) { - goto fail; - } - - shared = nxt_realloc(shared, nmounts); - if (nxt_slow_path(shared == NULL)) { - goto fail; - } - - goto again; - } - - for (index = 0; index < nmounts; index++) { - if (nxt_strcmp(mounts[index], rootfs) == 0) { - parent_mnt = (char *) rootfs; - break; - } - } - - if (parent_mnt == NULL) { - len = nxt_strlen(rootfs); - - parent_mnt = nxt_malloc(len + 1); - if (parent_mnt == NULL) { - goto fail; - } - - nxt_memcpy(parent_mnt, rootfs, len); - parent_mnt[len] = '\0'; - - if (parent_mnt[len - 1] == '/') { - parent_mnt[len - 1] = '\0'; - len--; - } - - for ( ;; ) { - for (index = 0; index < nmounts; index++) { - if (nxt_strcmp(mounts[index], parent_mnt) == 0) { - goto found; - } - } - - if (len == 1 && parent_mnt[0] == '/') { - nxt_alert(task, "parent mount not found"); - goto fail; - } - - /* parent dir */ - while (parent_mnt[len - 1] != '/' && len > 0) { - len--; - } - - if (nxt_slow_path(len == 0)) { - nxt_alert(task, "parent mount not found"); - goto fail; - } - - if (len == 1) { - parent_mnt[len] = '\0'; /* / */ - } else { - parent_mnt[len - 1] = '\0'; /* / */ - } - } - } - -found: - - if (shared[index]) { - if (nxt_slow_path(mount("", parent_mnt, "", MS_PRIVATE, "") != 0)) { - nxt_alert(task, "mount(\"\", \"%s\", MS_PRIVATE) %E", parent_mnt, - nxt_errno); - - goto fail; - } - } - - ret = NXT_OK; - -fail: - - if (procfile != NULL) { - endmntent(procfile); - } - - if (mounts != NULL) { - for (index = 0; index < nmounts; index++) { - nxt_free(mounts[index]); - } - - nxt_free(mounts); - } - - if (shared != NULL) { - nxt_free(shared); - } - - if (parent_mnt != NULL && parent_mnt != rootfs) { - nxt_free(parent_mnt); - } - - return ret; -} - - -static int -nxt_pivot_root(const char *new_root, const char *old_root) -{ - return syscall(__NR_pivot_root, new_root, old_root); -} - -#endif - -#endif - - static nxt_int_t nxt_process_send_ready(nxt_task_t *task, nxt_process_t *process) { diff --git a/src/nxt_process.h b/src/nxt_process.h index ecd813e2..f2383687 100644 --- a/src/nxt_process.h +++ b/src/nxt_process.h @@ -60,6 +60,9 @@ typedef enum { typedef struct nxt_port_mmap_s nxt_port_mmap_t; +typedef struct nxt_process_s nxt_process_t; +typedef void (*nxt_isolation_cleanup_t)(nxt_task_t *task, + nxt_process_t *process); typedef struct { @@ -73,6 +76,8 @@ typedef struct { u_char *rootfs; nxt_array_t *mounts; /* of nxt_mount_t */ + nxt_isolation_cleanup_t cleanup; + #if (NXT_HAVE_CLONE) nxt_clone_t clone; #endif @@ -83,7 +88,7 @@ typedef struct { } nxt_process_isolation_t; -typedef struct { +struct nxt_process_s { nxt_pid_t pid; const char *name; nxt_queue_t ports; /* of nxt_port_t */ @@ -103,7 +108,7 @@ typedef struct { nxt_process_data_t data; nxt_process_isolation_t isolation; -} nxt_process_t; +}; typedef nxt_int_t (*nxt_process_prefork_t)(nxt_task_t *task, @@ -178,17 +183,6 @@ nxt_int_t nxt_process_creds_set(nxt_task_t *task, nxt_process_t *process, nxt_str_t *user, nxt_str_t *group); nxt_int_t nxt_process_apply_creds(nxt_task_t *task, nxt_process_t *process); -#if (NXT_HAVE_CLONE_NEWUSER) -nxt_int_t nxt_process_vldt_isolation_creds(nxt_task_t *task, - nxt_process_t *process); -#endif - -nxt_int_t nxt_process_change_root(nxt_task_t *task, nxt_process_t *process); - -#if (NXT_HAVE_ISOLATION_ROOTFS) -void nxt_process_unmount_all(nxt_task_t *task, nxt_process_t *process); -#endif - #if (NXT_HAVE_SETPROCTITLE) #define nxt_process_title(task, fmt, ...) \ -- cgit From 4ad8c5b8989d2772a2f68f9386bf31036e39ccdd Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Thu, 20 Aug 2020 15:44:29 +0100 Subject: Isolation: mount tmpfs by default. --- src/nxt_fs.c | 76 +++++++++++++++++++++++++++++-------- src/nxt_fs.h | 24 ++++++++++++ src/nxt_isolation.c | 105 ++++++++++++++++++++++++++++------------------------ src/nxt_runtime.c | 6 ++- src/nxt_string.h | 10 +++++ 5 files changed, 155 insertions(+), 66 deletions(-) diff --git a/src/nxt_fs.c b/src/nxt_fs.c index fe271802..0228c25a 100644 --- a/src/nxt_fs.c +++ b/src/nxt_fs.c @@ -40,30 +40,31 @@ nxt_fs_mount(nxt_task_t *task, nxt_fs_mount_t *mnt) nxt_int_t nxt_fs_mount(nxt_task_t *task, nxt_fs_mount_t *mnt) { + u_char *data, *p, *end; + size_t iovlen; + nxt_int_t ret; const char *fstype; - uint8_t is_bind, is_proc; - struct iovec iov[8]; + struct iovec iov[128]; char errmsg[256]; - is_bind = nxt_strncmp(mnt->fstype, "bind", 4) == 0; - is_proc = nxt_strncmp(mnt->fstype, "proc", 4) == 0; + if (nxt_strncmp(mnt->fstype, "bind", 4) == 0) { + fstype = "nullfs"; - if (nxt_slow_path(!is_bind && !is_proc)) { - nxt_alert(task, "mount type \"%s\" not implemented.", mnt->fstype); - return NXT_ERROR; - } + } else if (nxt_strncmp(mnt->fstype, "proc", 4) == 0) { + fstype = "procfs"; - if (is_bind) { - fstype = "nullfs"; + } else if (nxt_strncmp(mnt->fstype, "tmpfs", 5) == 0) { + fstype = "tmpfs"; } else { - fstype = "procfs"; + nxt_alert(task, "mount type \"%s\" not implemented.", mnt->fstype); + return NXT_ERROR; } iov[0].iov_base = (void *) "fstype"; iov[0].iov_len = 7; iov[1].iov_base = (void *) fstype; - iov[1].iov_len = strlen(fstype) + 1; + iov[1].iov_len = nxt_strlen(fstype) + 1; iov[2].iov_base = (void *) "fspath"; iov[2].iov_len = 7; iov[3].iov_base = (void *) mnt->dst; @@ -77,12 +78,55 @@ nxt_fs_mount(nxt_task_t *task, nxt_fs_mount_t *mnt) iov[7].iov_base = (void *) errmsg; iov[7].iov_len = sizeof(errmsg); - if (nxt_slow_path(nmount(iov, 8, 0) < 0)) { - nxt_alert(task, "nmount(%p, 8, 0) %s", errmsg); - return NXT_ERROR; + iovlen = 8; + + data = NULL; + + if (mnt->data != NULL) { + data = (u_char *) nxt_strdup(mnt->data); + if (nxt_slow_path(data == NULL)) { + return NXT_ERROR; + } + + end = data - 1; + + do { + p = end + 1; + end = nxt_strchr(p, '='); + if (end == NULL) { + break; + } + + *end = '\0'; + + iov[iovlen++].iov_base = (void *) p; + iov[iovlen++].iov_len = (end - p) + 1; + + p = end + 1; + + end = nxt_strchr(p, ','); + if (end != NULL) { + *end = '\0'; + } + + iov[iovlen++].iov_base = (void *) p; + iov[iovlen++].iov_len = nxt_strlen(p) + 1; + + } while (end != NULL && nxt_nitems(iov) > (iovlen + 2)); } - return NXT_OK; + ret = NXT_OK; + + if (nxt_slow_path(nmount(iov, iovlen, 0) < 0)) { + nxt_alert(task, "nmount(%p, %d, 0) %s", iov, iovlen, errmsg); + ret = NXT_ERROR; + } + + if (data != NULL) { + free(data); + } + + return ret; } #endif diff --git a/src/nxt_fs.h b/src/nxt_fs.h index 85c78b27..9f3266d8 100644 --- a/src/nxt_fs.h +++ b/src/nxt_fs.h @@ -18,6 +18,30 @@ #define NXT_MS_REC 0 #endif +#ifdef MS_NOSUID +#define NXT_MS_NOSUID MS_NOSUID +#else +#define NXT_MS_NOSUID 0 +#endif + +#ifdef MS_NOEXEC +#define NXT_MS_NOEXEC MS_NOEXEC +#else +#define NXT_MS_NOEXEC 0 +#endif + +#ifdef MS_RELATIME +#define NXT_MS_RELATIME MS_RELATIME +#else +#define NXT_MS_RELATIME 0 +#endif + +#ifdef MS_NODEV +#define NXT_MS_NODEV MS_NODEV +#else +#define NXT_MS_NODEV 0 +#endif + typedef struct { u_char *src; diff --git a/src/nxt_isolation.c b/src/nxt_isolation.c index 60de4324..586c0368 100644 --- a/src/nxt_isolation.c +++ b/src/nxt_isolation.c @@ -476,15 +476,13 @@ nxt_isolation_set_mounts(nxt_task_t *task, nxt_process_t *process, return NXT_ERROR; } - if (lang->mounts != NULL && lang->mounts->nelts > 0) { - ret = nxt_isolation_set_lang_mounts(task, process, lang->mounts); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - - process->isolation.cleanup = nxt_isolation_unmount_all; + ret = nxt_isolation_set_lang_mounts(task, process, lang->mounts); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; } + process->isolation.cleanup = nxt_isolation_unmount_all; + return NXT_OK; } @@ -500,8 +498,6 @@ nxt_isolation_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, const u_char *rootfs; nxt_fs_mount_t *mnt, *lang_mnt; - rootfs = process->isolation.rootfs; - rootfs_len = nxt_strlen(rootfs); mp = process->mem_pool; /* copy to init mem pool */ @@ -514,11 +510,14 @@ nxt_isolation_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, mnt = mounts->elts; lang_mnt = lang_mounts->elts; + rootfs = process->isolation.rootfs; + rootfs_len = nxt_strlen(rootfs); + for (i = 0; i < n; i++) { len = nxt_strlen(lang_mnt[i].dst); mnt[i].dst = nxt_mp_alloc(mp, rootfs_len + len + 1); - if (mnt[i].dst == NULL) { + if (nxt_slow_path(mnt[i].dst == NULL)) { return NXT_ERROR; } @@ -527,6 +526,52 @@ nxt_isolation_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, *p = '\0'; } + mnt = nxt_array_add(mounts); + if (nxt_slow_path(mnt == NULL)) { + return NXT_ERROR; + } + + mnt->src = (u_char *) "tmpfs"; + mnt->fstype = (u_char *) "tmpfs"; + mnt->flags = NXT_MS_NOSUID | NXT_MS_NODEV | NXT_MS_NOEXEC | NXT_MS_RELATIME; + mnt->data = (u_char *) "size=1m,mode=777"; + + mnt->dst = nxt_mp_nget(mp, rootfs_len + nxt_length("/tmp") + 1); + if (nxt_slow_path(mnt->dst == NULL)) { + return NXT_ERROR; + } + + p = nxt_cpymem(mnt->dst, rootfs, rootfs_len); + p = nxt_cpymem(p, "/tmp", 4); + *p = '\0'; + +#if (NXT_HAVE_CLONE_NEWPID) && (NXT_HAVE_CLONE_NEWNS) + + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWPID) + && nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) + { + mnt = nxt_array_add(mounts); + if (nxt_slow_path(mnt == NULL)) { + return NXT_ERROR; + } + + mnt->fstype = (u_char *) "proc"; + mnt->src = (u_char *) "proc"; + + mnt->dst = nxt_mp_nget(mp, rootfs_len + nxt_length("/proc") + 1); + if (nxt_slow_path(mnt->dst == NULL)) { + return NXT_ERROR; + } + + p = nxt_cpymem(mnt->dst, rootfs, rootfs_len); + p = nxt_cpymem(p, "/proc", 5); + *p = '\0'; + + mnt->data = (u_char *) ""; + mnt->flags = 0; + } +#endif + process->isolation.mounts = mounts; return NXT_OK; @@ -556,44 +601,12 @@ nxt_int_t nxt_isolation_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) { size_t i, n; - nxt_int_t ret, hasproc; + nxt_int_t ret; struct stat st; nxt_array_t *mounts; const u_char *dst; nxt_fs_mount_t *mnt; - hasproc = 0; - -#if (NXT_HAVE_CLONE_NEWPID) && (NXT_HAVE_CLONE_NEWNS) - nxt_fs_mount_t mount; - - if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWPID) - && nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) - { - /* - * This mount point will automatically be gone when the namespace is - * destroyed. - */ - - mount.fstype = (u_char *) "proc"; - mount.src = (u_char *) "proc"; - mount.dst = (u_char *) "/proc"; - mount.data = (u_char *) ""; - mount.flags = 0; - - ret = nxt_fs_mkdir_all(mount.dst, S_IRWXU | S_IRWXG | S_IRWXO); - if (nxt_fast_path(ret == NXT_OK)) { - ret = nxt_fs_mount(task, &mount); - if (nxt_fast_path(ret == NXT_OK)) { - hasproc = 1; - } - - } else { - nxt_log(task, NXT_LOG_WARN, "mkdir(%s) %E", mount.dst, nxt_errno); - } - } -#endif - mounts = process->isolation.mounts; n = mounts->nelts; @@ -609,12 +622,6 @@ nxt_isolation_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) continue; } - if (hasproc && nxt_memcmp(mnt[i].fstype, "proc", 4) == 0 - && nxt_memcmp(mnt[i].dst, "/proc", 5) == 0) - { - continue; - } - ret = nxt_fs_mkdir_all(dst, S_IRWXU | S_IRWXG | S_IRWXO); if (nxt_slow_path(ret != NXT_OK)) { nxt_alert(task, "mkdir(%s) %E", dst, nxt_errno); diff --git a/src/nxt_runtime.c b/src/nxt_runtime.c index 435276a0..44970b34 100644 --- a/src/nxt_runtime.c +++ b/src/nxt_runtime.c @@ -84,7 +84,11 @@ nxt_runtime_create(nxt_task_t *task) lang->version = (u_char *) ""; lang->file = NULL; lang->module = &nxt_external_module; - lang->mounts = NULL; + + lang->mounts = nxt_array_create(mp, 1, sizeof(nxt_fs_mount_t)); + if (nxt_slow_path(lang->mounts == NULL)) { + goto fail; + } listen_sockets = nxt_array_create(mp, 1, sizeof(nxt_listen_socket_t)); if (nxt_slow_path(listen_sockets == NULL)) { diff --git a/src/nxt_string.h b/src/nxt_string.h index 3f9192e2..7e02f59a 100644 --- a/src/nxt_string.h +++ b/src/nxt_string.h @@ -30,6 +30,16 @@ nxt_strlen(s) \ strlen((char *) s) +#define \ +nxt_strdup(s) \ + strdup((char *) s) + + +#define \ +nxt_strchr(buf, delim) \ + (u_char *) strchr((char *) buf, delim) + + #define \ nxt_memzero(buf, length) \ (void) memset(buf, 0, length) -- cgit From 10738c3fe22d2c46669630e55e465c5c8a84c77c Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Thu, 20 Aug 2020 15:53:20 +0100 Subject: Tests: default tmpfs in rootfs. --- test/test_go_isolation.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py index 61d39617..76434f62 100644 --- a/test/test_go_isolation.py +++ b/test/test_go_isolation.py @@ -327,6 +327,26 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson(url='/?file=/bin/sh')['body'] self.assertEqual(obj['FileExists'], False, 'file should not exists') + def test_go_isolation_rootfs_default_tmpfs(self): + if not self.isolation_key('unprivileged_userns_clone'): + print('unprivileged clone is not available') + raise unittest.SkipTest() + + if not self.isolation_key('mnt'): + print('mnt namespace is not supported') + raise unittest.SkipTest() + + isolation = { + 'namespaces': {'mount': True, 'credential': True}, + 'rootfs': self.testdir, + } + + self.load('ns_inspect', isolation=isolation) + + obj = self.getjson(url='/?file=/tmp')['body'] + + self.assertEqual(obj['FileExists'], True, 'app has /tmp') + if __name__ == '__main__': TestGoIsolation.main() -- cgit From dcfa92c161296af903fc1ebd4ddb98d1c38c59e7 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Fri, 21 Aug 2020 20:50:04 +0300 Subject: Configuration: removed "reschedule_timeout" option. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's not used since cbcd76704c90. This option is a leftover from previous IPC between router and applications processes. It was never documented, though. Thanks to 洪志道 (Hong Zhi Dao). --- src/nxt_conf_validation.c | 6 ------ src/nxt_router.c | 11 ----------- src/nxt_router.h | 1 - 3 files changed, 18 deletions(-) diff --git a/src/nxt_conf_validation.c b/src/nxt_conf_validation.c index 4c979782..16ed87c6 100644 --- a/src/nxt_conf_validation.c +++ b/src/nxt_conf_validation.c @@ -496,12 +496,6 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_app_limits_members[] = { NULL, NULL }, - { nxt_string("reschedule_timeout"), - NXT_CONF_VLDT_INTEGER, - 0, - NULL, - NULL }, - { nxt_string("requests"), NXT_CONF_VLDT_INTEGER, 0, diff --git a/src/nxt_router.c b/src/nxt_router.c index 0e1de6fa..c08bf7d7 100644 --- a/src/nxt_router.c +++ b/src/nxt_router.c @@ -24,7 +24,6 @@ typedef struct { uint32_t max_processes; uint32_t spare_processes; nxt_msec_t timeout; - nxt_msec_t res_timeout; nxt_msec_t idle_timeout; uint32_t requests; nxt_conf_value_t *limits_value; @@ -1157,12 +1156,6 @@ static nxt_conf_map_t nxt_router_app_limits_conf[] = { offsetof(nxt_router_app_conf_t, timeout), }, - { - nxt_string("reschedule_timeout"), - NXT_CONF_MAP_MSEC, - offsetof(nxt_router_app_conf_t, res_timeout), - }, - { nxt_string("requests"), NXT_CONF_MAP_INT32, @@ -1423,7 +1416,6 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, apcf.max_processes = 1; apcf.spare_processes = 0; apcf.timeout = 0; - apcf.res_timeout = 1000; apcf.idle_timeout = 15000; apcf.requests = 0; apcf.limits_value = NULL; @@ -1505,8 +1497,6 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_debug(task, "application type: %V", &apcf.type); nxt_debug(task, "application processes: %D", apcf.processes); nxt_debug(task, "application request timeout: %M", apcf.timeout); - nxt_debug(task, "application reschedule timeout: %M", - apcf.res_timeout); nxt_debug(task, "application requests: %D", apcf.requests); lang = nxt_app_lang_module(task->thread->runtime, &apcf.type); @@ -1537,7 +1527,6 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, app->max_pending_processes = apcf.spare_processes ? apcf.spare_processes : 1; app->timeout = apcf.timeout; - app->res_timeout = apcf.res_timeout * 1000000; app->idle_timeout = apcf.idle_timeout; app->max_requests = apcf.requests; diff --git a/src/nxt_router.h b/src/nxt_router.h index 81b3538c..512f1810 100644 --- a/src/nxt_router.h +++ b/src/nxt_router.h @@ -126,7 +126,6 @@ struct nxt_app_s { uint32_t max_requests; nxt_msec_t timeout; - nxt_nsec_t res_timeout; nxt_msec_t idle_timeout; nxt_str_t *targets; -- cgit From 30a242aa3cbfc3c3368ba0b6d2b4e579b31dc553 Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Tue, 25 Aug 2020 13:28:14 +0100 Subject: PHP: added bind mounts for extensions directory. --- auto/modules/php | 33 ++++++++++++++++++++++++++++++++- src/nxt_php_sapi.c | 6 ++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/auto/modules/php b/auto/modules/php index 75d60242..0ba82eae 100644 --- a/auto/modules/php +++ b/auto/modules/php @@ -59,6 +59,12 @@ NXT_PHP_MODULE=${NXT_PHP_MODULE=${NXT_PHP##*/}} NXT_PHP_LIB_PATH=${NXT_PHP_LIB_PATH=} NXT_PHP_LIB_STATIC=${NXT_PHP_LIB_STATIC=no} NXT_PHP_ADDITIONAL_FLAGS= +NXT_PHP_REALPATH=realpath + + +if [ -z `which $NXT_PHP_REALPATH` ]; then + NXT_PHP_REALPATH="readlink -e" +fi $echo "configuring PHP module" @@ -74,6 +80,14 @@ if /bin/sh -c "${NXT_PHP_CONFIG} --version" >> $NXT_AUTOCONF_ERR 2>&1; then $echo " found" NXT_PHP_VERSION="`${NXT_PHP_CONFIG} --version`" + NXT_PHP_EXT_DIR="`${NXT_PHP_CONFIG} --extension-dir`" + NXT_PHP_LIBC_DIR="`${CC} --print-file-name=libc.so`" + NXT_PHP_LIBC_DIR="`$NXT_PHP_REALPATH $NXT_PHP_LIBC_DIR`" + NXT_PHP_LIBC_DIR="`dirname $NXT_PHP_LIBC_DIR`" + NXT_PHP_SYSLIB_DIR="`${CC} --print-file-name=libtinfo.so`" + NXT_PHP_SYSLIB_DIR="`$NXT_PHP_REALPATH $NXT_PHP_SYSLIB_DIR`" + NXT_PHP_SYSLIB_DIR="`dirname $NXT_PHP_SYSLIB_DIR`" + $echo " + PHP SAPI: [`${NXT_PHP_CONFIG} --php-sapis`]" NXT_PHP_MAJOR_VERSION=${NXT_PHP_VERSION%%.*} @@ -213,6 +227,22 @@ if grep ^$NXT_PHP_MODULE: $NXT_MAKEFILE 2>&1 > /dev/null; then exit 1; fi + +NXT_PHP_MOUNTS_HEADER=nxt_${NXT_PHP_MODULE}_mounts.h + +cat << END > $NXT_BUILD_DIR/$NXT_PHP_MOUNTS_HEADER +static const nxt_fs_mount_t nxt_php_mounts[] = { + {(u_char *) "$NXT_PHP_EXT_DIR", (u_char *) "$NXT_PHP_EXT_DIR", + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + {(u_char *) "$NXT_PHP_LIBC_DIR", (u_char *) "$NXT_PHP_LIBC_DIR", + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + {(u_char *) "$NXT_PHP_SYSLIB_DIR", (u_char *) "$NXT_PHP_SYSLIB_DIR", + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, +}; + +END + + $echo " + PHP module: ${NXT_PHP_MODULE}.unit.so" . auto/cc/deps @@ -238,7 +268,8 @@ for nxt_src in $NXT_PHP_MODULE_SRCS; do cat << END >> $NXT_MAKEFILE $NXT_BUILD_DIR/$nxt_obj: $nxt_src $NXT_VERSION_H - \$(CC) -c \$(CFLAGS) $NXT_PHP_ADDITIONAL_FLAGS \$(NXT_INCS) \\ + \$(CC) -c \$(CFLAGS) -DNXT_PHP_MOUNTS_H=\"$NXT_PHP_MOUNTS_HEADER\" \\ + $NXT_PHP_ADDITIONAL_FLAGS \$(NXT_INCS) \\ $NXT_PHP_INCLUDE -DNXT_ZEND_SIGNAL_STARTUP=$NXT_ZEND_SIGNAL_STARTUP \\ $nxt_dep_flags \\ -o $NXT_BUILD_DIR/$nxt_obj $nxt_src diff --git a/src/nxt_php_sapi.c b/src/nxt_php_sapi.c index bc8341f4..d7e5b476 100644 --- a/src/nxt_php_sapi.c +++ b/src/nxt_php_sapi.c @@ -14,6 +14,8 @@ #include #include +#include NXT_PHP_MOUNTS_H + #if PHP_VERSION_ID >= 50400 #define NXT_HAVE_PHP_IGNORE_CWD 1 @@ -250,8 +252,8 @@ NXT_EXPORT nxt_app_module_t nxt_app_module = { compat, nxt_string("php"), PHP_VERSION, - NULL, - 0, + nxt_php_mounts, + nxt_nitems(nxt_php_mounts), NULL, nxt_php_start, }; -- cgit From 244ffb2829cfbac26e013988474184d6050504fe Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Tue, 25 Aug 2020 13:48:33 +0100 Subject: Tests: PHP extension mounts. --- test/php/list-extensions/index.php | 11 ++++++++ test/php/list-extensions/php.ini | 1 + test/test_php_isolation.py | 54 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 test/php/list-extensions/index.php create mode 100644 test/php/list-extensions/php.ini diff --git a/test/php/list-extensions/index.php b/test/php/list-extensions/index.php new file mode 100644 index 00000000..d6eb40d0 --- /dev/null +++ b/test/php/list-extensions/index.php @@ -0,0 +1,11 @@ + diff --git a/test/php/list-extensions/php.ini b/test/php/list-extensions/php.ini new file mode 100644 index 00000000..5848a0f2 --- /dev/null +++ b/test/php/list-extensions/php.ini @@ -0,0 +1 @@ +extension=json.so diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index 1b70ef02..abaf70a2 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -52,6 +52,60 @@ class TestPHPIsolation(TestApplicationPHP): self.assertEqual(self.get()['status'], 200, 'empty rootfs') + def test_php_isolation_rootfs_extensions(self): + isolation_features = self.available['features']['isolation'].keys() + + if not self.is_su: + if 'user' not in isolation_features: + print('requires unprivileged userns or root') + raise unittest.SkipTest() + + if not 'unprivileged_userns_clone' in isolation_features: + print('requires unprivileged userns or root') + raise unittest.SkipTest() + + if 'mnt' not in isolation_features: + print('requires mnt ns') + raise unittest.SkipTest() + + isolation = { + 'rootfs': self.current_dir, + 'namespaces': { + 'credential': not self.is_su, + 'mount': not self.is_su, + }, + } + + self.load('list-extensions', isolation=isolation) + + self.assertIn( + 'success', + self.conf( + '"/php/list-extensions"', 'applications/list-extensions/root' + ), + ) + + self.assertIn( + 'success', + self.conf( + {'file': '/php/list-extensions/php.ini'}, + 'applications/list-extensions/options', + ), + ) + + self.assertIn( + 'success', + self.conf( + '"/php/list-extensions"', + 'applications/list-extensions/working_directory', + ), + ) + + extensions = self.getjson()['body'] + + self.assertIn('json', extensions, 'json in extensions list') + self.assertIn('unit', extensions, 'unit in extensions list') + if __name__ == '__main__': TestPHPIsolation.main() -- cgit From b65a8636bb5b2ee61c69660aa6f7edc7d909e632 Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Tue, 25 Aug 2020 15:25:51 +0100 Subject: Isolation: added "automount" option. Now it's possible to disable default bind mounts of languages by setting: { "isolation": { "automount": { "language_deps": false } } } In this case, the user is responsible to provide a "rootfs" containing the language libraries and required files for the application. --- auto/modules/java | 6 ++--- auto/modules/php | 6 ++--- auto/modules/python | 2 +- auto/modules/ruby | 14 +++++----- src/nxt_conf_validation.c | 21 +++++++++++++++ src/nxt_fs.h | 11 ++++---- src/nxt_isolation.c | 67 ++++++++++++++++++++++++++++++++++++++++------- src/nxt_main_process.c | 2 ++ src/nxt_process.h | 17 ++++++++---- 9 files changed, 112 insertions(+), 34 deletions(-) diff --git a/auto/modules/java b/auto/modules/java index fa68f573..be8f443c 100644 --- a/auto/modules/java +++ b/auto/modules/java @@ -326,11 +326,11 @@ cat << END > $NXT_BUILD_DIR/$NXT_JAVA_MOUNTS_HEADER static const nxt_fs_mount_t nxt_java_mounts[] = { - {(u_char *) "proc", (u_char *) "/proc", (u_char *) "proc", 0, NULL}, + {(u_char *) "proc", (u_char *) "/proc", (u_char *) "proc", 0, NULL, 1}, {(u_char *) "$NXT_JAVA_LIBC_DIR", (u_char *) "$NXT_JAVA_LIBC_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_JAVA_HOME", (u_char *) "$NXT_JAVA_HOME", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, }; diff --git a/auto/modules/php b/auto/modules/php index 0ba82eae..848fc1bc 100644 --- a/auto/modules/php +++ b/auto/modules/php @@ -233,11 +233,11 @@ NXT_PHP_MOUNTS_HEADER=nxt_${NXT_PHP_MODULE}_mounts.h cat << END > $NXT_BUILD_DIR/$NXT_PHP_MOUNTS_HEADER static const nxt_fs_mount_t nxt_php_mounts[] = { {(u_char *) "$NXT_PHP_EXT_DIR", (u_char *) "$NXT_PHP_EXT_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_PHP_LIBC_DIR", (u_char *) "$NXT_PHP_LIBC_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_PHP_SYSLIB_DIR", (u_char *) "$NXT_PHP_SYSLIB_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, }; END diff --git a/auto/modules/python b/auto/modules/python index c14bf7e0..58d8f66f 100644 --- a/auto/modules/python +++ b/auto/modules/python @@ -138,7 +138,7 @@ pyver = "python" + str(sys.version_info[0]) + "." + str(sys.version_info[1]) print("static const nxt_fs_mount_t nxt_python_mounts[] = {") -pattern = "{(u_char *) \"%s\", (u_char *) \"%s\", (u_char *) \"bind\", NXT_MS_BIND|NXT_MS_REC, NULL}," +pattern = "{(u_char *) \"%s\", (u_char *) \"%s\", (u_char *) \"bind\", NXT_MS_BIND|NXT_MS_REC, NULL, 1}," base = None for p in sys.path: if len(p) > 0: diff --git a/auto/modules/ruby b/auto/modules/ruby index c1444f07..e0d54516 100644 --- a/auto/modules/ruby +++ b/auto/modules/ruby @@ -156,23 +156,23 @@ cat << END > $NXT_RUBY_MOUNTS_PATH static const nxt_fs_mount_t nxt_ruby_mounts[] = { {(u_char *) "$NXT_RUBY_RUBYHDRDIR", (u_char *) "$NXT_RUBY_RUBYHDRDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_ARCHHDRDIR", (u_char *) "$NXT_RUBY_ARCHHDRDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_SITEDIR", (u_char *) "$NXT_RUBY_SITEDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_LIBDIR", (u_char *) "$NXT_RUBY_LIBDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_TOPDIR", (u_char *) "$NXT_RUBY_TOPDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_PREFIXDIR", (u_char *) "$NXT_RUBY_PREFIXDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, END for path in `echo $NXT_RUBY_GEMPATH | tr ':' '\n'`; do $echo "{(u_char *) \"$path\", (u_char *) \"$path\"," >> $NXT_RUBY_MOUNTS_PATH - $echo "(u_char *) \"bind\", NXT_MS_BIND | NXT_MS_REC, NULL}," >> $NXT_RUBY_MOUNTS_PATH + $echo "(u_char *) \"bind\", NXT_MS_BIND | NXT_MS_REC, NULL, 1}," >> $NXT_RUBY_MOUNTS_PATH done $echo "};" >> $NXT_RUBY_MOUNTS_PATH diff --git a/src/nxt_conf_validation.c b/src/nxt_conf_validation.c index 16ed87c6..9dd4f715 100644 --- a/src/nxt_conf_validation.c +++ b/src/nxt_conf_validation.c @@ -616,6 +616,21 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_app_procmap_members[] = { #endif +#if (NXT_HAVE_ISOLATION_ROOTFS) + +static nxt_conf_vldt_object_t nxt_conf_vldt_app_automount_members[] = { + { nxt_string("language_deps"), + NXT_CONF_VLDT_BOOLEAN, + 0, + NULL, + NULL }, + + NXT_CONF_VLDT_END +}; + +#endif + + static nxt_conf_vldt_object_t nxt_conf_vldt_app_isolation_members[] = { { nxt_string("namespaces"), NXT_CONF_VLDT_OBJECT, @@ -647,6 +662,12 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_app_isolation_members[] = { NULL, NULL }, + { nxt_string("automount"), + NXT_CONF_VLDT_OBJECT, + 0, + &nxt_conf_vldt_object, + (void *) &nxt_conf_vldt_app_automount_members }, + #endif #if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) diff --git a/src/nxt_fs.h b/src/nxt_fs.h index 9f3266d8..bbd7ab9f 100644 --- a/src/nxt_fs.h +++ b/src/nxt_fs.h @@ -44,11 +44,12 @@ typedef struct { - u_char *src; - u_char *dst; - u_char *fstype; - nxt_int_t flags; - u_char *data; + u_char *src; + u_char *dst; + u_char *fstype; + nxt_int_t flags; + u_char *data; + nxt_uint_t builtin; /* 1-bit */ } nxt_fs_mount_t; diff --git a/src/nxt_isolation.c b/src/nxt_isolation.c index 586c0368..a497b729 100644 --- a/src/nxt_isolation.c +++ b/src/nxt_isolation.c @@ -35,6 +35,8 @@ static nxt_int_t nxt_isolation_vldt_creds(nxt_task_t *task, #if (NXT_HAVE_ISOLATION_ROOTFS) static nxt_int_t nxt_isolation_set_rootfs(nxt_task_t *task, nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_set_automount(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); static nxt_int_t nxt_isolation_set_mounts(nxt_task_t *task, nxt_process_t *process, nxt_str_t *app_type); static nxt_int_t nxt_isolation_set_lang_mounts(nxt_task_t *task, @@ -159,6 +161,12 @@ nxt_isolation_set(nxt_task_t *task, nxt_conf_value_t *isolation, { return NXT_ERROR; } + + if (nxt_slow_path(nxt_isolation_set_automount(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } #endif #if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) @@ -451,6 +459,32 @@ nxt_isolation_set_rootfs(nxt_task_t *task, nxt_conf_value_t *isolation, } +static nxt_int_t +nxt_isolation_set_automount(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_conf_value_t *conf, *value; + nxt_process_automount_t *automount; + + static nxt_str_t automount_name = nxt_string("automount"); + static nxt_str_t langdeps_name = nxt_string("language_deps"); + + automount = &process->isolation.automount; + + automount->language_deps = 1; + + conf = nxt_conf_get_object_member(isolation, &automount_name, NULL); + if (conf != NULL) { + value = nxt_conf_get_object_member(conf, &langdeps_name, NULL); + if (value != NULL) { + automount->language_deps = nxt_conf_get_boolean(value); + } + } + + return NXT_OK; +} + + static nxt_int_t nxt_isolation_set_mounts(nxt_task_t *task, nxt_process_t *process, nxt_str_t *app_type) @@ -535,6 +569,7 @@ nxt_isolation_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, mnt->fstype = (u_char *) "tmpfs"; mnt->flags = NXT_MS_NOSUID | NXT_MS_NODEV | NXT_MS_NOEXEC | NXT_MS_RELATIME; mnt->data = (u_char *) "size=1m,mode=777"; + mnt->builtin = 1; mnt->dst = nxt_mp_nget(mp, rootfs_len + nxt_length("/tmp") + 1); if (nxt_slow_path(mnt->dst == NULL)) { @@ -581,17 +616,23 @@ nxt_isolation_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, void nxt_isolation_unmount_all(nxt_task_t *task, nxt_process_t *process) { - size_t i, n; - nxt_array_t *mounts; - nxt_fs_mount_t *mnt; + size_t i, n; + nxt_array_t *mounts; + nxt_fs_mount_t *mnt; + nxt_process_automount_t *automount; nxt_debug(task, "unmount all (%s)", process->name); + automount = &process->isolation.automount; mounts = process->isolation.mounts; n = mounts->nelts; mnt = mounts->elts; for (i = 0; i < n; i++) { + if (mnt[i].builtin && !automount->language_deps) { + continue; + } + nxt_fs_unmount(mnt[i].dst); } } @@ -600,13 +641,15 @@ nxt_isolation_unmount_all(nxt_task_t *task, nxt_process_t *process) nxt_int_t nxt_isolation_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) { - size_t i, n; - nxt_int_t ret; - struct stat st; - nxt_array_t *mounts; - const u_char *dst; - nxt_fs_mount_t *mnt; - + size_t i, n; + nxt_int_t ret; + struct stat st; + nxt_array_t *mounts; + const u_char *dst; + nxt_fs_mount_t *mnt; + nxt_process_automount_t *automount; + + automount = &process->isolation.automount; mounts = process->isolation.mounts; n = mounts->nelts; @@ -615,6 +658,10 @@ nxt_isolation_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) for (i = 0; i < n; i++) { dst = mnt[i].dst; + if (mnt[i].builtin && !automount->language_deps) { + continue; + } + if (nxt_slow_path(nxt_memcmp(mnt[i].fstype, "bind", 4) == 0 && stat((const char *) mnt[i].src, &st) != 0)) { diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c index a7b84b73..544a0cbd 100644 --- a/src/nxt_main_process.c +++ b/src/nxt_main_process.c @@ -1290,6 +1290,8 @@ nxt_main_port_modules_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) goto fail; } + mnt->builtin = 1; + ret = nxt_conf_map_object(rt->mem_pool, value, nxt_app_lang_mounts_map, nxt_nitems(nxt_app_lang_mounts_map), mnt); diff --git a/src/nxt_process.h b/src/nxt_process.h index f2383687..d9b4dff1 100644 --- a/src/nxt_process.h +++ b/src/nxt_process.h @@ -72,18 +72,25 @@ typedef struct { nxt_port_mmap_t *elts; } nxt_port_mmaps_t; + +typedef struct { + uint8_t language_deps; /* 1-byte */ +} nxt_process_automount_t; + + typedef struct { - u_char *rootfs; - nxt_array_t *mounts; /* of nxt_mount_t */ + u_char *rootfs; + nxt_process_automount_t automount; + nxt_array_t *mounts; /* of nxt_mount_t */ - nxt_isolation_cleanup_t cleanup; + nxt_isolation_cleanup_t cleanup; #if (NXT_HAVE_CLONE) - nxt_clone_t clone; + nxt_clone_t clone; #endif #if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) - uint8_t new_privs; /* 1 bit */ + uint8_t new_privs; /* 1 bit */ #endif } nxt_process_isolation_t; -- cgit From d1bdaf98ba15cdba62e1f2ecb83c37e08728682c Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Tue, 25 Aug 2020 15:27:51 +0100 Subject: Tests: disable of language_deps. --- test/test_php_isolation.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index abaf70a2..3004a7b8 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -107,5 +107,74 @@ class TestPHPIsolation(TestApplicationPHP): self.assertIn('unit', extensions, 'unit in extensions list') + def test_php_isolation_rootfs_no_language_libs(self): + isolation_features = self.available['features']['isolation'].keys() + + if not self.is_su: + if 'user' not in isolation_features: + print('requires unprivileged userns or root') + raise unittest.SkipTest() + + if not 'unprivileged_userns_clone' in isolation_features: + print('requires unprivileged userns or root') + raise unittest.SkipTest() + + if 'mnt' not in isolation_features: + print('requires mnt ns') + raise unittest.SkipTest() + + isolation = { + 'rootfs': self.current_dir, + 'automount': {'language_deps': False}, + 'namespaces': { + 'credential': not self.is_su, + 'mount': not self.is_su, + }, + } + + self.load('list-extensions', isolation=isolation) + + self.assertIn( + 'success', + self.conf( + '"/php/list-extensions"', 'applications/list-extensions/root' + ), + ) + + self.assertIn( + 'success', + self.conf( + {'file': '/php/list-extensions/php.ini'}, + 'applications/list-extensions/options', + ), + ) + + self.assertIn( + 'success', + self.conf( + '"/php/list-extensions"', + 'applications/list-extensions/working_directory', + ), + ) + + extensions = self.getjson()['body'] + + self.assertIn('unit', extensions, 'unit in extensions list') + self.assertNotIn('json', extensions, 'json not in extensions list') + + self.assertIn( + 'success', + self.conf( + {'language_deps': True}, + 'applications/list-extensions/isolation/automount', + ), + ) + + extensions = self.getjson()['body'] + + self.assertIn('unit', extensions, 'unit in extensions list 2') + self.assertIn('json', extensions, 'json in extensions list 2') + + if __name__ == '__main__': TestPHPIsolation.main() -- cgit From 70c2a4645efd62ad8219f5f7a992436d2930f6b7 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Fri, 28 Aug 2020 19:34:49 +0300 Subject: Vars: added $host. This closes #407 issue on GitHub. --- src/nxt_http_variables.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/nxt_http_variables.c b/src/nxt_http_variables.c index 222d717c..1c0b561d 100644 --- a/src/nxt_http_variables.c +++ b/src/nxt_http_variables.c @@ -11,6 +11,8 @@ static nxt_int_t nxt_http_var_method(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, void *ctx); static nxt_int_t nxt_http_var_uri(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, void *ctx); +static nxt_int_t nxt_http_var_host(nxt_task_t *task, nxt_var_query_t *query, + nxt_str_t *str, void *ctx); static nxt_var_decl_t nxt_http_vars[] = { @@ -21,6 +23,10 @@ static nxt_var_decl_t nxt_http_vars[] = { { nxt_string("uri"), &nxt_http_var_uri, 0 }, + + { nxt_string("host"), + &nxt_http_var_host, + 0 }, }; @@ -57,3 +63,17 @@ nxt_http_var_uri(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, return NXT_OK; } + + +static nxt_int_t +nxt_http_var_host(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, + void *ctx) +{ + nxt_http_request_t *r; + + r = ctx; + + *str = r->host; + + return NXT_OK; +} -- cgit From df374057f7a42808d9482a8cac82111f24a104fa Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 31 Aug 2020 03:14:02 +0100 Subject: Tests: $host varaible test. Also added few tests for $uri and minor style fixes. --- test/test_variables.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/test/test_variables.py b/test/test_variables.py index 805c5144..fb481be5 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -16,10 +16,11 @@ class TestVariables(TestApplicationProto): "GET": [{"action": {"return": 201}}], "POST": [{"action": {"return": 202}}], "3": [{"action": {"return": 203}}], - "4": [{"action": {"return": 204}}], + "4*": [{"action": {"return": 204}}], "blahGET}": [{"action": {"return": 205}}], "5GET": [{"action": {"return": 206}}], "GETGET": [{"action": {"return": 207}}], + "localhost": [{"action": {"return": 208}}], }, }, ), @@ -27,10 +28,7 @@ class TestVariables(TestApplicationProto): ) def conf_routes(self, routes): - self.assertIn( - 'success', - self.conf(routes, 'listeners/*:7080/pass') - ) + self.assertIn('success', self.conf(routes, 'listeners/*:7080/pass')) def test_variables_method(self): self.assertEqual(self.get()['status'], 201, 'method GET') @@ -40,7 +38,26 @@ class TestVariables(TestApplicationProto): self.conf_routes("\"routes$uri\"") self.assertEqual(self.get(url='/3')['status'], 203, 'uri') - self.assertEqual(self.get(url='/4')['status'], 204, 'uri 2') + self.assertEqual(self.get(url='/4*')['status'], 204, 'uri 2') + self.assertEqual(self.get(url='/4%2A')['status'], 204, 'uri 3') + + def test_variables_host(self): + self.conf_routes("\"routes/$host\"") + + def check_host(host, status=208): + self.assertEqual( + self.get(headers={'Host': host, 'Connection': 'close'})[ + 'status' + ], + status, + ) + + check_host('localhost') + check_host('localhost.') + check_host('localhost:7080') + check_host('.localhost', 404) + check_host('www.localhost', 404) + check_host('localhost1', 404) def test_variables_many(self): self.conf_routes("\"routes$uri$method\"") @@ -69,7 +86,7 @@ class TestVariables(TestApplicationProto): self.assertEqual(self.post()['status'], 202) self.conf_routes("\"routes${uri}\"") - self.assertEqual(self.get(url='/4')['status'], 204) + self.assertEqual(self.get(url='/4*')['status'], 204) self.conf_routes("\"routes/blah$method}\"") self.assertEqual(self.get()['status'], 205) -- cgit From 22c88f0253d57756ad541326df09d1398a871708 Mon Sep 17 00:00:00 2001 From: Igor Sysoev Date: Mon, 7 Sep 2020 15:21:14 +0300 Subject: Upstream connection was not closed for short chunked response. --- src/nxt_http_proxy.c | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/nxt_http_proxy.c b/src/nxt_http_proxy.c index 34d0f36e..338d9fce 100644 --- a/src/nxt_http_proxy.c +++ b/src/nxt_http_proxy.c @@ -27,7 +27,6 @@ static void nxt_http_proxy_header_send(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_header_sent(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_header_read(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_send_body(nxt_task_t *task, void *obj, void *data); -static void nxt_http_proxy_read(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_buf_mem_completion(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_error(nxt_task_t *task, void *obj, void *data); @@ -275,39 +274,16 @@ nxt_http_proxy_header_read(nxt_task_t *task, void *obj, void *data) } -static void -nxt_http_proxy_send_body(nxt_task_t *task, void *obj, void *data) -{ - nxt_buf_t *out; - nxt_http_peer_t *peer; - nxt_http_request_t *r; - - r = obj; - peer = data; - out = peer->body; - - if (out != NULL) { - peer->body = NULL; - nxt_http_request_send(task, r, out); - - } - - if (!peer->closed) { - nxt_http_proto[peer->protocol].peer_read(task, peer); - } -} - - static const nxt_http_request_state_t nxt_http_proxy_read_state nxt_aligned(64) = { - .ready_handler = nxt_http_proxy_read, + .ready_handler = nxt_http_proxy_send_body, .error_handler = nxt_http_proxy_error, }; static void -nxt_http_proxy_read(nxt_task_t *task, void *obj, void *data) +nxt_http_proxy_send_body(nxt_task_t *task, void *obj, void *data) { nxt_buf_t *out; nxt_http_peer_t *peer; @@ -316,9 +292,9 @@ nxt_http_proxy_read(nxt_task_t *task, void *obj, void *data) r = obj; peer = data; out = peer->body; - peer->body = NULL; if (out != NULL) { + peer->body = NULL; nxt_http_request_send(task, r, out); } -- cgit From bd4ca6a057ca6e480a47112b3d379d72a8819e8c Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 10 Sep 2020 12:16:32 +0300 Subject: Fixing WebSocket frame retain function. Some of the pointers were not adjusted after frame's memory re-allocation. Fortunately, this function was not used and the bug has no effect. --- src/nxt_unit.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/nxt_unit.c b/src/nxt_unit.c index 6b7d631d..539c381c 100644 --- a/src/nxt_unit.c +++ b/src/nxt_unit.c @@ -3288,7 +3288,7 @@ int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) { char *b; - size_t size; + size_t size, hsize; nxt_unit_websocket_frame_impl_t *ws_impl; ws_impl = nxt_container_of(ws, nxt_unit_websocket_frame_impl_t, ws); @@ -3306,12 +3306,23 @@ nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) memcpy(b, ws_impl->buf->buf.start, size); + hsize = nxt_websocket_frame_header_size(b); + ws_impl->buf->buf.start = b; - ws_impl->buf->buf.free = b; + ws_impl->buf->buf.free = b + hsize; ws_impl->buf->buf.end = b + size; ws_impl->buf->free_ptr = b; + ws_impl->ws.header = (nxt_websocket_header_t *) b; + + if (ws_impl->ws.header->mask) { + ws_impl->ws.mask = (uint8_t *) b + hsize - 4; + + } else { + ws_impl->ws.mask = NULL; + } + return NXT_UNIT_OK; } -- cgit From d483aa74e61af411e40e98153a597d5a0473e2f1 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 14 Sep 2020 12:07:30 +0300 Subject: Python: source file moved to 'python' sub-directory. No functional changes. Get ready for an increase in file number. --- auto/modules/python | 3 +- src/nxt_python_wsgi.c | 1446 ------------------------------------------ src/python/nxt_python_wsgi.c | 1446 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1448 insertions(+), 1447 deletions(-) delete mode 100644 src/nxt_python_wsgi.c create mode 100644 src/python/nxt_python_wsgi.c diff --git a/auto/modules/python b/auto/modules/python index 58d8f66f..0e80d93f 100644 --- a/auto/modules/python +++ b/auto/modules/python @@ -167,7 +167,7 @@ $echo " + Python module: ${NXT_PYTHON_MODULE}.unit.so" $echo >> $NXT_MAKEFILE NXT_PYTHON_MODULE_SRCS=" \ - src/nxt_python_wsgi.c \ + src/python/nxt_python_wsgi.c \ " # The python module object files. @@ -185,6 +185,7 @@ for nxt_src in $NXT_PYTHON_MODULE_SRCS; do cat << END >> $NXT_MAKEFILE $NXT_BUILD_DIR/$nxt_obj: $nxt_src $NXT_VERSION_H + mkdir -p $NXT_BUILD_DIR/src/python \$(CC) -c \$(CFLAGS) -DNXT_PYTHON_MOUNTS_H=\"$NXT_PYTHON_MOUNTS_HEADER\" \\ \$(NXT_INCS) $NXT_PYTHON_INCLUDE \\ $nxt_dep_flags \\ diff --git a/src/nxt_python_wsgi.c b/src/nxt_python_wsgi.c deleted file mode 100644 index c4b7702e..00000000 --- a/src/nxt_python_wsgi.c +++ /dev/null @@ -1,1446 +0,0 @@ - -/* - * Copyright (C) Max Romanov - * Copyright (C) Valentin V. Bartenev - * Copyright (C) NGINX, Inc. - */ - - -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include NXT_PYTHON_MOUNTS_H - -/* - * According to "PEP 3333 / A Note On String Types" - * [https://www.python.org/dev/peps/pep-3333/#a-note-on-string-types] - * - * WSGI therefore defines two kinds of "string": - * - * - "Native" strings (which are always implemented using the type named str ) - * that are used for request/response headers and metadata - * - * will use PyString_* or corresponding PyUnicode_* functions - * - * - "Bytestrings" (which are implemented using the bytes type in Python 3, and - * str elsewhere), that are used for the bodies of requests and responses - * (e.g. POST/PUT input data and HTML page outputs). - * - * will use PyString_* or corresponding PyBytes_* functions - */ - - -#if PY_MAJOR_VERSION == 3 -#define NXT_PYTHON_BYTES_TYPE "bytestring" - -#define PyString_FromStringAndSize(str, size) \ - PyUnicode_DecodeLatin1((str), (size), "strict") - -#else -#define NXT_PYTHON_BYTES_TYPE "string" - -#define PyBytes_FromStringAndSize PyString_FromStringAndSize -#define PyBytes_Check PyString_Check -#define PyBytes_GET_SIZE PyString_GET_SIZE -#define PyBytes_AS_STRING PyString_AS_STRING -#define PyUnicode_InternInPlace PyString_InternInPlace -#define PyUnicode_AsUTF8 PyString_AS_STRING -#endif - -typedef struct nxt_python_run_ctx_s nxt_python_run_ctx_t; - -typedef struct { - PyObject_HEAD -} nxt_py_input_t; - - -typedef struct { - PyObject_HEAD -} nxt_py_error_t; - -static nxt_int_t nxt_python_start(nxt_task_t *task, - nxt_process_data_t *data); -static nxt_int_t nxt_python_init_strings(void); -static void nxt_python_request_handler(nxt_unit_request_info_t *req); -static void nxt_python_atexit(void); - -static PyObject *nxt_python_create_environ(nxt_task_t *task); -static PyObject *nxt_python_get_environ(nxt_python_run_ctx_t *ctx); -static int nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, - nxt_unit_sptr_t *sptr, uint32_t size); -static int nxt_python_add_field(nxt_python_run_ctx_t *ctx, - nxt_unit_field_t *field); -static int nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, - PyObject *value); - -static PyObject *nxt_py_start_resp(PyObject *self, PyObject *args); -static int nxt_python_response_add_field(nxt_python_run_ctx_t *ctx, - PyObject *name, PyObject *value, int i); -static int nxt_python_str_buf(PyObject *str, char **buf, uint32_t *len, - PyObject **bytes); -static PyObject *nxt_py_write(PyObject *self, PyObject *args); - -static void nxt_py_input_dealloc(nxt_py_input_t *self); -static PyObject *nxt_py_input_read(nxt_py_input_t *self, PyObject *args); -static PyObject *nxt_py_input_readline(nxt_py_input_t *self, PyObject *args); -static PyObject *nxt_py_input_getline(nxt_python_run_ctx_t *ctx, size_t size); -static PyObject *nxt_py_input_readlines(nxt_py_input_t *self, PyObject *args); - -static PyObject *nxt_py_input_iter(PyObject *self); -static PyObject *nxt_py_input_next(PyObject *self); - -static void nxt_python_print_exception(void); -static int nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes); - -struct nxt_python_run_ctx_s { - uint64_t content_length; - uint64_t bytes_sent; - PyObject *environ; - nxt_unit_request_info_t *req; -}; - -static uint32_t compat[] = { - NXT_VERNUM, NXT_DEBUG, -}; - - -NXT_EXPORT nxt_app_module_t nxt_app_module = { - sizeof(compat), - compat, - nxt_string("python"), - PY_VERSION, - nxt_python_mounts, - nxt_nitems(nxt_python_mounts), - NULL, - nxt_python_start, -}; - - -static PyMethodDef nxt_py_start_resp_method[] = { - {"unit_start_response", nxt_py_start_resp, METH_VARARGS, ""} -}; - - -static PyMethodDef nxt_py_write_method[] = { - {"unit_write", nxt_py_write, METH_O, ""} -}; - - -static PyMethodDef nxt_py_input_methods[] = { - { "read", (PyCFunction) nxt_py_input_read, METH_VARARGS, 0 }, - { "readline", (PyCFunction) nxt_py_input_readline, METH_VARARGS, 0 }, - { "readlines", (PyCFunction) nxt_py_input_readlines, METH_VARARGS, 0 }, - { NULL, NULL, 0, 0 } -}; - - -static PyTypeObject nxt_py_input_type = { - PyVarObject_HEAD_INIT(NULL, 0) - - .tp_name = "unit._input", - .tp_basicsize = sizeof(nxt_py_input_t), - .tp_dealloc = (destructor) nxt_py_input_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = "unit input object.", - .tp_iter = nxt_py_input_iter, - .tp_iternext = nxt_py_input_next, - .tp_methods = nxt_py_input_methods, -}; - - -static PyObject *nxt_py_stderr_flush; -static PyObject *nxt_py_application; -static PyObject *nxt_py_start_resp_obj; -static PyObject *nxt_py_write_obj; -static PyObject *nxt_py_environ_ptyp; - -#if PY_MAJOR_VERSION == 3 -static wchar_t *nxt_py_home; -#else -static char *nxt_py_home; -#endif - -static PyThreadState *nxt_python_thread_state; -static nxt_python_run_ctx_t *nxt_python_run_ctx; - - -static PyObject *nxt_py_80_str; -static PyObject *nxt_py_close_str; -static PyObject *nxt_py_content_length_str; -static PyObject *nxt_py_content_type_str; -static PyObject *nxt_py_http_str; -static PyObject *nxt_py_https_str; -static PyObject *nxt_py_path_info_str; -static PyObject *nxt_py_query_string_str; -static PyObject *nxt_py_remote_addr_str; -static PyObject *nxt_py_request_method_str; -static PyObject *nxt_py_request_uri_str; -static PyObject *nxt_py_server_addr_str; -static PyObject *nxt_py_server_name_str; -static PyObject *nxt_py_server_port_str; -static PyObject *nxt_py_server_protocol_str; -static PyObject *nxt_py_wsgi_uri_scheme_str; - -typedef struct { - nxt_str_t string; - PyObject **object_p; -} nxt_python_string_t; - -static nxt_python_string_t nxt_python_strings[] = { - { nxt_string("80"), &nxt_py_80_str }, - { nxt_string("close"), &nxt_py_close_str }, - { nxt_string("CONTENT_LENGTH"), &nxt_py_content_length_str }, - { nxt_string("CONTENT_TYPE"), &nxt_py_content_type_str }, - { nxt_string("http"), &nxt_py_http_str }, - { nxt_string("https"), &nxt_py_https_str }, - { nxt_string("PATH_INFO"), &nxt_py_path_info_str }, - { nxt_string("QUERY_STRING"), &nxt_py_query_string_str }, - { nxt_string("REMOTE_ADDR"), &nxt_py_remote_addr_str }, - { nxt_string("REQUEST_METHOD"), &nxt_py_request_method_str }, - { nxt_string("REQUEST_URI"), &nxt_py_request_uri_str }, - { nxt_string("SERVER_ADDR"), &nxt_py_server_addr_str }, - { nxt_string("SERVER_NAME"), &nxt_py_server_name_str }, - { nxt_string("SERVER_PORT"), &nxt_py_server_port_str }, - { nxt_string("SERVER_PROTOCOL"), &nxt_py_server_protocol_str }, - { nxt_string("wsgi.url_scheme"), &nxt_py_wsgi_uri_scheme_str }, -}; - - -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; - PyObject *obj, *pypath, *module; - nxt_unit_ctx_t *unit_ctx; - nxt_unit_init_t python_init; - 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"; - static const char bin_python[] = "/bin/python"; -#endif - - app_conf = data->app; - c = &app_conf->u.python; - - if (c->home != NULL) { - len = nxt_strlen(c->home); - -#if PY_MAJOR_VERSION == 3 - - path = nxt_malloc(len + sizeof(pyvenv)); - if (nxt_slow_path(path == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - nxt_memcpy(path, c->home, len); - nxt_memcpy(path + len, pyvenv, sizeof(pyvenv)); - - pep405 = (access(path, R_OK) == 0); - - nxt_free(path); - - if (pep405) { - size = (len + sizeof(bin_python)) * sizeof(wchar_t); - - } else { - size = (len + 1) * sizeof(wchar_t); - } - - nxt_py_home = nxt_malloc(size); - if (nxt_slow_path(nxt_py_home == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - if (pep405) { - mbstowcs(nxt_py_home, c->home, len); - mbstowcs(nxt_py_home + len, bin_python, sizeof(bin_python)); - Py_SetProgramName(nxt_py_home); - - } else { - mbstowcs(nxt_py_home, c->home, len + 1); - Py_SetPythonHome(nxt_py_home); - } - -#else - nxt_py_home = nxt_malloc(len + 1); - if (nxt_slow_path(nxt_py_home == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - nxt_memcpy(nxt_py_home, c->home, len + 1); - Py_SetPythonHome(nxt_py_home); -#endif - } - - Py_InitializeEx(0); - - module = NULL; - obj = NULL; - - if (nxt_slow_path(nxt_python_init_strings() != NXT_OK)) { - nxt_alert(task, "Python failed to init string objects"); - goto fail; - } - - obj = PySys_GetObject((char *) "stderr"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"sys.stderr\" object"); - goto fail; - } - - nxt_py_stderr_flush = PyObject_GetAttrString(obj, "flush"); - if (nxt_slow_path(nxt_py_stderr_flush == NULL)) { - nxt_alert(task, "Python failed to get \"flush\" attribute of " - "\"sys.stderr\" object"); - goto fail; - } - - Py_DECREF(obj); - - if (c->path.length > 0) { - obj = PyString_FromStringAndSize((char *) c->path.start, - c->path.length); - - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to create string object \"%V\"", - &c->path); - goto fail; - } - - pypath = PySys_GetObject((char *) "path"); - - if (nxt_slow_path(pypath == NULL)) { - nxt_alert(task, "Python failed to get \"sys.path\" list"); - goto fail; - } - - if (nxt_slow_path(PyList_Insert(pypath, 0, obj) != 0)) { - nxt_alert(task, "Python failed to insert \"%V\" into \"sys.path\"", - &c->path); - goto fail; - } - - Py_DECREF(obj); - } - - obj = PyCFunction_New(nxt_py_start_resp_method, NULL); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, - "Python failed to initialize the \"start_response\" function"); - goto fail; - } - - nxt_py_start_resp_obj = obj; - - obj = PyCFunction_New(nxt_py_write_method, NULL); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to initialize the \"write\" function"); - goto fail; - } - - nxt_py_write_obj = obj; - - obj = nxt_python_create_environ(task); - if (nxt_slow_path(obj == NULL)) { - goto fail; - } - - nxt_py_environ_ptyp = obj; - - obj = Py_BuildValue("[s]", "unit"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to create the \"sys.argv\" list"); - goto fail; - } - - if (nxt_slow_path(PySys_SetObject((char *) "argv", obj) != 0)) { - nxt_alert(task, "Python failed to set the \"sys.argv\" list"); - goto fail; - } - - 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'; - - 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(); - goto fail; - } - - obj = PyDict_GetItemString(PyModule_GetDict(module), "application"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"application\" " - "from module \"%s\"", nxt_py_module); - goto fail; - } - - if (nxt_slow_path(PyCallable_Check(obj) == 0)) { - nxt_alert(task, "\"application\" in module \"%s\" " - "is not a callable object", nxt_py_module); - goto fail; - } - - Py_INCREF(obj); - Py_CLEAR(module); - - nxt_py_application = obj; - obj = NULL; - - nxt_unit_default_init(task, &python_init); - - python_init.callbacks.request_handler = nxt_python_request_handler; - python_init.shm_limit = data->app->shm_limit; - - unit_ctx = nxt_unit_init(&python_init); - if (nxt_slow_path(unit_ctx == NULL)) { - goto fail; - } - - nxt_python_thread_state = PyEval_SaveThread(); - - rc = nxt_unit_run(unit_ctx); - - nxt_unit_done(unit_ctx); - - PyEval_RestoreThread(nxt_python_thread_state); - - nxt_python_atexit(); - - exit(rc); - - return NXT_OK; - -fail: - - Py_XDECREF(obj); - Py_XDECREF(module); - - nxt_python_atexit(); - - return NXT_ERROR; -} - - -static nxt_int_t -nxt_python_init_strings(void) -{ - PyObject *obj; - nxt_uint_t i; - nxt_python_string_t *pstr; - - for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { - pstr = &nxt_python_strings[i]; - - obj = PyString_FromStringAndSize((char *) pstr->string.start, - pstr->string.length); - if (nxt_slow_path(obj == NULL)) { - return NXT_ERROR; - } - - PyUnicode_InternInPlace(&obj); - - *pstr->object_p = obj; - } - - return NXT_OK; -} - - -static void -nxt_python_request_handler(nxt_unit_request_info_t *req) -{ - int rc; - PyObject *environ, *args, *response, *iterator, *item; - PyObject *close, *result; - nxt_python_run_ctx_t run_ctx = {-1, 0, NULL, req}; - - PyEval_RestoreThread(nxt_python_thread_state); - - environ = nxt_python_get_environ(&run_ctx); - if (nxt_slow_path(environ == NULL)) { - rc = NXT_UNIT_ERROR; - goto done; - } - - args = PyTuple_New(2); - if (nxt_slow_path(args == NULL)) { - Py_DECREF(environ); - - nxt_unit_req_error(req, "Python failed to create arguments tuple"); - - rc = NXT_UNIT_ERROR; - goto done; - } - - PyTuple_SET_ITEM(args, 0, environ); - - Py_INCREF(nxt_py_start_resp_obj); - PyTuple_SET_ITEM(args, 1, nxt_py_start_resp_obj); - - nxt_python_run_ctx = &run_ctx; - - response = PyObject_CallObject(nxt_py_application, args); - - Py_DECREF(args); - - if (nxt_slow_path(response == NULL)) { - nxt_unit_req_error(req, "Python failed to call the application"); - nxt_python_print_exception(); - - rc = NXT_UNIT_ERROR; - goto done; - } - - /* Shortcut: avoid iterate over response string symbols. */ - if (PyBytes_Check(response)) { - rc = nxt_python_write(&run_ctx, response); - - } else { - iterator = PyObject_GetIter(response); - - if (nxt_fast_path(iterator != NULL)) { - rc = NXT_UNIT_OK; - - while (run_ctx.bytes_sent < run_ctx.content_length) { - item = PyIter_Next(iterator); - - if (item == NULL) { - if (nxt_slow_path(PyErr_Occurred() != NULL)) { - nxt_unit_req_error(req, "Python failed to iterate over " - "the application response object"); - nxt_python_print_exception(); - - rc = NXT_UNIT_ERROR; - } - - break; - } - - if (nxt_fast_path(PyBytes_Check(item))) { - rc = nxt_python_write(&run_ctx, item); - - } else { - nxt_unit_req_error(req, "the application returned " - "not a bytestring object"); - rc = NXT_UNIT_ERROR; - } - - Py_DECREF(item); - - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - break; - } - } - - Py_DECREF(iterator); - - } else { - nxt_unit_req_error(req, - "the application returned not an iterable object"); - nxt_python_print_exception(); - - rc = NXT_UNIT_ERROR; - } - - close = PyObject_GetAttr(response, nxt_py_close_str); - - if (close != NULL) { - result = PyObject_CallFunction(close, NULL); - if (nxt_slow_path(result == NULL)) { - nxt_unit_req_error(req, "Python failed to call the close() " - "method of the application response"); - nxt_python_print_exception(); - - } else { - Py_DECREF(result); - } - - Py_DECREF(close); - - } else { - PyErr_Clear(); - } - } - - Py_DECREF(response); - -done: - - nxt_python_thread_state = PyEval_SaveThread(); - - nxt_python_run_ctx = NULL; - nxt_unit_request_done(req, rc); -} - - -static void -nxt_python_atexit(void) -{ - nxt_uint_t i; - - for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { - Py_XDECREF(*nxt_python_strings[i].object_p); - } - - Py_XDECREF(nxt_py_stderr_flush); - Py_XDECREF(nxt_py_application); - Py_XDECREF(nxt_py_start_resp_obj); - Py_XDECREF(nxt_py_write_obj); - Py_XDECREF(nxt_py_environ_ptyp); - - Py_Finalize(); - - if (nxt_py_home != NULL) { - nxt_free(nxt_py_home); - } -} - - -static PyObject * -nxt_python_create_environ(nxt_task_t *task) -{ - PyObject *obj, *err, *environ; - - environ = PyDict_New(); - - if (nxt_slow_path(environ == NULL)) { - nxt_alert(task, "Python failed to create the \"environ\" dictionary"); - return NULL; - } - - obj = PyString_FromStringAndSize((char *) nxt_server.start, - nxt_server.length); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, - "Python failed to create the \"SERVER_SOFTWARE\" environ value"); - goto fail; - } - - if (nxt_slow_path(PyDict_SetItemString(environ, "SERVER_SOFTWARE", obj) - != 0)) - { - nxt_alert(task, - "Python failed to set the \"SERVER_SOFTWARE\" environ value"); - goto fail; - } - - Py_DECREF(obj); - - obj = Py_BuildValue("(ii)", 1, 0); - - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, - "Python failed to build the \"wsgi.version\" environ value"); - goto fail; - } - - if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.version", obj) != 0)) - { - nxt_alert(task, - "Python failed to set the \"wsgi.version\" environ value"); - goto fail; - } - - Py_DECREF(obj); - obj = NULL; - - - if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.multithread", - Py_False) - != 0)) - { - nxt_alert(task, - "Python failed to set the \"wsgi.multithread\" environ value"); - goto fail; - } - - if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.multiprocess", - Py_True) - != 0)) - { - nxt_alert(task, - "Python failed to set the \"wsgi.multiprocess\" environ value"); - goto fail; - } - - if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.run_once", - Py_False) - != 0)) - { - nxt_alert(task, - "Python failed to set the \"wsgi.run_once\" environ value"); - goto fail; - } - - - if (nxt_slow_path(PyType_Ready(&nxt_py_input_type) != 0)) { - nxt_alert(task, - "Python failed to initialize the \"wsgi.input\" type object"); - goto fail; - } - - obj = (PyObject *) PyObject_New(nxt_py_input_t, &nxt_py_input_type); - - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to create the \"wsgi.input\" object"); - goto fail; - } - - if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.input", obj) != 0)) { - nxt_alert(task, - "Python failed to set the \"wsgi.input\" environ value"); - goto fail; - } - - Py_DECREF(obj); - obj = NULL; - - - err = PySys_GetObject((char *) "stderr"); - - if (nxt_slow_path(err == NULL)) { - nxt_alert(task, "Python failed to get \"sys.stderr\" object"); - goto fail; - } - - if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.errors", err) != 0)) - { - nxt_alert(task, - "Python failed to set the \"wsgi.errors\" environ value"); - goto fail; - } - - return environ; - -fail: - - Py_XDECREF(obj); - Py_DECREF(environ); - - return NULL; -} - - -static PyObject * -nxt_python_get_environ(nxt_python_run_ctx_t *ctx) -{ - int rc; - uint32_t i; - PyObject *environ; - nxt_unit_field_t *f; - nxt_unit_request_t *r; - - environ = PyDict_Copy(nxt_py_environ_ptyp); - if (nxt_slow_path(environ == NULL)) { - nxt_unit_req_error(ctx->req, - "Python failed to copy the \"environ\" dictionary"); - - return NULL; - } - - ctx->environ = environ; - - r = ctx->req->request; - -#define RC(S) \ - do { \ - rc = (S); \ - if (nxt_slow_path(rc != NXT_UNIT_OK)) { \ - goto fail; \ - } \ - } while(0) - - RC(nxt_python_add_sptr(ctx, nxt_py_request_method_str, &r->method, - r->method_length)); - RC(nxt_python_add_sptr(ctx, nxt_py_request_uri_str, &r->target, - r->target_length)); - RC(nxt_python_add_sptr(ctx, nxt_py_query_string_str, &r->query, - r->query_length)); - RC(nxt_python_add_sptr(ctx, nxt_py_path_info_str, &r->path, - r->path_length)); - - RC(nxt_python_add_sptr(ctx, nxt_py_remote_addr_str, &r->remote, - r->remote_length)); - RC(nxt_python_add_sptr(ctx, nxt_py_server_addr_str, &r->local, - r->local_length)); - - if (r->tls) { - RC(nxt_python_add_obj(ctx, nxt_py_wsgi_uri_scheme_str, - nxt_py_https_str)); - } else { - RC(nxt_python_add_obj(ctx, nxt_py_wsgi_uri_scheme_str, - nxt_py_http_str)); - } - - RC(nxt_python_add_sptr(ctx, nxt_py_server_protocol_str, &r->version, - r->version_length)); - - RC(nxt_python_add_sptr(ctx, nxt_py_server_name_str, &r->server_name, - r->server_name_length)); - RC(nxt_python_add_obj(ctx, nxt_py_server_port_str, nxt_py_80_str)); - - for (i = 0; i < r->fields_count; i++) { - f = r->fields + i; - - RC(nxt_python_add_field(ctx, f)); - } - - if (r->content_length_field != NXT_UNIT_NONE_FIELD) { - f = r->fields + r->content_length_field; - - RC(nxt_python_add_sptr(ctx, nxt_py_content_length_str, &f->value, - f->value_length)); - } - - if (r->content_type_field != NXT_UNIT_NONE_FIELD) { - f = r->fields + r->content_type_field; - - RC(nxt_python_add_sptr(ctx, nxt_py_content_type_str, &f->value, - f->value_length)); - } - -#undef RC - - return environ; - -fail: - - Py_DECREF(environ); - - return NULL; -} - - -static int -nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, - nxt_unit_sptr_t *sptr, uint32_t size) -{ - char *src; - PyObject *value; - - src = nxt_unit_sptr_get(sptr); - - value = PyString_FromStringAndSize(src, size); - if (nxt_slow_path(value == NULL)) { - nxt_unit_req_error(ctx->req, - "Python failed to create value string \"%.*s\"", - (int) size, src); - nxt_python_print_exception(); - - return NXT_UNIT_ERROR; - } - - if (nxt_slow_path(PyDict_SetItem(ctx->environ, name, value) != 0)) { - nxt_unit_req_error(ctx->req, - "Python failed to set the \"%s\" environ value", - PyUnicode_AsUTF8(name)); - Py_DECREF(value); - - return NXT_UNIT_ERROR; - } - - Py_DECREF(value); - - return NXT_UNIT_OK; -} - - -static int -nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field) -{ - char *src; - PyObject *name, *value; - - src = nxt_unit_sptr_get(&field->name); - - name = PyString_FromStringAndSize(src, field->name_length); - if (nxt_slow_path(name == NULL)) { - nxt_unit_req_error(ctx->req, - "Python failed to create name string \"%.*s\"", - (int) field->name_length, src); - nxt_python_print_exception(); - - return NXT_UNIT_ERROR; - } - - src = nxt_unit_sptr_get(&field->value); - - value = PyString_FromStringAndSize(src, field->value_length); - if (nxt_slow_path(value == NULL)) { - nxt_unit_req_error(ctx->req, - "Python failed to create value string \"%.*s\"", - (int) field->value_length, src); - nxt_python_print_exception(); - - goto fail; - } - - if (nxt_slow_path(PyDict_SetItem(ctx->environ, name, value) != 0)) { - nxt_unit_req_error(ctx->req, - "Python failed to set the \"%s\" environ value", - PyUnicode_AsUTF8(name)); - goto fail; - } - - Py_DECREF(name); - Py_DECREF(value); - - return NXT_UNIT_OK; - -fail: - - Py_DECREF(name); - Py_XDECREF(value); - - return NXT_UNIT_ERROR; -} - - -static int -nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, PyObject *value) -{ - if (nxt_slow_path(PyDict_SetItem(ctx->environ, name, value) != 0)) { - nxt_unit_req_error(ctx->req, - "Python failed to set the \"%s\" environ value", - PyUnicode_AsUTF8(name)); - - return NXT_UNIT_ERROR; - } - - return NXT_UNIT_OK; -} - - -static PyObject * -nxt_py_start_resp(PyObject *self, PyObject *args) -{ - int rc, status; - char *status_str, *space_ptr; - uint32_t status_len; - PyObject *headers, *tuple, *string, *status_bytes; - Py_ssize_t i, n, fields_size, fields_count; - nxt_python_run_ctx_t *ctx; - - ctx = nxt_python_run_ctx; - if (nxt_slow_path(ctx == NULL)) { - return PyErr_Format(PyExc_RuntimeError, - "start_response() is called " - "outside of WSGI request processing"); - } - - n = PyTuple_GET_SIZE(args); - - if (n < 2 || n > 3) { - return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); - } - - string = PyTuple_GET_ITEM(args, 0); - if (!PyBytes_Check(string) && !PyUnicode_Check(string)) { - return PyErr_Format(PyExc_TypeError, - "failed to write first argument (not a string?)"); - } - - headers = PyTuple_GET_ITEM(args, 1); - if (!PyList_Check(headers)) { - return PyErr_Format(PyExc_TypeError, - "the second argument is not a response headers list"); - } - - fields_size = 0; - fields_count = PyList_GET_SIZE(headers); - - for (i = 0; i < fields_count; i++) { - tuple = PyList_GET_ITEM(headers, i); - - if (!PyTuple_Check(tuple)) { - return PyErr_Format(PyExc_TypeError, - "the response headers must be a list of tuples"); - } - - if (PyTuple_GET_SIZE(tuple) != 2) { - return PyErr_Format(PyExc_TypeError, - "each header must be a tuple of two items"); - } - - string = PyTuple_GET_ITEM(tuple, 0); - if (PyBytes_Check(string)) { - fields_size += PyBytes_GET_SIZE(string); - - } else if (PyUnicode_Check(string)) { - fields_size += PyUnicode_GET_SIZE(string); - - } else { - return PyErr_Format(PyExc_TypeError, - "header #%d name is not a string", (int) i); - } - - string = PyTuple_GET_ITEM(tuple, 1); - if (PyBytes_Check(string)) { - fields_size += PyBytes_GET_SIZE(string); - - } else if (PyUnicode_Check(string)) { - fields_size += PyUnicode_GET_SIZE(string); - - } else { - return PyErr_Format(PyExc_TypeError, - "header #%d value is not a string", (int) i); - } - } - - ctx->content_length = -1; - - string = PyTuple_GET_ITEM(args, 0); - rc = nxt_python_str_buf(string, &status_str, &status_len, &status_bytes); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - return PyErr_Format(PyExc_TypeError, "status is not a string"); - } - - space_ptr = memchr(status_str, ' ', status_len); - if (space_ptr != NULL) { - status_len = space_ptr - status_str; - } - - status = nxt_int_parse((u_char *) status_str, status_len); - if (nxt_slow_path(status < 0)) { - return PyErr_Format(PyExc_TypeError, "failed to parse status code"); - } - - Py_XDECREF(status_bytes); - - /* - * PEP 3333: - * - * ... applications can replace their originally intended output with error - * output, up until the last possible moment. - */ - rc = nxt_unit_response_init(ctx->req, status, fields_count, fields_size); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - return PyErr_Format(PyExc_RuntimeError, - "failed to allocate response object"); - } - - for (i = 0; i < fields_count; i++) { - tuple = PyList_GET_ITEM(headers, i); - - rc = nxt_python_response_add_field(ctx, PyTuple_GET_ITEM(tuple, 0), - PyTuple_GET_ITEM(tuple, 1), i); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - return PyErr_Format(PyExc_RuntimeError, - "failed to add header #%d", (int) i); - } - } - - /* - * PEP 3333: - * - * However, the start_response callable must not actually transmit the - * response headers. Instead, it must store them for the server or gateway - * to transmit only after the first iteration of the application return - * value that yields a non-empty bytestring, or upon the application's - * first invocation of the write() callable. In other words, response - * headers must not be sent until there is actual body data available, or - * until the application's returned iterable is exhausted. (The only - * possible exception to this rule is if the response headers explicitly - * include a Content-Length of zero.) - */ - if (ctx->content_length == 0) { - rc = nxt_unit_response_send(ctx->req); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - return PyErr_Format(PyExc_RuntimeError, - "failed to send response headers"); - } - } - - Py_INCREF(nxt_py_write_obj); - return nxt_py_write_obj; -} - - -static int -nxt_python_response_add_field(nxt_python_run_ctx_t *ctx, PyObject *name, - PyObject *value, int i) -{ - int rc; - char *name_str, *value_str; - uint32_t name_length, value_length; - PyObject *name_bytes, *value_bytes; - nxt_off_t content_length; - - name_bytes = NULL; - value_bytes = NULL; - - rc = nxt_python_str_buf(name, &name_str, &name_length, &name_bytes); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - goto fail; - } - - rc = nxt_python_str_buf(value, &value_str, &value_length, &value_bytes); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - goto fail; - } - - rc = nxt_unit_response_add_field(ctx->req, name_str, name_length, - value_str, value_length); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - goto fail; - } - - if (ctx->req->response->fields[i].hash == NXT_UNIT_HASH_CONTENT_LENGTH) { - content_length = nxt_off_t_parse((u_char *) value_str, value_length); - if (nxt_slow_path(content_length < 0)) { - nxt_unit_req_error(ctx->req, "failed to parse Content-Length " - "value %.*s", (int) value_length, value_str); - - } else { - ctx->content_length = content_length; - } - } - -fail: - - Py_XDECREF(name_bytes); - Py_XDECREF(value_bytes); - - return rc; -} - - -static int -nxt_python_str_buf(PyObject *str, char **buf, uint32_t *len, PyObject **bytes) -{ - if (PyBytes_Check(str)) { - *buf = PyBytes_AS_STRING(str); - *len = PyBytes_GET_SIZE(str); - *bytes = NULL; - - } else { - *bytes = PyUnicode_AsLatin1String(str); - if (nxt_slow_path(*bytes == NULL)) { - return NXT_UNIT_ERROR; - } - - *buf = PyBytes_AS_STRING(*bytes); - *len = PyBytes_GET_SIZE(*bytes); - } - - return NXT_UNIT_OK; -} - - -static PyObject * -nxt_py_write(PyObject *self, PyObject *str) -{ - int rc; - - if (nxt_fast_path(!PyBytes_Check(str))) { - return PyErr_Format(PyExc_TypeError, "the argument is not a %s", - NXT_PYTHON_BYTES_TYPE); - } - - rc = nxt_python_write(nxt_python_run_ctx, str); - if (nxt_slow_path(rc != NXT_UNIT_OK)) { - return PyErr_Format(PyExc_RuntimeError, - "failed to write response value"); - } - - Py_RETURN_NONE; -} - - -static void -nxt_py_input_dealloc(nxt_py_input_t *self) -{ - PyObject_Del(self); -} - - -static PyObject * -nxt_py_input_read(nxt_py_input_t *self, PyObject *args) -{ - char *buf; - PyObject *content, *obj; - Py_ssize_t size, n; - nxt_python_run_ctx_t *ctx; - - ctx = nxt_python_run_ctx; - if (nxt_slow_path(ctx == NULL)) { - return PyErr_Format(PyExc_RuntimeError, - "wsgi.input.read() is called " - "outside of WSGI request processing"); - } - - size = ctx->req->content_length; - - n = PyTuple_GET_SIZE(args); - - if (n > 0) { - if (n != 1) { - return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); - } - - obj = PyTuple_GET_ITEM(args, 0); - - size = PyNumber_AsSsize_t(obj, PyExc_OverflowError); - - if (nxt_slow_path(size < 0)) { - if (size == -1 && PyErr_Occurred()) { - return NULL; - } - - if (size != -1) { - return PyErr_Format(PyExc_ValueError, - "the read body size cannot be zero or less"); - } - } - - if (size == -1 || size > (Py_ssize_t) ctx->req->content_length) { - size = ctx->req->content_length; - } - } - - content = PyBytes_FromStringAndSize(NULL, size); - if (nxt_slow_path(content == NULL)) { - return NULL; - } - - buf = PyBytes_AS_STRING(content); - - size = nxt_unit_request_read(ctx->req, buf, size); - - return content; -} - - -static PyObject * -nxt_py_input_readline(nxt_py_input_t *self, PyObject *args) -{ - ssize_t ssize; - PyObject *obj; - Py_ssize_t n; - nxt_python_run_ctx_t *ctx; - - ctx = nxt_python_run_ctx; - if (nxt_slow_path(ctx == NULL)) { - return PyErr_Format(PyExc_RuntimeError, - "wsgi.input.readline() is called " - "outside of WSGI request processing"); - } - - n = PyTuple_GET_SIZE(args); - - if (n > 0) { - if (n != 1) { - return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); - } - - obj = PyTuple_GET_ITEM(args, 0); - - ssize = PyNumber_AsSsize_t(obj, PyExc_OverflowError); - - if (nxt_fast_path(ssize > 0)) { - return nxt_py_input_getline(ctx, ssize); - } - - if (ssize == 0) { - return PyBytes_FromStringAndSize("", 0); - } - - if (ssize != -1) { - return PyErr_Format(PyExc_ValueError, - "the read line size cannot be zero or less"); - } - - if (PyErr_Occurred()) { - return NULL; - } - } - - return nxt_py_input_getline(ctx, SSIZE_MAX); -} - - -static PyObject * -nxt_py_input_getline(nxt_python_run_ctx_t *ctx, size_t size) -{ - void *buf; - ssize_t res; - PyObject *content; - - res = nxt_unit_request_readline_size(ctx->req, size); - if (nxt_slow_path(res < 0)) { - return NULL; - } - - if (res == 0) { - return PyBytes_FromStringAndSize("", 0); - } - - content = PyBytes_FromStringAndSize(NULL, res); - if (nxt_slow_path(content == NULL)) { - return NULL; - } - - buf = PyBytes_AS_STRING(content); - - res = nxt_unit_request_read(ctx->req, buf, res); - - return content; -} - - -static PyObject * -nxt_py_input_readlines(nxt_py_input_t *self, PyObject *args) -{ - PyObject *res; - nxt_python_run_ctx_t *ctx; - - ctx = nxt_python_run_ctx; - if (nxt_slow_path(ctx == NULL)) { - return PyErr_Format(PyExc_RuntimeError, - "wsgi.input.readlines() is called " - "outside of WSGI request processing"); - } - - res = PyList_New(0); - if (nxt_slow_path(res == NULL)) { - return NULL; - } - - for ( ;; ) { - PyObject *line = nxt_py_input_getline(ctx, SSIZE_MAX); - if (nxt_slow_path(line == NULL)) { - Py_DECREF(res); - return NULL; - } - - if (PyBytes_GET_SIZE(line) == 0) { - Py_DECREF(line); - return res; - } - - PyList_Append(res, line); - Py_DECREF(line); - } - - return res; -} - - -static PyObject * -nxt_py_input_iter(PyObject *self) -{ - Py_INCREF(self); - return self; -} - - -static PyObject * -nxt_py_input_next(PyObject *self) -{ - PyObject *line; - nxt_python_run_ctx_t *ctx; - - ctx = nxt_python_run_ctx; - if (nxt_slow_path(ctx == NULL)) { - return PyErr_Format(PyExc_RuntimeError, - "wsgi.input.next() is called " - "outside of WSGI request processing"); - } - - line = nxt_py_input_getline(ctx, SSIZE_MAX); - if (nxt_slow_path(line == NULL)) { - return NULL; - } - - if (PyBytes_GET_SIZE(line) == 0) { - Py_DECREF(line); - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - - return line; -} - - -static void -nxt_python_print_exception(void) -{ - PyErr_Print(); - -#if PY_MAJOR_VERSION == 3 - /* The backtrace may be buffered in sys.stderr file object. */ - { - PyObject *result; - - result = PyObject_CallFunction(nxt_py_stderr_flush, NULL); - if (nxt_slow_path(result == NULL)) { - PyErr_Clear(); - return; - } - - Py_DECREF(result); - } -#endif -} - - -static int -nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes) -{ - int rc; - char *str_buf; - uint32_t str_length; - - str_buf = PyBytes_AS_STRING(bytes); - str_length = PyBytes_GET_SIZE(bytes); - - if (nxt_slow_path(str_length == 0)) { - return NXT_UNIT_OK; - } - - /* - * PEP 3333: - * - * If the application supplies a Content-Length header, the server should - * not transmit more bytes to the client than the header allows, and should - * stop iterating over the response when enough data has been sent, or raise - * an error if the application tries to write() past that point. - */ - if (nxt_slow_path(str_length > ctx->content_length - ctx->bytes_sent)) { - nxt_unit_req_error(ctx->req, "content length %"PRIu64" exceeded", - ctx->content_length); - - return NXT_UNIT_ERROR; - } - - rc = nxt_unit_response_write(ctx->req, str_buf, str_length); - if (nxt_fast_path(rc == NXT_UNIT_OK)) { - ctx->bytes_sent += str_length; - } - - return rc; -} diff --git a/src/python/nxt_python_wsgi.c b/src/python/nxt_python_wsgi.c new file mode 100644 index 00000000..c4b7702e --- /dev/null +++ b/src/python/nxt_python_wsgi.c @@ -0,0 +1,1446 @@ + +/* + * Copyright (C) Max Romanov + * Copyright (C) Valentin V. Bartenev + * Copyright (C) NGINX, Inc. + */ + + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include NXT_PYTHON_MOUNTS_H + +/* + * According to "PEP 3333 / A Note On String Types" + * [https://www.python.org/dev/peps/pep-3333/#a-note-on-string-types] + * + * WSGI therefore defines two kinds of "string": + * + * - "Native" strings (which are always implemented using the type named str ) + * that are used for request/response headers and metadata + * + * will use PyString_* or corresponding PyUnicode_* functions + * + * - "Bytestrings" (which are implemented using the bytes type in Python 3, and + * str elsewhere), that are used for the bodies of requests and responses + * (e.g. POST/PUT input data and HTML page outputs). + * + * will use PyString_* or corresponding PyBytes_* functions + */ + + +#if PY_MAJOR_VERSION == 3 +#define NXT_PYTHON_BYTES_TYPE "bytestring" + +#define PyString_FromStringAndSize(str, size) \ + PyUnicode_DecodeLatin1((str), (size), "strict") + +#else +#define NXT_PYTHON_BYTES_TYPE "string" + +#define PyBytes_FromStringAndSize PyString_FromStringAndSize +#define PyBytes_Check PyString_Check +#define PyBytes_GET_SIZE PyString_GET_SIZE +#define PyBytes_AS_STRING PyString_AS_STRING +#define PyUnicode_InternInPlace PyString_InternInPlace +#define PyUnicode_AsUTF8 PyString_AS_STRING +#endif + +typedef struct nxt_python_run_ctx_s nxt_python_run_ctx_t; + +typedef struct { + PyObject_HEAD +} nxt_py_input_t; + + +typedef struct { + PyObject_HEAD +} nxt_py_error_t; + +static nxt_int_t nxt_python_start(nxt_task_t *task, + nxt_process_data_t *data); +static nxt_int_t nxt_python_init_strings(void); +static void nxt_python_request_handler(nxt_unit_request_info_t *req); +static void nxt_python_atexit(void); + +static PyObject *nxt_python_create_environ(nxt_task_t *task); +static PyObject *nxt_python_get_environ(nxt_python_run_ctx_t *ctx); +static int nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, + nxt_unit_sptr_t *sptr, uint32_t size); +static int nxt_python_add_field(nxt_python_run_ctx_t *ctx, + nxt_unit_field_t *field); +static int nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, + PyObject *value); + +static PyObject *nxt_py_start_resp(PyObject *self, PyObject *args); +static int nxt_python_response_add_field(nxt_python_run_ctx_t *ctx, + PyObject *name, PyObject *value, int i); +static int nxt_python_str_buf(PyObject *str, char **buf, uint32_t *len, + PyObject **bytes); +static PyObject *nxt_py_write(PyObject *self, PyObject *args); + +static void nxt_py_input_dealloc(nxt_py_input_t *self); +static PyObject *nxt_py_input_read(nxt_py_input_t *self, PyObject *args); +static PyObject *nxt_py_input_readline(nxt_py_input_t *self, PyObject *args); +static PyObject *nxt_py_input_getline(nxt_python_run_ctx_t *ctx, size_t size); +static PyObject *nxt_py_input_readlines(nxt_py_input_t *self, PyObject *args); + +static PyObject *nxt_py_input_iter(PyObject *self); +static PyObject *nxt_py_input_next(PyObject *self); + +static void nxt_python_print_exception(void); +static int nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes); + +struct nxt_python_run_ctx_s { + uint64_t content_length; + uint64_t bytes_sent; + PyObject *environ; + nxt_unit_request_info_t *req; +}; + +static uint32_t compat[] = { + NXT_VERNUM, NXT_DEBUG, +}; + + +NXT_EXPORT nxt_app_module_t nxt_app_module = { + sizeof(compat), + compat, + nxt_string("python"), + PY_VERSION, + nxt_python_mounts, + nxt_nitems(nxt_python_mounts), + NULL, + nxt_python_start, +}; + + +static PyMethodDef nxt_py_start_resp_method[] = { + {"unit_start_response", nxt_py_start_resp, METH_VARARGS, ""} +}; + + +static PyMethodDef nxt_py_write_method[] = { + {"unit_write", nxt_py_write, METH_O, ""} +}; + + +static PyMethodDef nxt_py_input_methods[] = { + { "read", (PyCFunction) nxt_py_input_read, METH_VARARGS, 0 }, + { "readline", (PyCFunction) nxt_py_input_readline, METH_VARARGS, 0 }, + { "readlines", (PyCFunction) nxt_py_input_readlines, METH_VARARGS, 0 }, + { NULL, NULL, 0, 0 } +}; + + +static PyTypeObject nxt_py_input_type = { + PyVarObject_HEAD_INIT(NULL, 0) + + .tp_name = "unit._input", + .tp_basicsize = sizeof(nxt_py_input_t), + .tp_dealloc = (destructor) nxt_py_input_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "unit input object.", + .tp_iter = nxt_py_input_iter, + .tp_iternext = nxt_py_input_next, + .tp_methods = nxt_py_input_methods, +}; + + +static PyObject *nxt_py_stderr_flush; +static PyObject *nxt_py_application; +static PyObject *nxt_py_start_resp_obj; +static PyObject *nxt_py_write_obj; +static PyObject *nxt_py_environ_ptyp; + +#if PY_MAJOR_VERSION == 3 +static wchar_t *nxt_py_home; +#else +static char *nxt_py_home; +#endif + +static PyThreadState *nxt_python_thread_state; +static nxt_python_run_ctx_t *nxt_python_run_ctx; + + +static PyObject *nxt_py_80_str; +static PyObject *nxt_py_close_str; +static PyObject *nxt_py_content_length_str; +static PyObject *nxt_py_content_type_str; +static PyObject *nxt_py_http_str; +static PyObject *nxt_py_https_str; +static PyObject *nxt_py_path_info_str; +static PyObject *nxt_py_query_string_str; +static PyObject *nxt_py_remote_addr_str; +static PyObject *nxt_py_request_method_str; +static PyObject *nxt_py_request_uri_str; +static PyObject *nxt_py_server_addr_str; +static PyObject *nxt_py_server_name_str; +static PyObject *nxt_py_server_port_str; +static PyObject *nxt_py_server_protocol_str; +static PyObject *nxt_py_wsgi_uri_scheme_str; + +typedef struct { + nxt_str_t string; + PyObject **object_p; +} nxt_python_string_t; + +static nxt_python_string_t nxt_python_strings[] = { + { nxt_string("80"), &nxt_py_80_str }, + { nxt_string("close"), &nxt_py_close_str }, + { nxt_string("CONTENT_LENGTH"), &nxt_py_content_length_str }, + { nxt_string("CONTENT_TYPE"), &nxt_py_content_type_str }, + { nxt_string("http"), &nxt_py_http_str }, + { nxt_string("https"), &nxt_py_https_str }, + { nxt_string("PATH_INFO"), &nxt_py_path_info_str }, + { nxt_string("QUERY_STRING"), &nxt_py_query_string_str }, + { nxt_string("REMOTE_ADDR"), &nxt_py_remote_addr_str }, + { nxt_string("REQUEST_METHOD"), &nxt_py_request_method_str }, + { nxt_string("REQUEST_URI"), &nxt_py_request_uri_str }, + { nxt_string("SERVER_ADDR"), &nxt_py_server_addr_str }, + { nxt_string("SERVER_NAME"), &nxt_py_server_name_str }, + { nxt_string("SERVER_PORT"), &nxt_py_server_port_str }, + { nxt_string("SERVER_PROTOCOL"), &nxt_py_server_protocol_str }, + { nxt_string("wsgi.url_scheme"), &nxt_py_wsgi_uri_scheme_str }, +}; + + +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; + PyObject *obj, *pypath, *module; + nxt_unit_ctx_t *unit_ctx; + nxt_unit_init_t python_init; + 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"; + static const char bin_python[] = "/bin/python"; +#endif + + app_conf = data->app; + c = &app_conf->u.python; + + if (c->home != NULL) { + len = nxt_strlen(c->home); + +#if PY_MAJOR_VERSION == 3 + + path = nxt_malloc(len + sizeof(pyvenv)); + if (nxt_slow_path(path == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + nxt_memcpy(path, c->home, len); + nxt_memcpy(path + len, pyvenv, sizeof(pyvenv)); + + pep405 = (access(path, R_OK) == 0); + + nxt_free(path); + + if (pep405) { + size = (len + sizeof(bin_python)) * sizeof(wchar_t); + + } else { + size = (len + 1) * sizeof(wchar_t); + } + + nxt_py_home = nxt_malloc(size); + if (nxt_slow_path(nxt_py_home == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + if (pep405) { + mbstowcs(nxt_py_home, c->home, len); + mbstowcs(nxt_py_home + len, bin_python, sizeof(bin_python)); + Py_SetProgramName(nxt_py_home); + + } else { + mbstowcs(nxt_py_home, c->home, len + 1); + Py_SetPythonHome(nxt_py_home); + } + +#else + nxt_py_home = nxt_malloc(len + 1); + if (nxt_slow_path(nxt_py_home == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + nxt_memcpy(nxt_py_home, c->home, len + 1); + Py_SetPythonHome(nxt_py_home); +#endif + } + + Py_InitializeEx(0); + + module = NULL; + obj = NULL; + + if (nxt_slow_path(nxt_python_init_strings() != NXT_OK)) { + nxt_alert(task, "Python failed to init string objects"); + goto fail; + } + + obj = PySys_GetObject((char *) "stderr"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to get \"sys.stderr\" object"); + goto fail; + } + + nxt_py_stderr_flush = PyObject_GetAttrString(obj, "flush"); + if (nxt_slow_path(nxt_py_stderr_flush == NULL)) { + nxt_alert(task, "Python failed to get \"flush\" attribute of " + "\"sys.stderr\" object"); + goto fail; + } + + Py_DECREF(obj); + + if (c->path.length > 0) { + obj = PyString_FromStringAndSize((char *) c->path.start, + c->path.length); + + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to create string object \"%V\"", + &c->path); + goto fail; + } + + pypath = PySys_GetObject((char *) "path"); + + if (nxt_slow_path(pypath == NULL)) { + nxt_alert(task, "Python failed to get \"sys.path\" list"); + goto fail; + } + + if (nxt_slow_path(PyList_Insert(pypath, 0, obj) != 0)) { + nxt_alert(task, "Python failed to insert \"%V\" into \"sys.path\"", + &c->path); + goto fail; + } + + Py_DECREF(obj); + } + + obj = PyCFunction_New(nxt_py_start_resp_method, NULL); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, + "Python failed to initialize the \"start_response\" function"); + goto fail; + } + + nxt_py_start_resp_obj = obj; + + obj = PyCFunction_New(nxt_py_write_method, NULL); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to initialize the \"write\" function"); + goto fail; + } + + nxt_py_write_obj = obj; + + obj = nxt_python_create_environ(task); + if (nxt_slow_path(obj == NULL)) { + goto fail; + } + + nxt_py_environ_ptyp = obj; + + obj = Py_BuildValue("[s]", "unit"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to create the \"sys.argv\" list"); + goto fail; + } + + if (nxt_slow_path(PySys_SetObject((char *) "argv", obj) != 0)) { + nxt_alert(task, "Python failed to set the \"sys.argv\" list"); + goto fail; + } + + 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'; + + 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(); + goto fail; + } + + obj = PyDict_GetItemString(PyModule_GetDict(module), "application"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to get \"application\" " + "from module \"%s\"", nxt_py_module); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(obj) == 0)) { + nxt_alert(task, "\"application\" in module \"%s\" " + "is not a callable object", nxt_py_module); + goto fail; + } + + Py_INCREF(obj); + Py_CLEAR(module); + + nxt_py_application = obj; + obj = NULL; + + nxt_unit_default_init(task, &python_init); + + python_init.callbacks.request_handler = nxt_python_request_handler; + python_init.shm_limit = data->app->shm_limit; + + unit_ctx = nxt_unit_init(&python_init); + if (nxt_slow_path(unit_ctx == NULL)) { + goto fail; + } + + nxt_python_thread_state = PyEval_SaveThread(); + + rc = nxt_unit_run(unit_ctx); + + nxt_unit_done(unit_ctx); + + PyEval_RestoreThread(nxt_python_thread_state); + + nxt_python_atexit(); + + exit(rc); + + return NXT_OK; + +fail: + + Py_XDECREF(obj); + Py_XDECREF(module); + + nxt_python_atexit(); + + return NXT_ERROR; +} + + +static nxt_int_t +nxt_python_init_strings(void) +{ + PyObject *obj; + nxt_uint_t i; + nxt_python_string_t *pstr; + + for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { + pstr = &nxt_python_strings[i]; + + obj = PyString_FromStringAndSize((char *) pstr->string.start, + pstr->string.length); + if (nxt_slow_path(obj == NULL)) { + return NXT_ERROR; + } + + PyUnicode_InternInPlace(&obj); + + *pstr->object_p = obj; + } + + return NXT_OK; +} + + +static void +nxt_python_request_handler(nxt_unit_request_info_t *req) +{ + int rc; + PyObject *environ, *args, *response, *iterator, *item; + PyObject *close, *result; + nxt_python_run_ctx_t run_ctx = {-1, 0, NULL, req}; + + PyEval_RestoreThread(nxt_python_thread_state); + + environ = nxt_python_get_environ(&run_ctx); + if (nxt_slow_path(environ == NULL)) { + rc = NXT_UNIT_ERROR; + goto done; + } + + args = PyTuple_New(2); + if (nxt_slow_path(args == NULL)) { + Py_DECREF(environ); + + nxt_unit_req_error(req, "Python failed to create arguments tuple"); + + rc = NXT_UNIT_ERROR; + goto done; + } + + PyTuple_SET_ITEM(args, 0, environ); + + Py_INCREF(nxt_py_start_resp_obj); + PyTuple_SET_ITEM(args, 1, nxt_py_start_resp_obj); + + nxt_python_run_ctx = &run_ctx; + + response = PyObject_CallObject(nxt_py_application, args); + + Py_DECREF(args); + + if (nxt_slow_path(response == NULL)) { + nxt_unit_req_error(req, "Python failed to call the application"); + nxt_python_print_exception(); + + rc = NXT_UNIT_ERROR; + goto done; + } + + /* Shortcut: avoid iterate over response string symbols. */ + if (PyBytes_Check(response)) { + rc = nxt_python_write(&run_ctx, response); + + } else { + iterator = PyObject_GetIter(response); + + if (nxt_fast_path(iterator != NULL)) { + rc = NXT_UNIT_OK; + + while (run_ctx.bytes_sent < run_ctx.content_length) { + item = PyIter_Next(iterator); + + if (item == NULL) { + if (nxt_slow_path(PyErr_Occurred() != NULL)) { + nxt_unit_req_error(req, "Python failed to iterate over " + "the application response object"); + nxt_python_print_exception(); + + rc = NXT_UNIT_ERROR; + } + + break; + } + + if (nxt_fast_path(PyBytes_Check(item))) { + rc = nxt_python_write(&run_ctx, item); + + } else { + nxt_unit_req_error(req, "the application returned " + "not a bytestring object"); + rc = NXT_UNIT_ERROR; + } + + Py_DECREF(item); + + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + break; + } + } + + Py_DECREF(iterator); + + } else { + nxt_unit_req_error(req, + "the application returned not an iterable object"); + nxt_python_print_exception(); + + rc = NXT_UNIT_ERROR; + } + + close = PyObject_GetAttr(response, nxt_py_close_str); + + if (close != NULL) { + result = PyObject_CallFunction(close, NULL); + if (nxt_slow_path(result == NULL)) { + nxt_unit_req_error(req, "Python failed to call the close() " + "method of the application response"); + nxt_python_print_exception(); + + } else { + Py_DECREF(result); + } + + Py_DECREF(close); + + } else { + PyErr_Clear(); + } + } + + Py_DECREF(response); + +done: + + nxt_python_thread_state = PyEval_SaveThread(); + + nxt_python_run_ctx = NULL; + nxt_unit_request_done(req, rc); +} + + +static void +nxt_python_atexit(void) +{ + nxt_uint_t i; + + for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { + Py_XDECREF(*nxt_python_strings[i].object_p); + } + + Py_XDECREF(nxt_py_stderr_flush); + Py_XDECREF(nxt_py_application); + Py_XDECREF(nxt_py_start_resp_obj); + Py_XDECREF(nxt_py_write_obj); + Py_XDECREF(nxt_py_environ_ptyp); + + Py_Finalize(); + + if (nxt_py_home != NULL) { + nxt_free(nxt_py_home); + } +} + + +static PyObject * +nxt_python_create_environ(nxt_task_t *task) +{ + PyObject *obj, *err, *environ; + + environ = PyDict_New(); + + if (nxt_slow_path(environ == NULL)) { + nxt_alert(task, "Python failed to create the \"environ\" dictionary"); + return NULL; + } + + obj = PyString_FromStringAndSize((char *) nxt_server.start, + nxt_server.length); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, + "Python failed to create the \"SERVER_SOFTWARE\" environ value"); + goto fail; + } + + if (nxt_slow_path(PyDict_SetItemString(environ, "SERVER_SOFTWARE", obj) + != 0)) + { + nxt_alert(task, + "Python failed to set the \"SERVER_SOFTWARE\" environ value"); + goto fail; + } + + Py_DECREF(obj); + + obj = Py_BuildValue("(ii)", 1, 0); + + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, + "Python failed to build the \"wsgi.version\" environ value"); + goto fail; + } + + if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.version", obj) != 0)) + { + nxt_alert(task, + "Python failed to set the \"wsgi.version\" environ value"); + goto fail; + } + + Py_DECREF(obj); + obj = NULL; + + + if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.multithread", + Py_False) + != 0)) + { + nxt_alert(task, + "Python failed to set the \"wsgi.multithread\" environ value"); + goto fail; + } + + if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.multiprocess", + Py_True) + != 0)) + { + nxt_alert(task, + "Python failed to set the \"wsgi.multiprocess\" environ value"); + goto fail; + } + + if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.run_once", + Py_False) + != 0)) + { + nxt_alert(task, + "Python failed to set the \"wsgi.run_once\" environ value"); + goto fail; + } + + + if (nxt_slow_path(PyType_Ready(&nxt_py_input_type) != 0)) { + nxt_alert(task, + "Python failed to initialize the \"wsgi.input\" type object"); + goto fail; + } + + obj = (PyObject *) PyObject_New(nxt_py_input_t, &nxt_py_input_type); + + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to create the \"wsgi.input\" object"); + goto fail; + } + + if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.input", obj) != 0)) { + nxt_alert(task, + "Python failed to set the \"wsgi.input\" environ value"); + goto fail; + } + + Py_DECREF(obj); + obj = NULL; + + + err = PySys_GetObject((char *) "stderr"); + + if (nxt_slow_path(err == NULL)) { + nxt_alert(task, "Python failed to get \"sys.stderr\" object"); + goto fail; + } + + if (nxt_slow_path(PyDict_SetItemString(environ, "wsgi.errors", err) != 0)) + { + nxt_alert(task, + "Python failed to set the \"wsgi.errors\" environ value"); + goto fail; + } + + return environ; + +fail: + + Py_XDECREF(obj); + Py_DECREF(environ); + + return NULL; +} + + +static PyObject * +nxt_python_get_environ(nxt_python_run_ctx_t *ctx) +{ + int rc; + uint32_t i; + PyObject *environ; + nxt_unit_field_t *f; + nxt_unit_request_t *r; + + environ = PyDict_Copy(nxt_py_environ_ptyp); + if (nxt_slow_path(environ == NULL)) { + nxt_unit_req_error(ctx->req, + "Python failed to copy the \"environ\" dictionary"); + + return NULL; + } + + ctx->environ = environ; + + r = ctx->req->request; + +#define RC(S) \ + do { \ + rc = (S); \ + if (nxt_slow_path(rc != NXT_UNIT_OK)) { \ + goto fail; \ + } \ + } while(0) + + RC(nxt_python_add_sptr(ctx, nxt_py_request_method_str, &r->method, + r->method_length)); + RC(nxt_python_add_sptr(ctx, nxt_py_request_uri_str, &r->target, + r->target_length)); + RC(nxt_python_add_sptr(ctx, nxt_py_query_string_str, &r->query, + r->query_length)); + RC(nxt_python_add_sptr(ctx, nxt_py_path_info_str, &r->path, + r->path_length)); + + RC(nxt_python_add_sptr(ctx, nxt_py_remote_addr_str, &r->remote, + r->remote_length)); + RC(nxt_python_add_sptr(ctx, nxt_py_server_addr_str, &r->local, + r->local_length)); + + if (r->tls) { + RC(nxt_python_add_obj(ctx, nxt_py_wsgi_uri_scheme_str, + nxt_py_https_str)); + } else { + RC(nxt_python_add_obj(ctx, nxt_py_wsgi_uri_scheme_str, + nxt_py_http_str)); + } + + RC(nxt_python_add_sptr(ctx, nxt_py_server_protocol_str, &r->version, + r->version_length)); + + RC(nxt_python_add_sptr(ctx, nxt_py_server_name_str, &r->server_name, + r->server_name_length)); + RC(nxt_python_add_obj(ctx, nxt_py_server_port_str, nxt_py_80_str)); + + for (i = 0; i < r->fields_count; i++) { + f = r->fields + i; + + RC(nxt_python_add_field(ctx, f)); + } + + if (r->content_length_field != NXT_UNIT_NONE_FIELD) { + f = r->fields + r->content_length_field; + + RC(nxt_python_add_sptr(ctx, nxt_py_content_length_str, &f->value, + f->value_length)); + } + + if (r->content_type_field != NXT_UNIT_NONE_FIELD) { + f = r->fields + r->content_type_field; + + RC(nxt_python_add_sptr(ctx, nxt_py_content_type_str, &f->value, + f->value_length)); + } + +#undef RC + + return environ; + +fail: + + Py_DECREF(environ); + + return NULL; +} + + +static int +nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, + nxt_unit_sptr_t *sptr, uint32_t size) +{ + char *src; + PyObject *value; + + src = nxt_unit_sptr_get(sptr); + + value = PyString_FromStringAndSize(src, size); + if (nxt_slow_path(value == NULL)) { + nxt_unit_req_error(ctx->req, + "Python failed to create value string \"%.*s\"", + (int) size, src); + nxt_python_print_exception(); + + return NXT_UNIT_ERROR; + } + + if (nxt_slow_path(PyDict_SetItem(ctx->environ, name, value) != 0)) { + nxt_unit_req_error(ctx->req, + "Python failed to set the \"%s\" environ value", + PyUnicode_AsUTF8(name)); + Py_DECREF(value); + + return NXT_UNIT_ERROR; + } + + Py_DECREF(value); + + return NXT_UNIT_OK; +} + + +static int +nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field) +{ + char *src; + PyObject *name, *value; + + src = nxt_unit_sptr_get(&field->name); + + name = PyString_FromStringAndSize(src, field->name_length); + if (nxt_slow_path(name == NULL)) { + nxt_unit_req_error(ctx->req, + "Python failed to create name string \"%.*s\"", + (int) field->name_length, src); + nxt_python_print_exception(); + + return NXT_UNIT_ERROR; + } + + src = nxt_unit_sptr_get(&field->value); + + value = PyString_FromStringAndSize(src, field->value_length); + if (nxt_slow_path(value == NULL)) { + nxt_unit_req_error(ctx->req, + "Python failed to create value string \"%.*s\"", + (int) field->value_length, src); + nxt_python_print_exception(); + + goto fail; + } + + if (nxt_slow_path(PyDict_SetItem(ctx->environ, name, value) != 0)) { + nxt_unit_req_error(ctx->req, + "Python failed to set the \"%s\" environ value", + PyUnicode_AsUTF8(name)); + goto fail; + } + + Py_DECREF(name); + Py_DECREF(value); + + return NXT_UNIT_OK; + +fail: + + Py_DECREF(name); + Py_XDECREF(value); + + return NXT_UNIT_ERROR; +} + + +static int +nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, PyObject *value) +{ + if (nxt_slow_path(PyDict_SetItem(ctx->environ, name, value) != 0)) { + nxt_unit_req_error(ctx->req, + "Python failed to set the \"%s\" environ value", + PyUnicode_AsUTF8(name)); + + return NXT_UNIT_ERROR; + } + + return NXT_UNIT_OK; +} + + +static PyObject * +nxt_py_start_resp(PyObject *self, PyObject *args) +{ + int rc, status; + char *status_str, *space_ptr; + uint32_t status_len; + PyObject *headers, *tuple, *string, *status_bytes; + Py_ssize_t i, n, fields_size, fields_count; + nxt_python_run_ctx_t *ctx; + + ctx = nxt_python_run_ctx; + if (nxt_slow_path(ctx == NULL)) { + return PyErr_Format(PyExc_RuntimeError, + "start_response() is called " + "outside of WSGI request processing"); + } + + n = PyTuple_GET_SIZE(args); + + if (n < 2 || n > 3) { + return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); + } + + string = PyTuple_GET_ITEM(args, 0); + if (!PyBytes_Check(string) && !PyUnicode_Check(string)) { + return PyErr_Format(PyExc_TypeError, + "failed to write first argument (not a string?)"); + } + + headers = PyTuple_GET_ITEM(args, 1); + if (!PyList_Check(headers)) { + return PyErr_Format(PyExc_TypeError, + "the second argument is not a response headers list"); + } + + fields_size = 0; + fields_count = PyList_GET_SIZE(headers); + + for (i = 0; i < fields_count; i++) { + tuple = PyList_GET_ITEM(headers, i); + + if (!PyTuple_Check(tuple)) { + return PyErr_Format(PyExc_TypeError, + "the response headers must be a list of tuples"); + } + + if (PyTuple_GET_SIZE(tuple) != 2) { + return PyErr_Format(PyExc_TypeError, + "each header must be a tuple of two items"); + } + + string = PyTuple_GET_ITEM(tuple, 0); + if (PyBytes_Check(string)) { + fields_size += PyBytes_GET_SIZE(string); + + } else if (PyUnicode_Check(string)) { + fields_size += PyUnicode_GET_SIZE(string); + + } else { + return PyErr_Format(PyExc_TypeError, + "header #%d name is not a string", (int) i); + } + + string = PyTuple_GET_ITEM(tuple, 1); + if (PyBytes_Check(string)) { + fields_size += PyBytes_GET_SIZE(string); + + } else if (PyUnicode_Check(string)) { + fields_size += PyUnicode_GET_SIZE(string); + + } else { + return PyErr_Format(PyExc_TypeError, + "header #%d value is not a string", (int) i); + } + } + + ctx->content_length = -1; + + string = PyTuple_GET_ITEM(args, 0); + rc = nxt_python_str_buf(string, &status_str, &status_len, &status_bytes); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_TypeError, "status is not a string"); + } + + space_ptr = memchr(status_str, ' ', status_len); + if (space_ptr != NULL) { + status_len = space_ptr - status_str; + } + + status = nxt_int_parse((u_char *) status_str, status_len); + if (nxt_slow_path(status < 0)) { + return PyErr_Format(PyExc_TypeError, "failed to parse status code"); + } + + Py_XDECREF(status_bytes); + + /* + * PEP 3333: + * + * ... applications can replace their originally intended output with error + * output, up until the last possible moment. + */ + rc = nxt_unit_response_init(ctx->req, status, fields_count, fields_size); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to allocate response object"); + } + + for (i = 0; i < fields_count; i++) { + tuple = PyList_GET_ITEM(headers, i); + + rc = nxt_python_response_add_field(ctx, PyTuple_GET_ITEM(tuple, 0), + PyTuple_GET_ITEM(tuple, 1), i); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to add header #%d", (int) i); + } + } + + /* + * PEP 3333: + * + * However, the start_response callable must not actually transmit the + * response headers. Instead, it must store them for the server or gateway + * to transmit only after the first iteration of the application return + * value that yields a non-empty bytestring, or upon the application's + * first invocation of the write() callable. In other words, response + * headers must not be sent until there is actual body data available, or + * until the application's returned iterable is exhausted. (The only + * possible exception to this rule is if the response headers explicitly + * include a Content-Length of zero.) + */ + if (ctx->content_length == 0) { + rc = nxt_unit_response_send(ctx->req); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to send response headers"); + } + } + + Py_INCREF(nxt_py_write_obj); + return nxt_py_write_obj; +} + + +static int +nxt_python_response_add_field(nxt_python_run_ctx_t *ctx, PyObject *name, + PyObject *value, int i) +{ + int rc; + char *name_str, *value_str; + uint32_t name_length, value_length; + PyObject *name_bytes, *value_bytes; + nxt_off_t content_length; + + name_bytes = NULL; + value_bytes = NULL; + + rc = nxt_python_str_buf(name, &name_str, &name_length, &name_bytes); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + goto fail; + } + + rc = nxt_python_str_buf(value, &value_str, &value_length, &value_bytes); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + goto fail; + } + + rc = nxt_unit_response_add_field(ctx->req, name_str, name_length, + value_str, value_length); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + goto fail; + } + + if (ctx->req->response->fields[i].hash == NXT_UNIT_HASH_CONTENT_LENGTH) { + content_length = nxt_off_t_parse((u_char *) value_str, value_length); + if (nxt_slow_path(content_length < 0)) { + nxt_unit_req_error(ctx->req, "failed to parse Content-Length " + "value %.*s", (int) value_length, value_str); + + } else { + ctx->content_length = content_length; + } + } + +fail: + + Py_XDECREF(name_bytes); + Py_XDECREF(value_bytes); + + return rc; +} + + +static int +nxt_python_str_buf(PyObject *str, char **buf, uint32_t *len, PyObject **bytes) +{ + if (PyBytes_Check(str)) { + *buf = PyBytes_AS_STRING(str); + *len = PyBytes_GET_SIZE(str); + *bytes = NULL; + + } else { + *bytes = PyUnicode_AsLatin1String(str); + if (nxt_slow_path(*bytes == NULL)) { + return NXT_UNIT_ERROR; + } + + *buf = PyBytes_AS_STRING(*bytes); + *len = PyBytes_GET_SIZE(*bytes); + } + + return NXT_UNIT_OK; +} + + +static PyObject * +nxt_py_write(PyObject *self, PyObject *str) +{ + int rc; + + if (nxt_fast_path(!PyBytes_Check(str))) { + return PyErr_Format(PyExc_TypeError, "the argument is not a %s", + NXT_PYTHON_BYTES_TYPE); + } + + rc = nxt_python_write(nxt_python_run_ctx, str); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to write response value"); + } + + Py_RETURN_NONE; +} + + +static void +nxt_py_input_dealloc(nxt_py_input_t *self) +{ + PyObject_Del(self); +} + + +static PyObject * +nxt_py_input_read(nxt_py_input_t *self, PyObject *args) +{ + char *buf; + PyObject *content, *obj; + Py_ssize_t size, n; + nxt_python_run_ctx_t *ctx; + + ctx = nxt_python_run_ctx; + if (nxt_slow_path(ctx == NULL)) { + return PyErr_Format(PyExc_RuntimeError, + "wsgi.input.read() is called " + "outside of WSGI request processing"); + } + + size = ctx->req->content_length; + + n = PyTuple_GET_SIZE(args); + + if (n > 0) { + if (n != 1) { + return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); + } + + obj = PyTuple_GET_ITEM(args, 0); + + size = PyNumber_AsSsize_t(obj, PyExc_OverflowError); + + if (nxt_slow_path(size < 0)) { + if (size == -1 && PyErr_Occurred()) { + return NULL; + } + + if (size != -1) { + return PyErr_Format(PyExc_ValueError, + "the read body size cannot be zero or less"); + } + } + + if (size == -1 || size > (Py_ssize_t) ctx->req->content_length) { + size = ctx->req->content_length; + } + } + + content = PyBytes_FromStringAndSize(NULL, size); + if (nxt_slow_path(content == NULL)) { + return NULL; + } + + buf = PyBytes_AS_STRING(content); + + size = nxt_unit_request_read(ctx->req, buf, size); + + return content; +} + + +static PyObject * +nxt_py_input_readline(nxt_py_input_t *self, PyObject *args) +{ + ssize_t ssize; + PyObject *obj; + Py_ssize_t n; + nxt_python_run_ctx_t *ctx; + + ctx = nxt_python_run_ctx; + if (nxt_slow_path(ctx == NULL)) { + return PyErr_Format(PyExc_RuntimeError, + "wsgi.input.readline() is called " + "outside of WSGI request processing"); + } + + n = PyTuple_GET_SIZE(args); + + if (n > 0) { + if (n != 1) { + return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); + } + + obj = PyTuple_GET_ITEM(args, 0); + + ssize = PyNumber_AsSsize_t(obj, PyExc_OverflowError); + + if (nxt_fast_path(ssize > 0)) { + return nxt_py_input_getline(ctx, ssize); + } + + if (ssize == 0) { + return PyBytes_FromStringAndSize("", 0); + } + + if (ssize != -1) { + return PyErr_Format(PyExc_ValueError, + "the read line size cannot be zero or less"); + } + + if (PyErr_Occurred()) { + return NULL; + } + } + + return nxt_py_input_getline(ctx, SSIZE_MAX); +} + + +static PyObject * +nxt_py_input_getline(nxt_python_run_ctx_t *ctx, size_t size) +{ + void *buf; + ssize_t res; + PyObject *content; + + res = nxt_unit_request_readline_size(ctx->req, size); + if (nxt_slow_path(res < 0)) { + return NULL; + } + + if (res == 0) { + return PyBytes_FromStringAndSize("", 0); + } + + content = PyBytes_FromStringAndSize(NULL, res); + if (nxt_slow_path(content == NULL)) { + return NULL; + } + + buf = PyBytes_AS_STRING(content); + + res = nxt_unit_request_read(ctx->req, buf, res); + + return content; +} + + +static PyObject * +nxt_py_input_readlines(nxt_py_input_t *self, PyObject *args) +{ + PyObject *res; + nxt_python_run_ctx_t *ctx; + + ctx = nxt_python_run_ctx; + if (nxt_slow_path(ctx == NULL)) { + return PyErr_Format(PyExc_RuntimeError, + "wsgi.input.readlines() is called " + "outside of WSGI request processing"); + } + + res = PyList_New(0); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + for ( ;; ) { + PyObject *line = nxt_py_input_getline(ctx, SSIZE_MAX); + if (nxt_slow_path(line == NULL)) { + Py_DECREF(res); + return NULL; + } + + if (PyBytes_GET_SIZE(line) == 0) { + Py_DECREF(line); + return res; + } + + PyList_Append(res, line); + Py_DECREF(line); + } + + return res; +} + + +static PyObject * +nxt_py_input_iter(PyObject *self) +{ + Py_INCREF(self); + return self; +} + + +static PyObject * +nxt_py_input_next(PyObject *self) +{ + PyObject *line; + nxt_python_run_ctx_t *ctx; + + ctx = nxt_python_run_ctx; + if (nxt_slow_path(ctx == NULL)) { + return PyErr_Format(PyExc_RuntimeError, + "wsgi.input.next() is called " + "outside of WSGI request processing"); + } + + line = nxt_py_input_getline(ctx, SSIZE_MAX); + if (nxt_slow_path(line == NULL)) { + return NULL; + } + + if (PyBytes_GET_SIZE(line) == 0) { + Py_DECREF(line); + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + return line; +} + + +static void +nxt_python_print_exception(void) +{ + PyErr_Print(); + +#if PY_MAJOR_VERSION == 3 + /* The backtrace may be buffered in sys.stderr file object. */ + { + PyObject *result; + + result = PyObject_CallFunction(nxt_py_stderr_flush, NULL); + if (nxt_slow_path(result == NULL)) { + PyErr_Clear(); + return; + } + + Py_DECREF(result); + } +#endif +} + + +static int +nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes) +{ + int rc; + char *str_buf; + uint32_t str_length; + + str_buf = PyBytes_AS_STRING(bytes); + str_length = PyBytes_GET_SIZE(bytes); + + if (nxt_slow_path(str_length == 0)) { + return NXT_UNIT_OK; + } + + /* + * PEP 3333: + * + * If the application supplies a Content-Length header, the server should + * not transmit more bytes to the client than the header allows, and should + * stop iterating over the response when enough data has been sent, or raise + * an error if the application tries to write() past that point. + */ + if (nxt_slow_path(str_length > ctx->content_length - ctx->bytes_sent)) { + nxt_unit_req_error(ctx->req, "content length %"PRIu64" exceeded", + ctx->content_length); + + return NXT_UNIT_ERROR; + } + + rc = nxt_unit_response_write(ctx->req, str_buf, str_length); + if (nxt_fast_path(rc == NXT_UNIT_OK)) { + ctx->bytes_sent += str_length; + } + + return rc; +} -- cgit From d94dac091f6a6878f10cfc8fa1ef059dd6bfe964 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 14 Sep 2020 13:27:02 +0300 Subject: Python: split module initialization from WSGI implementation. This is required for futher ASGI implementation. --- auto/modules/python | 1 + src/python/nxt_python.c | 331 +++++++++++++++++++++++++++++++++++++++++++ src/python/nxt_python.h | 31 ++++ src/python/nxt_python_wsgi.c | 310 ++++------------------------------------ 4 files changed, 388 insertions(+), 285 deletions(-) create mode 100644 src/python/nxt_python.c create mode 100644 src/python/nxt_python.h diff --git a/auto/modules/python b/auto/modules/python index 0e80d93f..afb1b586 100644 --- a/auto/modules/python +++ b/auto/modules/python @@ -167,6 +167,7 @@ $echo " + Python module: ${NXT_PYTHON_MODULE}.unit.so" $echo >> $NXT_MAKEFILE NXT_PYTHON_MODULE_SRCS=" \ + src/python/nxt_python.c \ src/python/nxt_python_wsgi.c \ " diff --git a/src/python/nxt_python.c b/src/python/nxt_python.c new file mode 100644 index 00000000..5b6021bb --- /dev/null +++ b/src/python/nxt_python.c @@ -0,0 +1,331 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include + +#include +#include +#include + +#include + +#include NXT_PYTHON_MOUNTS_H + + +#if PY_MAJOR_VERSION == 3 +#define PyString_FromStringAndSize(str, size) \ + PyUnicode_DecodeLatin1((str), (size), "strict") + +#else +#define PyUnicode_InternInPlace PyString_InternInPlace +#endif + +static nxt_int_t nxt_python_start(nxt_task_t *task, + nxt_process_data_t *data); +static void nxt_python_atexit(void); + +static uint32_t compat[] = { + NXT_VERNUM, NXT_DEBUG, +}; + + +NXT_EXPORT nxt_app_module_t nxt_app_module = { + sizeof(compat), + compat, + nxt_string("python"), + PY_VERSION, + nxt_python_mounts, + nxt_nitems(nxt_python_mounts), + NULL, + nxt_python_start, +}; + +static PyObject *nxt_py_stderr_flush; +PyObject *nxt_py_application; + +#if PY_MAJOR_VERSION == 3 +static wchar_t *nxt_py_home; +#else +static char *nxt_py_home; +#endif + + +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; + PyObject *obj, *pypath, *module; + nxt_unit_ctx_t *unit_ctx; + nxt_unit_init_t python_init; + 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"; + static const char bin_python[] = "/bin/python"; +#endif + + app_conf = data->app; + c = &app_conf->u.python; + + if (c->home != NULL) { + len = nxt_strlen(c->home); + +#if PY_MAJOR_VERSION == 3 + + path = nxt_malloc(len + sizeof(pyvenv)); + if (nxt_slow_path(path == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + nxt_memcpy(path, c->home, len); + nxt_memcpy(path + len, pyvenv, sizeof(pyvenv)); + + pep405 = (access(path, R_OK) == 0); + + nxt_free(path); + + if (pep405) { + size = (len + sizeof(bin_python)) * sizeof(wchar_t); + + } else { + size = (len + 1) * sizeof(wchar_t); + } + + nxt_py_home = nxt_malloc(size); + if (nxt_slow_path(nxt_py_home == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + if (pep405) { + mbstowcs(nxt_py_home, c->home, len); + mbstowcs(nxt_py_home + len, bin_python, sizeof(bin_python)); + Py_SetProgramName(nxt_py_home); + + } else { + mbstowcs(nxt_py_home, c->home, len + 1); + Py_SetPythonHome(nxt_py_home); + } + +#else + nxt_py_home = nxt_malloc(len + 1); + if (nxt_slow_path(nxt_py_home == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + nxt_memcpy(nxt_py_home, c->home, len + 1); + Py_SetPythonHome(nxt_py_home); +#endif + } + + Py_InitializeEx(0); + + module = NULL; + obj = NULL; + + obj = PySys_GetObject((char *) "stderr"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to get \"sys.stderr\" object"); + goto fail; + } + + nxt_py_stderr_flush = PyObject_GetAttrString(obj, "flush"); + if (nxt_slow_path(nxt_py_stderr_flush == NULL)) { + nxt_alert(task, "Python failed to get \"flush\" attribute of " + "\"sys.stderr\" object"); + goto fail; + } + + /* obj is a Borrowed reference. */ + + if (c->path.length > 0) { + obj = PyString_FromStringAndSize((char *) c->path.start, + c->path.length); + + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to create string object \"%V\"", + &c->path); + goto fail; + } + + pypath = PySys_GetObject((char *) "path"); + + if (nxt_slow_path(pypath == NULL)) { + nxt_alert(task, "Python failed to get \"sys.path\" list"); + goto fail; + } + + if (nxt_slow_path(PyList_Insert(pypath, 0, obj) != 0)) { + nxt_alert(task, "Python failed to insert \"%V\" into \"sys.path\"", + &c->path); + goto fail; + } + + Py_DECREF(obj); + } + + obj = Py_BuildValue("[s]", "unit"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to create the \"sys.argv\" list"); + goto fail; + } + + if (nxt_slow_path(PySys_SetObject((char *) "argv", obj) != 0)) { + nxt_alert(task, "Python failed to set the \"sys.argv\" list"); + goto fail; + } + + 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'; + + 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(); + goto fail; + } + + obj = PyDict_GetItemString(PyModule_GetDict(module), "application"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to get \"application\" " + "from module \"%s\"", nxt_py_module); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(obj) == 0)) { + nxt_alert(task, "\"application\" in module \"%s\" " + "is not a callable object", nxt_py_module); + goto fail; + } + + nxt_py_application = obj; + obj = NULL; + + Py_INCREF(nxt_py_application); + + Py_CLEAR(module); + + nxt_unit_default_init(task, &python_init); + + python_init.shm_limit = data->app->shm_limit; + + rc = nxt_python_wsgi_init(task, &python_init); + if (nxt_slow_path(rc == NXT_ERROR)) { + goto fail; + } + + unit_ctx = nxt_unit_init(&python_init); + if (nxt_slow_path(unit_ctx == NULL)) { + goto fail; + } + + rc = nxt_python_wsgi_run(unit_ctx); + + nxt_unit_done(unit_ctx); + + nxt_python_atexit(); + + exit(rc); + + return NXT_OK; + +fail: + + Py_XDECREF(obj); + Py_XDECREF(module); + + nxt_python_atexit(); + + return NXT_ERROR; +} + + +nxt_int_t +nxt_python_init_strings(nxt_python_string_t *pstr) +{ + PyObject *obj; + + while (pstr->string.start != NULL) { + obj = PyString_FromStringAndSize((char *) pstr->string.start, + pstr->string.length); + if (nxt_slow_path(obj == NULL)) { + return NXT_ERROR; + } + + PyUnicode_InternInPlace(&obj); + + *pstr->object_p = obj; + + pstr++; + } + + return NXT_OK; +} + + +void +nxt_python_done_strings(nxt_python_string_t *pstr) +{ + PyObject *obj; + + while (pstr->string.start != NULL) { + obj = *pstr->object_p; + + Py_XDECREF(obj); + *pstr->object_p = NULL; + + pstr++; + } +} + + +static void +nxt_python_atexit(void) +{ + nxt_python_wsgi_done(); + + Py_XDECREF(nxt_py_stderr_flush); + Py_XDECREF(nxt_py_application); + + Py_Finalize(); + + if (nxt_py_home != NULL) { + nxt_free(nxt_py_home); + } +} + + +void +nxt_python_print_exception(void) +{ + PyErr_Print(); + +#if PY_MAJOR_VERSION == 3 + /* The backtrace may be buffered in sys.stderr file object. */ + { + PyObject *result; + + result = PyObject_CallFunction(nxt_py_stderr_flush, NULL); + if (nxt_slow_path(result == NULL)) { + PyErr_Clear(); + return; + } + + Py_DECREF(result); + } +#endif +} diff --git a/src/python/nxt_python.h b/src/python/nxt_python.h new file mode 100644 index 00000000..417df7fd --- /dev/null +++ b/src/python/nxt_python.h @@ -0,0 +1,31 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_PYTHON_H_INCLUDED_ +#define _NXT_PYTHON_H_INCLUDED_ + + +#include +#include + + +extern PyObject *nxt_py_application; + +typedef struct { + nxt_str_t string; + PyObject **object_p; +} nxt_python_string_t; + +nxt_int_t nxt_python_init_strings(nxt_python_string_t *pstr); +void nxt_python_done_strings(nxt_python_string_t *pstr); + +void nxt_python_print_exception(void); + +nxt_int_t nxt_python_wsgi_init(nxt_task_t *task, nxt_unit_init_t *init); +int nxt_python_wsgi_run(nxt_unit_ctx_t *ctx); +void nxt_python_wsgi_done(void); + + +#endif /* _NXT_PYTHON_H_INCLUDED_ */ diff --git a/src/python/nxt_python_wsgi.c b/src/python/nxt_python_wsgi.c index c4b7702e..3371dae6 100644 --- a/src/python/nxt_python_wsgi.c +++ b/src/python/nxt_python_wsgi.c @@ -8,17 +8,15 @@ #include -#include -#include - #include -#include #include #include #include #include #include +#include + #include NXT_PYTHON_MOUNTS_H /* @@ -68,11 +66,7 @@ typedef struct { PyObject_HEAD } nxt_py_error_t; -static nxt_int_t nxt_python_start(nxt_task_t *task, - nxt_process_data_t *data); -static nxt_int_t nxt_python_init_strings(void); static void nxt_python_request_handler(nxt_unit_request_info_t *req); -static void nxt_python_atexit(void); static PyObject *nxt_python_create_environ(nxt_task_t *task); static PyObject *nxt_python_get_environ(nxt_python_run_ctx_t *ctx); @@ -99,7 +93,6 @@ static PyObject *nxt_py_input_readlines(nxt_py_input_t *self, PyObject *args); static PyObject *nxt_py_input_iter(PyObject *self); static PyObject *nxt_py_input_next(PyObject *self); -static void nxt_python_print_exception(void); static int nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes); struct nxt_python_run_ctx_s { @@ -109,22 +102,6 @@ struct nxt_python_run_ctx_s { nxt_unit_request_info_t *req; }; -static uint32_t compat[] = { - NXT_VERNUM, NXT_DEBUG, -}; - - -NXT_EXPORT nxt_app_module_t nxt_app_module = { - sizeof(compat), - compat, - nxt_string("python"), - PY_VERSION, - nxt_python_mounts, - nxt_nitems(nxt_python_mounts), - NULL, - nxt_python_start, -}; - static PyMethodDef nxt_py_start_resp_method[] = { {"unit_start_response", nxt_py_start_resp, METH_VARARGS, ""} @@ -158,22 +135,13 @@ static PyTypeObject nxt_py_input_type = { }; -static PyObject *nxt_py_stderr_flush; -static PyObject *nxt_py_application; static PyObject *nxt_py_start_resp_obj; static PyObject *nxt_py_write_obj; static PyObject *nxt_py_environ_ptyp; -#if PY_MAJOR_VERSION == 3 -static wchar_t *nxt_py_home; -#else -static char *nxt_py_home; -#endif - static PyThreadState *nxt_python_thread_state; static nxt_python_run_ctx_t *nxt_python_run_ctx; - static PyObject *nxt_py_80_str; static PyObject *nxt_py_close_str; static PyObject *nxt_py_content_length_str; @@ -191,11 +159,6 @@ static PyObject *nxt_py_server_port_str; static PyObject *nxt_py_server_protocol_str; static PyObject *nxt_py_wsgi_uri_scheme_str; -typedef struct { - nxt_str_t string; - PyObject **object_p; -} nxt_python_string_t; - static nxt_python_string_t nxt_python_strings[] = { { nxt_string("80"), &nxt_py_80_str }, { nxt_string("close"), &nxt_py_close_str }, @@ -213,136 +176,22 @@ static nxt_python_string_t nxt_python_strings[] = { { nxt_string("SERVER_PORT"), &nxt_py_server_port_str }, { nxt_string("SERVER_PROTOCOL"), &nxt_py_server_protocol_str }, { nxt_string("wsgi.url_scheme"), &nxt_py_wsgi_uri_scheme_str }, + { nxt_null_string, NULL }, }; -static nxt_int_t -nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) +nxt_int_t +nxt_python_wsgi_init(nxt_task_t *task, nxt_unit_init_t *init) { - int rc; - char *nxt_py_module; - size_t len; - PyObject *obj, *pypath, *module; - nxt_unit_ctx_t *unit_ctx; - nxt_unit_init_t python_init; - 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"; - static const char bin_python[] = "/bin/python"; -#endif - - app_conf = data->app; - c = &app_conf->u.python; - - if (c->home != NULL) { - len = nxt_strlen(c->home); - -#if PY_MAJOR_VERSION == 3 - - path = nxt_malloc(len + sizeof(pyvenv)); - if (nxt_slow_path(path == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - nxt_memcpy(path, c->home, len); - nxt_memcpy(path + len, pyvenv, sizeof(pyvenv)); - - pep405 = (access(path, R_OK) == 0); - - nxt_free(path); - - if (pep405) { - size = (len + sizeof(bin_python)) * sizeof(wchar_t); - - } else { - size = (len + 1) * sizeof(wchar_t); - } - - nxt_py_home = nxt_malloc(size); - if (nxt_slow_path(nxt_py_home == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - if (pep405) { - mbstowcs(nxt_py_home, c->home, len); - mbstowcs(nxt_py_home + len, bin_python, sizeof(bin_python)); - Py_SetProgramName(nxt_py_home); - - } else { - mbstowcs(nxt_py_home, c->home, len + 1); - Py_SetPythonHome(nxt_py_home); - } + PyObject *obj; -#else - nxt_py_home = nxt_malloc(len + 1); - if (nxt_slow_path(nxt_py_home == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - nxt_memcpy(nxt_py_home, c->home, len + 1); - Py_SetPythonHome(nxt_py_home); -#endif - } - - Py_InitializeEx(0); - - module = NULL; obj = NULL; - if (nxt_slow_path(nxt_python_init_strings() != NXT_OK)) { + if (nxt_slow_path(nxt_python_init_strings(nxt_python_strings) != NXT_OK)) { nxt_alert(task, "Python failed to init string objects"); goto fail; } - obj = PySys_GetObject((char *) "stderr"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"sys.stderr\" object"); - goto fail; - } - - nxt_py_stderr_flush = PyObject_GetAttrString(obj, "flush"); - if (nxt_slow_path(nxt_py_stderr_flush == NULL)) { - nxt_alert(task, "Python failed to get \"flush\" attribute of " - "\"sys.stderr\" object"); - goto fail; - } - - Py_DECREF(obj); - - if (c->path.length > 0) { - obj = PyString_FromStringAndSize((char *) c->path.start, - c->path.length); - - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to create string object \"%V\"", - &c->path); - goto fail; - } - - pypath = PySys_GetObject((char *) "path"); - - if (nxt_slow_path(pypath == NULL)) { - nxt_alert(task, "Python failed to get \"sys.path\" list"); - goto fail; - } - - if (nxt_slow_path(PyList_Insert(pypath, 0, obj) != 0)) { - nxt_alert(task, "Python failed to insert \"%V\" into \"sys.path\"", - &c->path); - goto fail; - } - - Py_DECREF(obj); - } - obj = PyCFunction_New(nxt_py_start_resp_method, NULL); if (nxt_slow_path(obj == NULL)) { nxt_alert(task, @@ -366,107 +215,43 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) } nxt_py_environ_ptyp = obj; - - obj = Py_BuildValue("[s]", "unit"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to create the \"sys.argv\" list"); - goto fail; - } - - if (nxt_slow_path(PySys_SetObject((char *) "argv", obj) != 0)) { - nxt_alert(task, "Python failed to set the \"sys.argv\" list"); - goto fail; - } - - 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'; - - 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(); - goto fail; - } - - obj = PyDict_GetItemString(PyModule_GetDict(module), "application"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"application\" " - "from module \"%s\"", nxt_py_module); - goto fail; - } - - if (nxt_slow_path(PyCallable_Check(obj) == 0)) { - nxt_alert(task, "\"application\" in module \"%s\" " - "is not a callable object", nxt_py_module); - goto fail; - } - - Py_INCREF(obj); - Py_CLEAR(module); - - nxt_py_application = obj; obj = NULL; - nxt_unit_default_init(task, &python_init); - - python_init.callbacks.request_handler = nxt_python_request_handler; - python_init.shm_limit = data->app->shm_limit; - - unit_ctx = nxt_unit_init(&python_init); - if (nxt_slow_path(unit_ctx == NULL)) { - goto fail; - } - - nxt_python_thread_state = PyEval_SaveThread(); - - rc = nxt_unit_run(unit_ctx); - - nxt_unit_done(unit_ctx); - - PyEval_RestoreThread(nxt_python_thread_state); - - nxt_python_atexit(); - - exit(rc); + init->callbacks.request_handler = nxt_python_request_handler; return NXT_OK; fail: Py_XDECREF(obj); - Py_XDECREF(module); - - nxt_python_atexit(); return NXT_ERROR; } -static nxt_int_t -nxt_python_init_strings(void) +int +nxt_python_wsgi_run(nxt_unit_ctx_t *ctx) { - PyObject *obj; - nxt_uint_t i; - nxt_python_string_t *pstr; + int rc; - for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { - pstr = &nxt_python_strings[i]; + nxt_python_thread_state = PyEval_SaveThread(); - obj = PyString_FromStringAndSize((char *) pstr->string.start, - pstr->string.length); - if (nxt_slow_path(obj == NULL)) { - return NXT_ERROR; - } + rc = nxt_unit_run(ctx); - PyUnicode_InternInPlace(&obj); + PyEval_RestoreThread(nxt_python_thread_state); - *pstr->object_p = obj; - } + return rc; +} - return NXT_OK; + +void +nxt_python_wsgi_done(void) +{ + nxt_python_done_strings(nxt_python_strings); + + Py_XDECREF(nxt_py_start_resp_obj); + Py_XDECREF(nxt_py_write_obj); + Py_XDECREF(nxt_py_environ_ptyp); } @@ -597,29 +382,6 @@ done: } -static void -nxt_python_atexit(void) -{ - nxt_uint_t i; - - for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { - Py_XDECREF(*nxt_python_strings[i].object_p); - } - - Py_XDECREF(nxt_py_stderr_flush); - Py_XDECREF(nxt_py_application); - Py_XDECREF(nxt_py_start_resp_obj); - Py_XDECREF(nxt_py_write_obj); - Py_XDECREF(nxt_py_environ_ptyp); - - Py_Finalize(); - - if (nxt_py_home != NULL) { - nxt_free(nxt_py_home); - } -} - - static PyObject * nxt_python_create_environ(nxt_task_t *task) { @@ -1386,28 +1148,6 @@ nxt_py_input_next(PyObject *self) } -static void -nxt_python_print_exception(void) -{ - PyErr_Print(); - -#if PY_MAJOR_VERSION == 3 - /* The backtrace may be buffered in sys.stderr file object. */ - { - PyObject *result; - - result = PyObject_CallFunction(nxt_py_stderr_flush, NULL); - if (nxt_slow_path(result == NULL)) { - PyErr_Clear(); - return; - } - - Py_DECREF(result); - } -#endif -} - - static int nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes) { -- cgit From 5163551ffe30c66b20b8c44c5ca15f13f1fbae2c Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 15 Sep 2020 20:11:48 +0300 Subject: Hardening header names comparation for grouping. --- src/nxt_unit.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/src/nxt_unit.c b/src/nxt_unit.c index 539c381c..b7c01f95 100644 --- a/src/nxt_unit.c +++ b/src/nxt_unit.c @@ -184,6 +184,7 @@ static nxt_unit_request_info_t *nxt_unit_request_hash_find( nxt_unit_ctx_t *ctx, uint32_t stream, int remove); static char * nxt_unit_snprint_prefix(char *p, char *end, pid_t pid, int level); +static int nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length); struct nxt_unit_mmap_buf_s { @@ -1815,42 +1816,66 @@ nxt_unit_field_hash(const char *name, size_t name_length) void nxt_unit_request_group_dup_fields(nxt_unit_request_info_t *req) { + char *name; uint32_t i, j; nxt_unit_field_t *fields, f; nxt_unit_request_t *r; + static nxt_str_t content_length = nxt_string("content-length"); + static nxt_str_t content_type = nxt_string("content-type"); + static nxt_str_t cookie = nxt_string("cookie"); + nxt_unit_req_debug(req, "group_dup_fields"); r = req->request; fields = r->fields; for (i = 0; i < r->fields_count; i++) { + name = nxt_unit_sptr_get(&fields[i].name); switch (fields[i].hash) { case NXT_UNIT_HASH_CONTENT_LENGTH: - r->content_length_field = i; + if (fields[i].name_length == content_length.length + && nxt_unit_memcasecmp(name, content_length.start, + content_length.length) == 0) + { + r->content_length_field = i; + } + break; case NXT_UNIT_HASH_CONTENT_TYPE: - r->content_type_field = i; + if (fields[i].name_length == content_type.length + && nxt_unit_memcasecmp(name, content_type.start, + content_type.length) == 0) + { + r->content_type_field = i; + } + break; case NXT_UNIT_HASH_COOKIE: - r->cookie_field = i; + if (fields[i].name_length == cookie.length + && nxt_unit_memcasecmp(name, cookie.start, + cookie.length) == 0) + { + r->cookie_field = i; + } + break; - }; + } for (j = i + 1; j < r->fields_count; j++) { - if (fields[i].hash != fields[j].hash) { - continue; - } - - if (j == i + 1) { + if (fields[i].hash != fields[j].hash + || fields[i].name_length != fields[j].name_length + || nxt_unit_memcasecmp(name, + nxt_unit_sptr_get(&fields[j].name), + fields[j].name_length) != 0) + { continue; } f = fields[j]; - f.name.offset += (j - (i + 1)) * sizeof(f); f.value.offset += (j - (i + 1)) * sizeof(f); while (j > i + 1) { @@ -1862,6 +1887,9 @@ nxt_unit_request_group_dup_fields(nxt_unit_request_info_t *req) fields[j] = f; + /* Assign the same name pointer for further grouping simplicity. */ + nxt_unit_sptr_set(&fields[j].name, name); + i++; } } @@ -6335,6 +6363,35 @@ nxt_memalign(size_t alignment, size_t size) return NULL; } + +static int +nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length) +{ + u_char c1, c2; + nxt_int_t n; + const u_char *s1, *s2; + + s1 = p1; + s2 = p2; + + while (length-- != 0) { + c1 = *s1++; + c2 = *s2++; + + c1 = nxt_lowcase(c1); + c2 = nxt_lowcase(c2); + + n = c1 - c2; + + if (n != 0) { + return n; + } + } + + return 0; +} + + #if (NXT_DEBUG) void -- cgit From d5973fb557c6eca95da671f80637955b5eca00d3 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 15 Sep 2020 20:12:25 +0300 Subject: Python: changed request headers format in router protocol. The coming ASGI support requires raw HTTP headers format. Headers grouping and upcase code were moved to WSGI module. --- src/nxt_router.c | 2 +- src/python/nxt_python_wsgi.c | 116 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/nxt_router.c b/src/nxt_router.c index c08bf7d7..ea14c6fb 100644 --- a/src/nxt_router.c +++ b/src/nxt_router.c @@ -259,7 +259,7 @@ static const nxt_str_t empty_prefix = nxt_string(""); static const nxt_str_t *nxt_app_msg_prefix[] = { &empty_prefix, - &http_prefix, + &empty_prefix, &http_prefix, &http_prefix, &http_prefix, diff --git a/src/python/nxt_python_wsgi.c b/src/python/nxt_python_wsgi.c index 3371dae6..3a5842f1 100644 --- a/src/python/nxt_python_wsgi.c +++ b/src/python/nxt_python_wsgi.c @@ -43,6 +43,7 @@ #define PyString_FromStringAndSize(str, size) \ PyUnicode_DecodeLatin1((str), (size), "strict") +#define PyString_AS_STRING PyUnicode_DATA #else #define NXT_PYTHON_BYTES_TYPE "string" @@ -73,7 +74,10 @@ static PyObject *nxt_python_get_environ(nxt_python_run_ctx_t *ctx); static int nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, nxt_unit_sptr_t *sptr, uint32_t size); static int nxt_python_add_field(nxt_python_run_ctx_t *ctx, - nxt_unit_field_t *field); + nxt_unit_field_t *field, int n, uint32_t vl); +static PyObject *nxt_python_field_name(const char *name, uint8_t len); +static PyObject *nxt_python_field_value(nxt_unit_field_t *f, int n, + uint32_t vl); static int nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, PyObject *value); @@ -511,9 +515,9 @@ static PyObject * nxt_python_get_environ(nxt_python_run_ctx_t *ctx) { int rc; - uint32_t i; + uint32_t i, j, vl; PyObject *environ; - nxt_unit_field_t *f; + nxt_unit_field_t *f, *f2; nxt_unit_request_t *r; environ = PyDict_Copy(nxt_py_environ_ptyp); @@ -565,10 +569,27 @@ nxt_python_get_environ(nxt_python_run_ctx_t *ctx) r->server_name_length)); RC(nxt_python_add_obj(ctx, nxt_py_server_port_str, nxt_py_80_str)); - for (i = 0; i < r->fields_count; i++) { + nxt_unit_request_group_dup_fields(ctx->req); + + for (i = 0; i < r->fields_count;) { f = r->fields + i; + vl = f->value_length; + + for (j = i + 1; j < r->fields_count; j++) { + f2 = r->fields + j; + + if (f2->hash != f->hash + || nxt_unit_sptr_get(&f2->name) != nxt_unit_sptr_get(&f->name)) + { + break; + } + + vl += 2 + f2->value_length; + } + + RC(nxt_python_add_field(ctx, f, j - i, vl)); - RC(nxt_python_add_field(ctx, f)); + i = j; } if (r->content_length_field != NXT_UNIT_NONE_FIELD) { @@ -632,14 +653,15 @@ nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, static int -nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field) +nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field, int n, + uint32_t vl) { char *src; PyObject *name, *value; src = nxt_unit_sptr_get(&field->name); - name = PyString_FromStringAndSize(src, field->name_length); + name = nxt_python_field_name(src, field->name_length); if (nxt_slow_path(name == NULL)) { nxt_unit_req_error(ctx->req, "Python failed to create name string \"%.*s\"", @@ -649,13 +671,13 @@ nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field) return NXT_UNIT_ERROR; } - src = nxt_unit_sptr_get(&field->value); + value = nxt_python_field_value(field, n, vl); - value = PyString_FromStringAndSize(src, field->value_length); if (nxt_slow_path(value == NULL)) { nxt_unit_req_error(ctx->req, "Python failed to create value string \"%.*s\"", - (int) field->value_length, src); + (int) field->value_length, + (char *) nxt_unit_sptr_get(&field->value)); nxt_python_print_exception(); goto fail; @@ -682,6 +704,80 @@ fail: } +static PyObject * +nxt_python_field_name(const char *name, uint8_t len) +{ + char *p, c; + uint8_t i; + PyObject *res; + +#if PY_MAJOR_VERSION == 3 + res = PyUnicode_New(len + 5, 255); +#else + res = PyString_FromStringAndSize(NULL, len + 5); +#endif + + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + p = PyString_AS_STRING(res); + + p = nxt_cpymem(p, "HTTP_", 5); + + for (i = 0; i < len; i++) { + c = name[i]; + + if (c >= 'a' && c <= 'z') { + *p++ = (c & ~0x20); + continue; + } + + if (c == '-') { + *p++ = '_'; + continue; + } + + *p++ = c; + } + + return res; +} + + +static PyObject * +nxt_python_field_value(nxt_unit_field_t *f, int n, uint32_t vl) +{ + int i; + char *p, *src; + PyObject *res; + +#if PY_MAJOR_VERSION == 3 + res = PyUnicode_New(vl, 255); +#else + res = PyString_FromStringAndSize(NULL, vl); +#endif + + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + p = PyString_AS_STRING(res); + + src = nxt_unit_sptr_get(&f->value); + p = nxt_cpymem(p, src, f->value_length); + + for (i = 1; i < n; i++) { + p = nxt_cpymem(p, ", ", 2); + + src = nxt_unit_sptr_get(&f[i].value); + p = nxt_cpymem(p, src, f[i].value_length); + } + + return res; +} + + static int nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, PyObject *value) { -- cgit From 77ecb6ab49257dd662aa9c461fed3dc1d74e5092 Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Wed, 16 Sep 2020 17:52:53 +0100 Subject: Isolation: remove redundant macro. --- src/nxt_clone.h | 3 --- src/nxt_isolation.c | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/nxt_clone.h b/src/nxt_clone.h index e89fd82d..c2066ce6 100644 --- a/src/nxt_clone.h +++ b/src/nxt_clone.h @@ -42,9 +42,6 @@ pid_t nxt_clone(nxt_int_t flags); #if (NXT_HAVE_CLONE_NEWUSER) -#define NXT_CLONE_MNT(flags) \ - ((flags & CLONE_NEWNS) == CLONE_NEWNS) - NXT_EXPORT nxt_int_t nxt_clone_credential_map(nxt_task_t *task, pid_t pid, nxt_credential_t *creds, nxt_clone_t *clone); NXT_EXPORT nxt_int_t nxt_clone_vldt_credential_uidmap(nxt_task_t *task, diff --git a/src/nxt_isolation.c b/src/nxt_isolation.c index a497b729..0ff63e50 100644 --- a/src/nxt_isolation.c +++ b/src/nxt_isolation.c @@ -707,7 +707,7 @@ nxt_isolation_change_root(nxt_task_t *task, nxt_process_t *process) nxt_debug(task, "change root: %s", rootfs); - if (NXT_CLONE_MNT(process->isolation.clone.flags)) { + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) { ret = nxt_isolation_pivot_root(task, rootfs); } else { -- cgit From d5e915934066c77a59d211efafca10c117b73d05 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 16 Sep 2020 21:31:15 +0100 Subject: Tests: migrated to the pytest. --- pkg/deb/Makefile | 4 +- pkg/rpm/Makefile | 4 +- test/conftest.py | 299 ++++++ test/run.py | 19 - test/test_access_log.py | 160 ++- test/test_configuration.py | 418 +++----- test/test_go_application.py | 113 +- test/test_go_isolation.py | 175 ++- test/test_go_isolation_rootfs.py | 22 +- test/test_http_header.py | 294 +++--- test/test_java_application.py | 1311 ++++++++++------------- test/test_java_isolation_rootfs.py | 61 +- test/test_java_websockets.py | 225 ++-- test/test_node_application.py | 251 ++--- test/test_node_websockets.py | 227 ++-- test/test_perl_application.py | 189 ++-- test/test_php_application.py | 495 ++++----- test/test_php_basic.py | 150 +-- test/test_php_isolation.py | 154 +-- test/test_php_targets.py | 192 ++-- test/test_proxy.py | 417 +++----- test/test_proxy_chunked.py | 135 ++- test/test_python_application.py | 514 ++++----- test/test_python_basic.py | 154 +-- test/test_python_environment.py | 108 +- test/test_python_isolation.py | 76 +- test/test_python_isolation_chroot.py | 62 +- test/test_python_procman.py | 136 +-- test/test_respawn.py | 45 +- test/test_return.py | 140 ++- test/test_routing.py | 1877 ++++++++++++++------------------- test/test_routing_tls.py | 48 +- test/test_ruby_application.py | 275 +++-- test/test_ruby_isolation.py | 54 +- test/test_settings.py | 61 +- test/test_share_fallback.py | 125 +-- test/test_static.py | 529 ++++------ test/test_tls.py | 398 ++++--- test/test_upstreams_rr.py | 594 +++++------ test/test_usr1.py | 76 +- test/test_variables.py | 89 +- test/unit/applications/lang/go.py | 23 +- test/unit/applications/lang/java.py | 10 +- test/unit/applications/lang/node.py | 13 +- test/unit/applications/lang/perl.py | 3 +- test/unit/applications/lang/php.py | 3 +- test/unit/applications/lang/python.py | 12 +- test/unit/applications/lang/ruby.py | 3 +- test/unit/applications/proto.py | 18 +- test/unit/applications/tls.py | 23 +- test/unit/applications/websockets.py | 14 +- test/unit/control.py | 2 +- test/unit/feature/isolation.py | 4 +- test/unit/http.py | 32 +- test/unit/main.py | 269 +---- 55 files changed, 4780 insertions(+), 6325 deletions(-) create mode 100644 test/conftest.py delete mode 100755 test/run.py diff --git a/pkg/deb/Makefile b/pkg/deb/Makefile index 55bf7082..efeb642f 100644 --- a/pkg/deb/Makefile +++ b/pkg/deb/Makefile @@ -299,7 +299,7 @@ test: unit modules test -h debuild/unit-$(VERSION)/debian/build-unit/build/$${soname} || \ ln -fs `pwd`/$${so} debuild/unit-$(VERSION)/debian/build-unit/build/$${soname} ; \ done ; \ - ( cd debuild/unit-$(VERSION)/debian/build-unit && ./test/run.py ) ; \ + ( cd debuild/unit-$(VERSION)/debian/build-unit && env python3 -m pytest ) ; \ } test-debug: unit modules @@ -310,7 +310,7 @@ test-debug: unit modules test -h debuild/unit-$(VERSION)/debian/build-unit-debug/build/$${soname} || \ ln -fs `pwd`/$${so} debuild/unit-$(VERSION)/debian/build-unit-debug/build/$${soname} ; \ done ; \ - ( cd debuild/unit-$(VERSION)/debian/build-unit-debug && ./test/run.py ) ; \ + ( cd debuild/unit-$(VERSION)/debian/build-unit-debug && env python3 -m pytest ) ; \ } clean: diff --git a/pkg/rpm/Makefile b/pkg/rpm/Makefile index 70100896..1944d58d 100644 --- a/pkg/rpm/Makefile +++ b/pkg/rpm/Makefile @@ -274,7 +274,7 @@ test: unit modules test -h rpmbuild/BUILD/unit-$(VERSION)/build-nodebug/$${soname} || \ ln -fs `pwd`/$${so} rpmbuild/BUILD/unit-$(VERSION)/build-nodebug/$${soname} ; \ done ; \ - ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-nodebug build && ./test/run.py ) ; \ + ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-nodebug build && env python3 -m pytest ) ; \ } test-debug: unit modules @@ -285,7 +285,7 @@ test-debug: unit modules test -h rpmbuild/BUILD/unit-$(VERSION)/build-debug/$${soname} || \ ln -fs `pwd`/$${so} rpmbuild/BUILD/unit-$(VERSION)/build-debug/$${soname} ; \ done ; \ - ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-debug build && ./test/run.py ) ; \ + ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-debug build && env python3 -m pytest ) ; \ } clean: diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..8683a023 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,299 @@ +import fcntl +import os +import platform +import pytest +import signal +import stat +import subprocess +import sys +import re +import tempfile +import time + + +def pytest_addoption(parser): + parser.addoption( + "--detailed", + default=False, + action="store_true", + help="Detailed output for tests", + ) + parser.addoption( + "--print_log", + default=False, + action="store_true", + help="Print unit.log to stdout in case of errors", + ) + parser.addoption( + "--save_log", + default=False, + action="store_true", + help="Save unit.log after the test execution", + ) + parser.addoption( + "--unsafe", + default=False, + action="store_true", + help="Run unsafe tests", + ) + + +unit_instance = {} +option = None + + +def pytest_configure(config): + global option + option = config.option + + option.generated_tests = {} + option.current_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir) + ) + option.test_dir = option.current_dir + '/test' + option.architecture = platform.architecture()[0] + option.system = platform.system() + + # set stdout to non-blocking + + if option.detailed or option.print_log: + fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) + + +def pytest_generate_tests(metafunc): + cls = metafunc.cls + if not hasattr(cls, 'application_type'): + return + + type = cls.application_type + + # take available module from option and generate tests for each version + + for module in cls.prerequisites['modules']: + if module in option.available['modules']: + prereq_version = cls.prerequisites['modules'][module] + available_versions = option.available['modules'][module] + + if prereq_version == 'all': + metafunc.fixturenames.append('tmp_ct') + metafunc.parametrize('tmp_ct', range(len(available_versions))) + + for i in range(len(available_versions)): + version = available_versions[i] + option.generated_tests[ + metafunc.function.__name__ + '[{}]'.format(i) + ] = (type + ' ' + version) + elif prereq_version == 'any': + option.generated_tests[metafunc.function.__name__] = ( + type + ' ' + available_versions[0] + ) + else: + for version in available_versions: + if version.startswith(prereq_version): + option.generated_tests[metafunc.function.__name__] = ( + type + ' ' + version + ) + + +def pytest_sessionstart(session): + option.available = {'modules': {}, 'features': {}} + + unit = unit_run() + + # read unit.log + + for i in range(50): + with open(unit['temp_dir'] + '/unit.log', 'r') as f: + log = f.read() + m = re.search('controller started', log) + + if m is None: + time.sleep(0.1) + else: + break + + if m is None: + _print_log() + exit("Unit is writing log too long") + + # discover available modules from unit.log + + for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): + if module[0] not in option.available['modules']: + option.available['modules'][module[0]] = [module[1]] + else: + option.available['modules'][module[0]].append(module[1]) + + unit_stop() + + +def setup_method(self): + option.skip_alerts = [ + r'read signalfd\(4\) failed', + r'sendmsg.+failed', + r'recvmsg.+failed', + ] + option.skip_sanitizer = False + +def unit_run(): + global unit_instance + build_dir = option.current_dir + '/build' + unitd = build_dir + '/unitd' + + if not os.path.isfile(unitd): + exit('Could not find unit') + + temp_dir = tempfile.mkdtemp(prefix='unit-test-') + public_dir(temp_dir) + + if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777': + public_dir(build_dir) + + os.mkdir(temp_dir + '/state') + + with open(temp_dir + '/unit.log', 'w') as log: + unit_instance['process'] = subprocess.Popen( + [ + unitd, + '--no-daemon', + '--modules', + build_dir, + '--state', + temp_dir + '/state', + '--pid', + temp_dir + '/unit.pid', + '--log', + temp_dir + '/unit.log', + '--control', + 'unix:' + temp_dir + '/control.unit.sock', + '--tmp', + temp_dir, + ], + stderr=log, + ) + + if not waitforfiles(temp_dir + '/control.unit.sock'): + _print_log() + exit('Could not start unit') + + # dumb (TODO: remove) + option.skip_alerts = [ + r'read signalfd\(4\) failed', + r'sendmsg.+failed', + r'recvmsg.+failed', + ] + option.skip_sanitizer = False + + 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 + + return unit_instance + + +def unit_stop(): + p = unit_instance['process'] + + if p.poll() is not None: + return + + p.send_signal(signal.SIGQUIT) + + try: + retcode = p.wait(15) + if retcode: + return 'Child process terminated with code ' + str(retcode) + except: + p.kill() + return 'Could not terminate unit' + + +def public_dir(path): + os.chmod(path, 0o777) + + for root, dirs, files in os.walk(path): + for d in dirs: + os.chmod(os.path.join(root, d), 0o777) + for f in files: + os.chmod(os.path.join(root, f), 0o777) + +def waitforfiles(*files): + for i in range(50): + wait = False + ret = False + + for f in files: + if not os.path.exists(f): + wait = True + break + + if wait: + time.sleep(0.1) + + else: + ret = True + break + + return ret + + +def skip_alert(*alerts): + option.skip_alerts.extend(alerts) + + +def _check_alerts(log): + found = False + + alerts = re.findall(r'.+\[alert\].+', log) + + if alerts: + print('All alerts/sanitizer errors found in log:') + [print(alert) for alert in alerts] + found = True + + if option.skip_alerts: + for skip in option.skip_alerts: + alerts = [al for al in alerts if re.search(skip, al) is None] + + if alerts: + _print_log(log) + assert not alerts, 'alert(s)' + + if not option.skip_sanitizer: + sanitizer_errors = re.findall('.+Sanitizer.+', log) + + if sanitizer_errors: + _print_log(log) + assert not sanitizer_errors, 'sanitizer error(s)' + + if found: + print('skipped.') + + +def _print_log(data=None): + unit_log = unit_instance['log'] + + print('Path to unit.log:\n' + unit_log + '\n') + + if option.print_log: + os.set_blocking(sys.stdout.fileno(), True) + sys.stdout.flush() + + if data is None: + with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: + shutil.copyfileobj(f, sys.stdout) + else: + sys.stdout.write(data) + + +@pytest.fixture +def is_unsafe(request): + return request.config.getoption("--unsafe") + +@pytest.fixture +def is_su(request): + return os.geteuid() == 0 + +def pytest_sessionfinish(session): + unit_stop() diff --git a/test/run.py b/test/run.py deleted file mode 100755 index 384663f9..00000000 --- a/test/run.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -import unittest - -if __name__ == '__main__': - loader = unittest.TestLoader() - suite = unittest.TestSuite() - - this_dir = os.path.dirname(__file__) - tests = loader.discover(start_dir=this_dir) - suite.addTests(tests) - - runner = unittest.TextTestRunner(stream=sys.stdout, verbosity=3) - result = runner.run(suite) - - ret = not (len(result.failures) == len(result.errors) == 0) - - sys.exit(ret) diff --git a/test/test_access_log.py b/test/test_access_log.py index 3ef8f7a0..010c56c2 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -1,5 +1,5 @@ +import pytest import time -import unittest from unit.applications.lang.python import TestApplicationPython @@ -10,11 +10,9 @@ class TestAccessLog(TestApplicationPython): def load(self, script): super().load(script) - self.assertIn( - 'success', - self.conf('"' + self.testdir + '/access.log"', 'access_log'), - 'access_log configure', - ) + assert 'success' in self.conf( + '"' + self.temp_dir + '/access.log"', 'access_log' + ), 'access_log configure' def wait_for_record(self, pattern, name='access.log'): return super().wait_for_record(pattern, name) @@ -22,7 +20,7 @@ class TestAccessLog(TestApplicationPython): def test_access_log_keepalive(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' (resp, sock) = self.post( headers={ @@ -35,9 +33,9 @@ class TestAccessLog(TestApplicationPython): read_timeout=1, ) - self.assertIsNotNone( - self.wait_for_record(r'"POST / HTTP/1.1" 200 5'), 'keepalive 1' - ) + assert ( + self.wait_for_record(r'"POST / HTTP/1.1" 200 5') is not None + ), 'keepalive 1' resp = self.post( headers={ @@ -51,9 +49,9 @@ class TestAccessLog(TestApplicationPython): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"POST / HTTP/1.1" 200 10'), 'keepalive 2' - ) + assert ( + self.wait_for_record(r'"POST / HTTP/1.1" 200 10') is not None + ), 'keepalive 2' def test_access_log_pipeline(self): self.load('empty') @@ -79,18 +77,18 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"'), - 'pipeline 1', - ) - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-2" "-"'), - 'pipeline 2', - ) - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-3" "-"'), - 'pipeline 3', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"') + is not None + ), 'pipeline 1' + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-2" "-"') + is not None + ), 'pipeline 2' + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-3" "-"') + is not None + ), 'pipeline 3' def test_access_log_ipv6(self): self.load('empty') @@ -101,17 +99,17 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'::1 - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"' - ), - 'ipv6', - ) + ) + is not None + ), 'ipv6' def test_access_log_unix(self): self.load('empty') - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' self.conf( {"unix:" + addr: {"pass": "applications/empty"}}, 'listeners' @@ -121,12 +119,12 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'unix: - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"' - ), - 'unix', - ) + ) + is not None + ), 'unix' def test_access_log_referer(self): self.load('empty') @@ -141,12 +139,10 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record( - r'"GET / HTTP/1.1" 200 0 "referer-value" "-"' - ), - 'referer', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "referer-value" "-"') + is not None + ), 'referer' def test_access_log_user_agent(self): self.load('empty') @@ -161,12 +157,12 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'"GET / HTTP/1.1" 200 0 "-" "user-agent-value"' - ), - 'user agent', - ) + ) + is not None + ), 'user agent' def test_access_log_http10(self): self.load('empty') @@ -175,14 +171,14 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"'), 'http 1.0' - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"') is not None + ), 'http 1.0' def test_access_log_partial(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' resp = self.http(b"""GE""", raw=True, read_timeout=1) @@ -190,27 +186,27 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GE" 400 0 "-" "-"'), 'partial' - ) + assert ( + self.wait_for_record(r'"GE" 400 0 "-" "-"') is not None + ), 'partial' def test_access_log_partial_2(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' self.http(b"""GET /\n""", raw=True) self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET /" 400 \d+ "-" "-"'), 'partial 2' - ) + assert ( + self.wait_for_record(r'"GET /" 400 \d+ "-" "-"') is not None + ), 'partial 2' def test_access_log_partial_3(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' resp = self.http(b"""GET / HTTP/1.1""", raw=True, read_timeout=1) @@ -218,14 +214,14 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET /" 400 0 "-" "-"'), 'partial 3' - ) + assert ( + self.wait_for_record(r'"GET /" 400 0 "-" "-"') is not None + ), 'partial 3' def test_access_log_partial_4(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' resp = self.http(b"""GET / HTTP/1.1\n""", raw=True, read_timeout=1) @@ -233,25 +229,24 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 400 0 "-" "-"'), - 'partial 4', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 400 0 "-" "-"') is not None + ), 'partial 4' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_access_log_partial_5(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' self.get(headers={'Connection': 'close'}) self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"'), - 'partial 5', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"') + is not None + ), 'partial 5' def test_access_log_get_parameters(self): self.load('empty') @@ -260,12 +255,12 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'"GET /\?blah&var=val HTTP/1.1" 200 0 "-" "-"' - ), - 'get parameters', - ) + ) + is not None + ), 'get parameters' def test_access_log_delete(self): self.load('empty') @@ -276,25 +271,20 @@ Connection: close self.stop() - self.assertIsNone( - self.search_in_log(r'/delete', 'access.log'), 'delete' - ) + assert self.search_in_log(r'/delete', 'access.log') is None, 'delete' def test_access_log_change(self): self.load('empty') self.get() - self.conf('"' + self.testdir + '/new.log"', 'access_log') + self.conf('"' + self.temp_dir + '/new.log"', 'access_log') self.get() self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log'), - 'change', - ) - -if __name__ == '__main__': - TestAccessLog.main() + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log') + is not None + ), 'change' diff --git a/test/test_configuration.py b/test/test_configuration.py index 0b0c9c78..07b8d522 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,22 +1,21 @@ -import unittest +import pytest from unit.control import TestControl +from conftest import skip_alert class TestConfiguration(TestControl): prerequisites = {'modules': {'python': 'any'}} def test_json_empty(self): - self.assertIn('error', self.conf(''), 'empty') + assert 'error' in self.conf(''), 'empty' def test_json_leading_zero(self): - self.assertIn('error', self.conf('00'), 'leading zero') + assert 'error' in self.conf('00'), 'leading zero' def test_json_unicode(self): - self.assertIn( - 'success', - self.conf( - b""" + assert 'success' in self.conf( + b""" { "ap\u0070": { "type": "\u0070ython", @@ -26,50 +25,36 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'unicode', - ) + 'applications', + ), 'unicode' + + assert self.conf_get('applications') == { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, 'unicode get' - self.assertDictEqual( - self.conf_get('applications'), + def test_json_unicode_2(self): + assert 'success' in self.conf( { - "app": { + "приложение": { "type": "python", "processes": {"spare": 0}, "path": "/app", "module": "wsgi", } }, - 'unicode get', - ) + 'applications', + ), 'unicode 2' - def test_json_unicode_2(self): - self.assertIn( - 'success', - self.conf( - { - "приложение": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - 'applications', - ), - 'unicode 2', - ) - - self.assertIn( - 'приложение', self.conf_get('applications'), 'unicode 2 get' - ) + assert 'приложение' in self.conf_get('applications'), 'unicode 2 get' def test_json_unicode_number(self): - self.assertIn( - 'error', - self.conf( - b""" + assert 'error' in self.conf( + b""" { "app": { "type": "python", @@ -79,16 +64,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'unicode number', - ) + 'applications', + ), 'unicode number' def test_json_utf8_bom(self): - self.assertIn( - 'success', - self.conf( - b"""\xEF\xBB\xBF + assert 'success' in self.conf( + b"""\xEF\xBB\xBF { "app": { "type": "python", @@ -98,16 +79,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'UTF-8 BOM', - ) + 'applications', + ), 'UTF-8 BOM' def test_json_comment_single_line(self): - self.assertIn( - 'success', - self.conf( - b""" + assert 'success' in self.conf( + b""" // this is bridge { "//app": { @@ -121,16 +98,12 @@ class TestConfiguration(TestControl): } // end of json \xEF\t """, - 'applications', - ), - 'single line comments', - ) + 'applications', + ), 'single line comments' def test_json_comment_multi_line(self): - self.assertIn( - 'success', - self.conf( - b""" + assert 'success' in self.conf( + b""" /* this is bridge */ { "/*app": { @@ -148,41 +121,31 @@ class TestConfiguration(TestControl): } /* end of json \xEF\t\b */ """, - 'applications', - ), - 'multi line comments', - ) + 'applications', + ), 'multi line comments' def test_json_comment_invalid(self): - self.assertIn('error', self.conf(b'/{}', 'applications'), 'slash') - self.assertIn('error', self.conf(b'//{}', 'applications'), 'comment') - self.assertIn('error', self.conf(b'{} /', 'applications'), 'slash end') - self.assertIn( - 'error', self.conf(b'/*{}', 'applications'), 'slash star' - ) - self.assertIn( - 'error', self.conf(b'{} /*', 'applications'), 'slash star end' - ) + assert 'error' in self.conf(b'/{}', 'applications'), 'slash' + assert 'error' in self.conf(b'//{}', 'applications'), 'comment' + assert 'error' in self.conf(b'{} /', 'applications'), 'slash end' + assert 'error' in self.conf(b'/*{}', 'applications'), 'slash star' + assert 'error' in self.conf(b'{} /*', 'applications'), 'slash star end' def test_applications_open_brace(self): - self.assertIn('error', self.conf('{', 'applications'), 'open brace') + assert 'error' in self.conf('{', 'applications'), 'open brace' def test_applications_string(self): - self.assertIn('error', self.conf('"{}"', 'applications'), 'string') + assert 'error' in self.conf('"{}"', 'applications'), 'string' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet, unsafe') def test_applications_type_only(self): - self.assertIn( - 'error', - self.conf({"app": {"type": "python"}}, 'applications'), - 'type only', - ) + assert 'error' in self.conf( + {"app": {"type": "python"}}, 'applications' + ), 'type only' def test_applications_miss_quote(self): - self.assertIn( - 'error', - self.conf( - """ + assert 'error' in self.conf( + """ { app": { "type": "python", @@ -192,16 +155,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'miss quote', - ) + 'applications', + ), 'miss quote' def test_applications_miss_colon(self): - self.assertIn( - 'error', - self.conf( - """ + assert 'error' in self.conf( + """ { "app" { "type": "python", @@ -211,16 +170,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'miss colon', - ) + 'applications', + ), 'miss colon' def test_applications_miss_comma(self): - self.assertIn( - 'error', - self.conf( - """ + assert 'error' in self.conf( + """ { "app": { "type": "python" @@ -230,144 +185,117 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'miss comma', - ) + 'applications', + ), 'miss comma' def test_applications_skip_spaces(self): - self.assertIn( - 'success', self.conf(b'{ \n\r\t}', 'applications'), 'skip spaces' - ) + assert 'success' in self.conf( + b'{ \n\r\t}', 'applications' + ), 'skip spaces' def test_applications_relative_path(self): - self.assertIn( - 'success', - self.conf( - { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "../app", - "module": "wsgi", - } - }, - 'applications', - ), - 'relative path', - ) + assert 'success' in self.conf( + { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "../app", + "module": "wsgi", + } + }, + 'applications', + ), 'relative path' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet, unsafe') def test_listeners_empty(self): - self.assertIn( - 'error', self.conf({"*:7080": {}}, 'listeners'), 'listener empty' - ) + assert 'error' in self.conf( + {"*:7080": {}}, 'listeners' + ), 'listener empty' def test_listeners_no_app(self): - self.assertIn( - 'error', - self.conf({"*:7080": {"pass": "applications/app"}}, 'listeners'), - 'listeners no app', - ) + assert 'error' in self.conf( + {"*:7080": {"pass": "applications/app"}}, 'listeners' + ), 'listeners no app' def test_listeners_wildcard(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'listeners wildcard', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'listeners wildcard' def test_listeners_explicit(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"127.0.0.1:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'explicit', - ) + assert 'success' in self.conf( + { + "listeners": {"127.0.0.1:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'explicit' def test_listeners_explicit_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"[::1]:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'explicit ipv6', - ) + assert 'success' in self.conf( + { + "listeners": {"[::1]:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'explicit ipv6' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet, unsafe') def test_listeners_no_port(self): - self.assertIn( - 'error', - self.conf( - { - "listeners": {"127.0.0.1": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'no port', - ) + assert 'error' in self.conf( + { + "listeners": {"127.0.0.1": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'no port' def test_json_application_name_large(self): name = "X" * 1024 * 1024 - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/" + name}}, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/" + name}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } ) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_json_application_many(self): apps = 999 @@ -388,7 +316,7 @@ class TestConfiguration(TestControl): }, } - self.assertIn('success', self.conf(conf)) + assert 'success' in self.conf(conf) def test_json_application_many2(self): conf = { @@ -407,35 +335,21 @@ class TestConfiguration(TestControl): "listeners": {"*:7080": {"pass": "applications/app-1"}}, } - self.assertIn('success', self.conf(conf)) - - def test_unprivileged_user_error(self): - self.skip_alerts.extend( - [ - r'cannot set user "root"', - r'failed to apply new conf', - ] - ) - if self.is_su: - print('unprivileged tests, skip this') - raise unittest.SkipTest() - - self.assertIn( - 'error', - self.conf( - { - "app": { - "type": "external", - "processes": 1, - "executable": "/app", - "user": "root", - } - }, - 'applications', - ), - 'setting user', - ) + assert 'success' in self.conf(conf) + def test_unprivileged_user_error(self, is_su): + skip_alert(r'cannot set user "root"', r'failed to apply new conf') + if is_su: + pytest.skip('unprivileged tests') -if __name__ == '__main__': - TestConfiguration.main() + assert 'error' in self.conf( + { + "app": { + "type": "external", + "processes": 1, + "executable": "/app", + "user": "root", + } + }, + 'applications', + ), 'setting user' diff --git a/test/test_go_application.py b/test/test_go_application.py index b9b78e2b..828a8e6e 100644 --- a/test/test_go_application.py +++ b/test/test_go_application.py @@ -1,4 +1,5 @@ from unit.applications.lang.go import TestApplicationGo +import re class TestGoApplication(TestApplicationGo): @@ -19,44 +20,38 @@ class TestGoApplication(TestApplicationGo): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) - - self.assertDictEqual( - headers, - { - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Server-Protocol-Major': '1', - 'Server-Protocol-Minor': '1', - 'Custom-Header': 'blah', - 'Connection': 'close', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Server-Protocol-Major': '1', + 'Server-Protocol-Minor': '1', + 'Custom-Header': 'blah', + 'Connection': 'close', + }, 'headers' + assert resp['body'] == body, 'body' def test_go_application_get_variables(self): self.load('get_variables') resp = self.get(url='/?var1=val1&var2=&var3') - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'GET variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'GET variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' + assert resp['headers']['X-Var-2'] == '', 'GET variables 2' + assert resp['headers']['X-Var-3'] == '', 'GET variables 3' def test_go_application_post_variables(self): self.load('post_variables') @@ -70,24 +65,24 @@ class TestGoApplication(TestApplicationGo): body='var1=val1&var2=&var3', ) - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'POST variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'POST variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' + assert resp['headers']['X-Var-2'] == '', 'POST variables 2' + assert resp['headers']['X-Var-3'] == '', 'POST variables 3' def test_go_application_404(self): self.load('404') resp = self.get() - self.assertEqual(resp['status'], 404, '404 status') - self.assertRegex( - resp['body'], r'404 Not Found', '404 body' - ) + assert resp['status'] == 404, '404 status' + assert re.search( + r'404 Not Found', resp['body'] + ), '404 body' def test_go_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -101,7 +96,7 @@ class TestGoApplication(TestApplicationGo): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -114,7 +109,7 @@ class TestGoApplication(TestApplicationGo): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_go_application_cookies(self): self.load('cookies') @@ -127,28 +122,24 @@ class TestGoApplication(TestApplicationGo): } ) - self.assertEqual(resp['headers']['X-Cookie-1'], 'val1', 'cookie 1') - self.assertEqual(resp['headers']['X-Cookie-2'], 'val2', 'cookie 2') + assert resp['headers']['X-Cookie-1'] == 'val1', 'cookie 1' + assert resp['headers']['X-Cookie-2'] == 'val2', 'cookie 2' def test_go_application_command_line_arguments_type(self): self.load('command_line_arguments') - self.assertIn( - 'error', + assert 'error' in \ self.conf( '' "a b c", 'applications/command_line_arguments/arguments' - ), - 'arguments type', - ) + ), \ + 'arguments type' def test_go_application_command_line_arguments_0(self): self.load('command_line_arguments') - self.assertEqual( - self.get()['headers']['X-Arg-0'], - self.conf_get('applications/command_line_arguments/executable'), - 'argument 0', - ) + assert self.get()['headers']['X-Arg-0'] == self.conf_get( + 'applications/command_line_arguments/executable' + ), 'argument 0' def test_go_application_command_line_arguments(self): self.load('command_line_arguments') @@ -162,9 +153,9 @@ class TestGoApplication(TestApplicationGo): 'applications/command_line_arguments/arguments', ) - self.assertEqual( - self.get()['body'], arg1 + ',' + arg2 + ',' + arg3, 'arguments' - ) + assert ( + self.get()['body'] == arg1 + ',' + arg2 + ',' + arg3 + ), 'arguments' def test_go_application_command_line_arguments_change(self): self.load('command_line_arguments') @@ -173,18 +164,14 @@ class TestGoApplication(TestApplicationGo): self.conf('["0", "a", "$", ""]', args_path) - self.assertEqual(self.get()['body'], '0,a,$,', 'arguments') + assert self.get()['body'] == '0,a,$,', 'arguments' self.conf('["-1", "b", "%"]', args_path) - self.assertEqual(self.get()['body'], '-1,b,%', 'arguments change') + assert self.get()['body'] == '-1,b,%', 'arguments change' self.conf('[]', args_path) - self.assertEqual( - self.get()['headers']['Content-Length'], '0', 'arguments empty' - ) - - -if __name__ == '__main__': - TestGoApplication.main() + assert ( + self.get()['headers']['Content-Length'] == '0' + ), 'arguments empty' diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py index 76434f62..bcfdd015 100644 --- a/test/test_go_isolation.py +++ b/test/test_go_isolation.py @@ -1,21 +1,21 @@ import grp +import os import pwd -import unittest +import pytest from unit.applications.lang.go import TestApplicationGo from unit.feature.isolation import TestFeatureIsolation - class TestGoIsolation(TestApplicationGo): prerequisites = {'modules': {'go': 'any'}, 'features': ['isolation']} isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() @@ -41,24 +41,20 @@ class TestGoIsolation(TestApplicationGo): for ns, ns_value in self.available['features']['isolation'].items(): if ns.upper() in obj['NS']: - self.assertEqual( - obj['NS'][ns.upper()], ns_value, '%s match' % ns - ) + assert obj['NS'][ns.upper()] == ns_value, '%s match' % ns - def test_isolation_unpriv_user(self): + def test_isolation_unpriv_user(self, is_su): if not self.isolation_key('unprivileged_userns_clone'): - print('unprivileged clone is not available') - raise unittest.SkipTest() + pytest.skip('unprivileged clone is not available') - if self.is_su: - print('privileged tests, skip this') - raise unittest.SkipTest() + if is_su: + pytest.skip('privileged tests, skip this') self.load('ns_inspect') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], self.uid, 'uid match') - self.assertEqual(obj['GID'], self.gid, 'gid match') + assert obj['UID'] == os.geteuid(), 'uid match' + assert obj['GID'] == os.getegid(), 'gid match' self.load('ns_inspect', isolation={'namespaces': {'credential': True}}) @@ -67,8 +63,8 @@ class TestGoIsolation(TestApplicationGo): nobody_uid, nogroup_gid, nogroup = self.unpriv_creds() # unprivileged unit map itself to nobody in the container by default - self.assertEqual(obj['UID'], nobody_uid, 'uid of nobody') - self.assertEqual(obj['GID'], nogroup_gid, 'gid of %s' % nogroup) + assert obj['UID'] == nobody_uid, 'uid of nobody' + assert obj['GID'] == nogroup_gid, 'gid of %s' % nogroup self.load( 'ns_inspect', @@ -78,8 +74,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match user=root') - self.assertEqual(obj['GID'], 0, 'gid match user=root') + assert obj['UID'] == 0, 'uid match user=root' + assert obj['GID'] == 0, 'gid match user=root' self.load( 'ns_inspect', @@ -90,10 +86,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match user=root group=nogroup') - self.assertEqual( - obj['GID'], nogroup_gid, 'gid match user=root group=nogroup' - ) + assert obj['UID'] == 0, 'uid match user=root group=nogroup' + assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup' self.load( 'ns_inspect', @@ -101,20 +95,19 @@ class TestGoIsolation(TestApplicationGo): group='root', isolation={ 'namespaces': {'credential': True}, - 'uidmap': [{'container': 0, 'host': self.uid, 'size': 1}], - 'gidmap': [{'container': 0, 'host': self.gid, 'size': 1}], + 'uidmap': [{'container': 0, 'host': os.geteuid(), 'size': 1}], + 'gidmap': [{'container': 0, 'host': os.getegid(), 'size': 1}], }, ) obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match uidmap') - self.assertEqual(obj['GID'], 0, 'gid match gidmap') + assert obj['UID'] == 0, 'uid match uidmap' + assert obj['GID'] == 0, 'gid match gidmap' - def test_isolation_priv_user(self): - if not self.is_su: - print('unprivileged tests, skip this') - raise unittest.SkipTest() + def test_isolation_priv_user(self, is_su): + if not is_su: + pytest.skip('unprivileged tests, skip this') self.load('ns_inspect') @@ -122,16 +115,16 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'uid match') - self.assertEqual(obj['GID'], nogroup_gid, 'gid match') + assert obj['UID'] == nobody_uid, 'uid match' + assert obj['GID'] == nogroup_gid, 'gid match' self.load('ns_inspect', isolation={'namespaces': {'credential': True}}) obj = self.getjson()['body'] # privileged unit map app creds in the container by default - self.assertEqual(obj['UID'], nobody_uid, 'uid nobody') - self.assertEqual(obj['GID'], nogroup_gid, 'gid nobody') + assert obj['UID'] == nobody_uid, 'uid nobody' + assert obj['GID'] == nogroup_gid, 'gid nobody' self.load( 'ns_inspect', @@ -141,8 +134,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid nobody user=root') - self.assertEqual(obj['GID'], 0, 'gid nobody user=root') + assert obj['UID'] == 0, 'uid nobody user=root' + assert obj['GID'] == 0, 'gid nobody user=root' self.load( 'ns_inspect', @@ -153,10 +146,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match user=root group=nogroup') - self.assertEqual( - obj['GID'], nogroup_gid, 'gid match user=root group=nogroup' - ) + assert obj['UID'] == 0, 'uid match user=root group=nogroup' + assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup' self.load( 'ns_inspect', @@ -171,8 +162,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match uidmap user=root') - self.assertEqual(obj['GID'], 0, 'gid match gidmap user=root') + assert obj['UID'] == 0, 'uid match uidmap user=root' + assert obj['GID'] == 0, 'gid match gidmap user=root' # map 65535 uids self.load( @@ -188,21 +179,15 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual( - obj['UID'], nobody_uid, 'uid match uidmap user=nobody' - ) - self.assertEqual( - obj['GID'], nogroup_gid, 'gid match uidmap user=nobody' - ) + assert obj['UID'] == nobody_uid, 'uid match uidmap user=nobody' + assert obj['GID'] == nogroup_gid, 'gid match uidmap user=nobody' def test_isolation_mnt(self): if not self.isolation_key('mnt'): - print('mnt namespace is not supported') - raise unittest.SkipTest() + pytest.skip('mnt namespace is not supported') if not self.isolation_key('unprivileged_userns_clone'): - print('unprivileged clone is not available') - raise unittest.SkipTest() + pytest.skip('unprivileged clone is not available') self.load( 'ns_inspect', @@ -218,27 +203,20 @@ class TestGoIsolation(TestApplicationGo): for ns in allns: if ns.upper() in obj['NS']: - self.assertEqual( - obj['NS'][ns.upper()], - self.available['features']['isolation'][ns], - '%s match' % ns, - ) - - self.assertNotEqual( - obj['NS']['MNT'], self.isolation.getns('mnt'), 'mnt set' - ) - self.assertNotEqual( - obj['NS']['USER'], self.isolation.getns('user'), 'user set' - ) + assert ( + obj['NS'][ns.upper()] + == self.available['features']['isolation'][ns] + ), ('%s match' % ns) + + assert obj['NS']['MNT'] != self.isolation.getns('mnt'), 'mnt set' + assert obj['NS']['USER'] != self.isolation.getns('user'), 'user set' - def test_isolation_pid(self): + def test_isolation_pid(self, is_su): if not self.isolation_key('pid'): - print('pid namespace is not supported') - raise unittest.SkipTest() + pytest.skip('pid namespace is not supported') - if not (self.is_su or self.isolation_key('unprivileged_userns_clone')): - print('requires root or unprivileged_userns_clone') - raise unittest.SkipTest() + if not (is_su or self.isolation_key('unprivileged_userns_clone')): + pytest.skip('requires root or unprivileged_userns_clone') self.load( 'ns_inspect', @@ -247,7 +225,7 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['PID'], 1, 'pid of container is 1') + assert obj['PID'] == 1, 'pid of container is 1' def test_isolation_namespace_false(self): self.load('ns_inspect') @@ -275,78 +253,67 @@ class TestGoIsolation(TestApplicationGo): for ns in allns: if ns.upper() in obj['NS']: - self.assertEqual( - obj['NS'][ns.upper()], - self.available['features']['isolation'][ns], - '%s match' % ns, - ) + assert ( + obj['NS'][ns.upper()] + == self.available['features']['isolation'][ns] + ), ('%s match' % ns) def test_go_isolation_rootfs_container(self): if not self.isolation_key('unprivileged_userns_clone'): - print('unprivileged clone is not available') - raise unittest.SkipTest() + pytest.skip('unprivileged clone is not available') if not self.isolation_key('mnt'): - print('mnt namespace is not supported') - raise unittest.SkipTest() + pytest.skip('mnt namespace is not supported') isolation = { 'namespaces': {'mount': True, 'credential': True}, - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?file=/go/app')['body'] - self.assertEqual(obj['FileExists'], True, 'app relative to rootfs') + assert obj['FileExists'] == True, 'app relative to rootfs' obj = self.getjson(url='/?file=/bin/sh')['body'] - self.assertEqual(obj['FileExists'], False, 'file should not exists') + assert obj['FileExists'] == False, 'file should not exists' - def test_go_isolation_rootfs_container_priv(self): - if not self.is_su: - print("requires root") - raise unittest.SkipTest() + def test_go_isolation_rootfs_container_priv(self, is_su): + if not is_su: + pytest.skip('requires root') if not self.isolation_key('mnt'): - print('mnt namespace is not supported') - raise unittest.SkipTest() + pytest.skip('mnt namespace is not supported') isolation = { 'namespaces': {'mount': True}, - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?file=/go/app')['body'] - self.assertEqual(obj['FileExists'], True, 'app relative to rootfs') + assert obj['FileExists'] == True, 'app relative to rootfs' obj = self.getjson(url='/?file=/bin/sh')['body'] - self.assertEqual(obj['FileExists'], False, 'file should not exists') + assert obj['FileExists'] == False, 'file should not exists' def test_go_isolation_rootfs_default_tmpfs(self): if not self.isolation_key('unprivileged_userns_clone'): - print('unprivileged clone is not available') - raise unittest.SkipTest() + pytest.skip('unprivileged clone is not available') if not self.isolation_key('mnt'): - print('mnt namespace is not supported') - raise unittest.SkipTest() + pytest.skip('mnt namespace is not supported') isolation = { 'namespaces': {'mount': True, 'credential': True}, - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?file=/tmp')['body'] - self.assertEqual(obj['FileExists'], True, 'app has /tmp') - - -if __name__ == '__main__': - TestGoIsolation.main() + assert obj['FileExists'] == True, 'app has /tmp' diff --git a/test/test_go_isolation_rootfs.py b/test/test_go_isolation_rootfs.py index 0039ff87..68891cd6 100644 --- a/test/test_go_isolation_rootfs.py +++ b/test/test_go_isolation_rootfs.py @@ -1,5 +1,5 @@ import os -import unittest +import pytest from unit.applications.lang.go import TestApplicationGo @@ -7,28 +7,22 @@ from unit.applications.lang.go import TestApplicationGo class TestGoIsolationRootfs(TestApplicationGo): prerequisites = {'modules': {'go': 'all'}} - def test_go_isolation_rootfs_chroot(self): - if not self.is_su: - print("requires root") - raise unittest.SkipTest() + def test_go_isolation_rootfs_chroot(self, is_su): + if not is_su: + pytest.skip('requires root') if os.uname().sysname == 'Darwin': - print('chroot tests not supported on OSX') - raise unittest.SkipTest() + pytest.skip('chroot tests not supported on OSX') isolation = { - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?file=/go/app')['body'] - self.assertEqual(obj['FileExists'], True, 'app relative to rootfs') + assert obj['FileExists'] == True, 'app relative to rootfs' obj = self.getjson(url='/?file=/bin/sh')['body'] - self.assertEqual(obj['FileExists'], False, 'file should not exists') - - -if __name__ == '__main__': - TestGoIsolationRootfs.main() + assert obj['FileExists'] == False, 'file should not exists' diff --git a/test/test_http_header.py b/test/test_http_header.py index ea4520c1..8c1e211b 100644 --- a/test/test_http_header.py +++ b/test/test_http_header.py @@ -1,4 +1,4 @@ -import unittest +import pytest from unit.applications.lang.python import TestApplicationPython @@ -17,12 +17,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value leading sp status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value leading sp custom header', - ) + assert resp['status'] == 200, 'value leading sp status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value leading sp custom header' def test_http_header_value_leading_htab(self): self.load('custom_header') @@ -35,12 +33,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value leading htab status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value leading htab custom header', - ) + assert resp['status'] == 200, 'value leading htab status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value leading htab custom header' def test_http_header_value_trailing_sp(self): self.load('custom_header') @@ -53,12 +49,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value trailing sp status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value trailing sp custom header', - ) + assert resp['status'] == 200, 'value trailing sp status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value trailing sp custom header' def test_http_header_value_trailing_htab(self): self.load('custom_header') @@ -71,12 +65,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value trailing htab status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value trailing htab custom header', - ) + assert resp['status'] == 200, 'value trailing htab status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value trailing htab custom header' def test_http_header_value_both_sp(self): self.load('custom_header') @@ -89,12 +81,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value both sp status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value both sp custom header', - ) + assert resp['status'] == 200, 'value both sp status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value both sp custom header' def test_http_header_value_both_htab(self): self.load('custom_header') @@ -107,12 +97,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value both htab status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value both htab custom header', - ) + assert resp['status'] == 200, 'value both htab status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value both htab custom header' def test_http_header_value_chars(self): self.load('custom_header') @@ -125,12 +113,11 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value chars status') - self.assertEqual( - resp['headers']['Custom-Header'], - '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~', - 'value chars custom header', - ) + assert resp['status'] == 200, 'value chars status' + assert ( + resp['headers']['Custom-Header'] + == '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' + ), 'value chars custom header' def test_http_header_value_chars_edge(self): self.load('custom_header') @@ -146,10 +133,8 @@ Connection: close encoding='latin1', ) - self.assertEqual(resp['status'], 200, 'value chars edge status') - self.assertEqual( - resp['headers']['Custom-Header'], '\xFF', 'value chars edge' - ) + assert resp['status'] == 200, 'value chars edge status' + assert resp['headers']['Custom-Header'] == '\xFF', 'value chars edge' def test_http_header_value_chars_below(self): self.load('custom_header') @@ -164,7 +149,7 @@ Connection: close raw=True, ) - self.assertEqual(resp['status'], 400, 'value chars below') + assert resp['status'] == 400, 'value chars below' def test_http_header_field_leading_sp(self): self.load('empty') @@ -177,7 +162,7 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field leading sp') + assert resp['status'] == 400, 'field leading sp' def test_http_header_field_leading_htab(self): self.load('empty') @@ -190,7 +175,7 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field leading htab') + assert resp['status'] == 400, 'field leading htab' def test_http_header_field_trailing_sp(self): self.load('empty') @@ -203,7 +188,7 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field trailing sp') + assert resp['status'] == 400, 'field trailing sp' def test_http_header_field_trailing_htab(self): self.load('empty') @@ -216,12 +201,12 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field trailing htab') + assert resp['status'] == 400, 'field trailing htab' def test_http_header_content_length_big(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -229,15 +214,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length big', - ) + )['status'] + == 400 + ), 'Content-Length big' def test_http_header_content_length_negative(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -245,15 +229,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length negative', - ) + )['status'] + == 400 + ), 'Content-Length negative' def test_http_header_content_length_text(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -261,15 +244,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length text', - ) + )['status'] + == 400 + ), 'Content-Length text' def test_http_header_content_length_multiple_values(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -277,15 +259,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length multiple value', - ) + )['status'] + == 400 + ), 'Content-Length multiple value' def test_http_header_content_length_multiple_fields(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -293,39 +274,35 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length multiple fields', - ) + )['status'] + == 400 + ), 'Content-Length multiple fields' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_http_header_host_absent(self): self.load('host') resp = self.get(headers={'Connection': 'close'}) - self.assertEqual(resp['status'], 400, 'Host absent status') + assert resp['status'] == 400, 'Host absent status' def test_http_header_host_empty(self): self.load('host') resp = self.get(headers={'Host': '', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host empty status') - self.assertNotEqual( - resp['headers']['X-Server-Name'], '', 'Host empty SERVER_NAME' - ) + assert resp['status'] == 200, 'Host empty status' + assert resp['headers']['X-Server-Name'] != '', 'Host empty SERVER_NAME' def test_http_header_host_big(self): self.load('empty') - self.assertEqual( + assert ( self.get(headers={'Host': 'X' * 10000, 'Connection': 'close'})[ 'status' - ], - 431, - 'Host big', - ) + ] + == 431 + ), 'Host big' def test_http_header_host_port(self): self.load('host') @@ -334,17 +311,13 @@ Connection: close headers={'Host': 'exmaple.com:7080', 'Connection': 'close'} ) - self.assertEqual(resp['status'], 200, 'Host port status') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'exmaple.com', - 'Host port SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - 'exmaple.com:7080', - 'Host port HTTP_HOST', - ) + assert resp['status'] == 200, 'Host port status' + assert ( + resp['headers']['X-Server-Name'] == 'exmaple.com' + ), 'Host port SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == 'exmaple.com:7080' + ), 'Host port HTTP_HOST' def test_http_header_host_port_empty(self): self.load('host') @@ -353,63 +326,49 @@ Connection: close headers={'Host': 'exmaple.com:', 'Connection': 'close'} ) - self.assertEqual(resp['status'], 200, 'Host port empty status') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'exmaple.com', - 'Host port empty SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - 'exmaple.com:', - 'Host port empty HTTP_HOST', - ) + assert resp['status'] == 200, 'Host port empty status' + assert ( + resp['headers']['X-Server-Name'] == 'exmaple.com' + ), 'Host port empty SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == 'exmaple.com:' + ), 'Host port empty HTTP_HOST' def test_http_header_host_literal(self): self.load('host') resp = self.get(headers={'Host': '127.0.0.1', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host literal status') - self.assertEqual( - resp['headers']['X-Server-Name'], - '127.0.0.1', - 'Host literal SERVER_NAME', - ) + assert resp['status'] == 200, 'Host literal status' + assert ( + resp['headers']['X-Server-Name'] == '127.0.0.1' + ), 'Host literal SERVER_NAME' def test_http_header_host_literal_ipv6(self): self.load('host') resp = self.get(headers={'Host': '[::1]:7080', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host literal ipv6 status') - self.assertEqual( - resp['headers']['X-Server-Name'], - '[::1]', - 'Host literal ipv6 SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - '[::1]:7080', - 'Host literal ipv6 HTTP_HOST', - ) + assert resp['status'] == 200, 'Host literal ipv6 status' + assert ( + resp['headers']['X-Server-Name'] == '[::1]' + ), 'Host literal ipv6 SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == '[::1]:7080' + ), 'Host literal ipv6 HTTP_HOST' def test_http_header_host_trailing_period(self): self.load('host') resp = self.get(headers={'Host': '127.0.0.1.', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host trailing period status') - self.assertEqual( - resp['headers']['X-Server-Name'], - '127.0.0.1', - 'Host trailing period SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - '127.0.0.1.', - 'Host trailing period HTTP_HOST', - ) + assert resp['status'] == 200, 'Host trailing period status' + assert ( + resp['headers']['X-Server-Name'] == '127.0.0.1' + ), 'Host trailing period SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == '127.0.0.1.' + ), 'Host trailing period HTTP_HOST' def test_http_header_host_trailing_period_2(self): self.load('host') @@ -418,66 +377,53 @@ Connection: close headers={'Host': 'EXAMPLE.COM.', 'Connection': 'close'} ) - self.assertEqual(resp['status'], 200, 'Host trailing period 2 status') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'example.com', - 'Host trailing period 2 SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - 'EXAMPLE.COM.', - 'Host trailing period 2 HTTP_HOST', - ) + assert resp['status'] == 200, 'Host trailing period 2 status' + assert ( + resp['headers']['X-Server-Name'] == 'example.com' + ), 'Host trailing period 2 SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == 'EXAMPLE.COM.' + ), 'Host trailing period 2 HTTP_HOST' def test_http_header_host_case_insensitive(self): self.load('host') resp = self.get(headers={'Host': 'EXAMPLE.COM', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host case insensitive') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'example.com', - 'Host case insensitive SERVER_NAME', - ) + assert resp['status'] == 200, 'Host case insensitive' + assert ( + resp['headers']['X-Server-Name'] == 'example.com' + ), 'Host case insensitive SERVER_NAME' def test_http_header_host_double_dot(self): self.load('empty') - self.assertEqual( + assert ( self.get(headers={'Host': '127.0.0..1', 'Connection': 'close'})[ 'status' - ], - 400, - 'Host double dot', - ) + ] + == 400 + ), 'Host double dot' def test_http_header_host_slash(self): self.load('empty') - self.assertEqual( + assert ( self.get(headers={'Host': '/localhost', 'Connection': 'close'})[ 'status' - ], - 400, - 'Host slash', - ) + ] + == 400 + ), 'Host slash' def test_http_header_host_multiple_fields(self): self.load('empty') - self.assertEqual( + assert ( self.get( headers={ 'Host': ['localhost', 'example.com'], 'Connection': 'close', } - )['status'], - 400, - 'Host multiple fields', - ) - - -if __name__ == '__main__': - TestHTTPHeader.main() + )['status'] + == 400 + ), 'Host multiple fields' diff --git a/test/test_java_application.py b/test/test_java_application.py index 0cb18c25..a2bd3d44 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -1,54 +1,44 @@ import io import os +import re import time from unit.applications.lang.java import TestApplicationJava - +from conftest import option, public_dir, skip_alert class TestJavaApplication(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} def test_java_conf_error(self): - self.skip_alerts.extend( - [ - r'realpath.*failed', - r'failed to apply new conf', - r'application setup failed', - ] - ) - self.assertIn( - 'error', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "java", - "processes": 1, - "working_directory": self.current_dir - + "/java/empty", - "webapp": self.testdir + "/java", - "unit_jars": self.testdir + "/no_such_dir", - } - }, - } - ), - 'conf error', - ) + skip_alert( + r'realpath.*failed', + r'failed to apply new conf', + r'application setup failed', + ) + assert 'error' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "java", + "processes": 1, + "working_directory": option.test_dir + "/java/empty", + "webapp": self.temp_dir + "/java", + "unit_jars": self.temp_dir + "/no_such_dir", + } + }, + } + ), 'conf error' def test_java_war(self): self.load('empty_war') - self.assertIn( - 'success', - self.conf( - '"' + self.testdir + '/java/empty.war"', - '/config/applications/empty_war/webapp', - ), - 'configure war', - ) + assert 'success' in self.conf( + '"' + self.temp_dir + '/java/empty.war"', + '/config/applications/empty_war/webapp', + ), 'configure war' - self.assertEqual(self.get()['status'], 200, 'war') + assert self.get()['status'] == 200, 'war' def test_java_application_cookies(self): self.load('cookies') @@ -61,22 +51,20 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual(headers['X-Cookie-1'], 'val1', 'cookie 1') - self.assertEqual(headers['X-Cookie-2'], 'val2', 'cookie 2') + assert headers['X-Cookie-1'] == 'val1', 'cookie 1' + assert headers['X-Cookie-2'] == 'val2', 'cookie 2' def test_java_application_filter(self): self.load('filter') headers = self.get()['headers'] - self.assertEqual(headers['X-Filter-Before'], '1', 'filter before') - self.assertEqual(headers['X-Filter-After'], '1', 'filter after') + assert headers['X-Filter-Before'] == '1', 'filter before' + assert headers['X-Filter-After'] == '1', 'filter after' - self.assertEqual( - self.get(url='/test')['headers']['X-Filter-After'], - '0', - 'filter after 2', - ) + assert ( + self.get(url='/test')['headers']['X-Filter-After'] == '0' + ), 'filter after 2' def test_java_application_get_variables(self): self.load('get_params') @@ -85,21 +73,17 @@ class TestJavaApplication(TestApplicationJava): 'headers' ] - self.assertEqual(headers['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(headers['X-Var-2'], 'true', 'GET variables 2') - self.assertEqual(headers['X-Var-3'], 'false', 'GET variables 3') + assert headers['X-Var-1'] == 'val1', 'GET variables' + assert headers['X-Var-2'] == 'true', 'GET variables 2' + assert headers['X-Var-3'] == 'false', 'GET variables 3' - self.assertEqual( - headers['X-Param-Names'], 'var4 var2 var1 ', 'getParameterNames' - ) - self.assertEqual( - headers['X-Param-Values'], 'val4 foo ', 'getParameterValues' - ) - self.assertEqual( - headers['X-Param-Map'], - 'var2= var1=val1 var4=val4,foo ', - 'getParameterMap', - ) + assert ( + headers['X-Param-Names'] == 'var4 var2 var1 ' + ), 'getParameterNames' + assert headers['X-Param-Values'] == 'val4 foo ', 'getParameterValues' + assert ( + headers['X-Param-Map'] == 'var2= var1=val1 var4=val4,foo ' + ), 'getParameterMap' def test_java_application_post_variables(self): self.load('post_params') @@ -113,9 +97,9 @@ class TestJavaApplication(TestApplicationJava): body='var1=val1&var2=', )['headers'] - self.assertEqual(headers['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(headers['X-Var-2'], 'true', 'POST variables 2') - self.assertEqual(headers['X-Var-3'], 'false', 'POST variables 3') + assert headers['X-Var-1'] == 'val1', 'POST variables' + assert headers['X-Var-2'] == 'true', 'POST variables 2' + assert headers['X-Var-3'] == 'false', 'POST variables 3' def test_java_application_session(self): self.load('session') @@ -123,8 +107,8 @@ class TestJavaApplication(TestApplicationJava): headers = self.get(url='/?var1=val1')['headers'] session_id = headers['X-Session-Id'] - self.assertEqual(headers['X-Var-1'], 'null', 'variable empty') - self.assertEqual(headers['X-Session-New'], 'true', 'session create') + assert headers['X-Var-1'] == 'null', 'variable empty' + assert headers['X-Session-New'] == 'true', 'session create' headers = self.get( headers={ @@ -135,36 +119,33 @@ class TestJavaApplication(TestApplicationJava): url='/?var1=val2', )['headers'] - self.assertEqual(headers['X-Var-1'], 'val1', 'variable') - self.assertEqual(headers['X-Session-New'], 'false', 'session resume') - self.assertEqual( - session_id, headers['X-Session-Id'], 'session same id' - ) + assert headers['X-Var-1'] == 'val1', 'variable' + assert headers['X-Session-New'] == 'false', 'session resume' + assert session_id == headers['X-Session-Id'], 'session same id' def test_java_application_session_active(self): self.load('session_inactive') - resp = self.get(headers={ - 'X-Interval': '4', - 'Host': 'localhost', - 'Connection': 'close', - }) + resp = self.get( + headers={ + 'X-Interval': '4', + 'Host': 'localhost', + 'Connection': 'close', + } + ) session_id = resp['headers']['X-Session-Id'] - self.assertEqual(resp['status'], 200, 'session init') - self.assertEqual( - resp['headers']['X-Session-Interval'], '4', 'session interval' - ) - self.assertLess( + assert resp['status'] == 200, 'session init' + assert resp['headers']['X-Session-Interval'] == '4', 'session interval' + assert ( abs( self.date_to_sec_epoch( resp['headers']['X-Session-Last-Access-Time'] ) - self.sec_epoch() - ), - 5, - 'session last access time', - ) + ) + < 5 + ), 'session last access time' time.sleep(1) @@ -176,9 +157,7 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertEqual( - resp['headers']['X-Session-Id'], session_id, 'session active' - ) + assert resp['headers']['X-Session-Id'] == session_id, 'session active' session_id = resp['headers']['X-Session-Id'] @@ -192,9 +171,9 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertEqual( - resp['headers']['X-Session-Id'], session_id, 'session active 2' - ) + assert ( + resp['headers']['X-Session-Id'] == session_id + ), 'session active 2' time.sleep(2) @@ -206,18 +185,20 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertEqual( - resp['headers']['X-Session-Id'], session_id, 'session active 3' - ) + assert ( + resp['headers']['X-Session-Id'] == session_id + ), 'session active 3' def test_java_application_session_inactive(self): self.load('session_inactive') - resp = self.get(headers={ - 'X-Interval': '1', - 'Host': 'localhost', - 'Connection': 'close', - }) + resp = self.get( + headers={ + 'X-Interval': '1', + 'Host': 'localhost', + 'Connection': 'close', + } + ) session_id = resp['headers']['X-Session-Id'] time.sleep(3) @@ -230,9 +211,9 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertNotEqual( - resp['headers']['X-Session-Id'], session_id, 'session inactive' - ) + assert ( + resp['headers']['X-Session-Id'] != session_id + ), 'session inactive' def test_java_application_session_invalidate(self): self.load('session_invalidate') @@ -248,9 +229,9 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertNotEqual( - resp['headers']['X-Session-Id'], session_id, 'session invalidate' - ) + assert ( + resp['headers']['X-Session-Id'] != session_id + ), 'session invalidate' def test_java_application_session_listeners(self): self.load('session_listeners') @@ -258,10 +239,8 @@ class TestJavaApplication(TestApplicationJava): headers = self.get(url='/test?var1=val1')['headers'] session_id = headers['X-Session-Id'] - self.assertEqual( - headers['X-Session-Created'], session_id, 'session create' - ) - self.assertEqual(headers['X-Attr-Added'], 'var1=val1', 'attribute add') + assert headers['X-Session-Created'] == session_id, 'session create' + assert headers['X-Attr-Added'] == 'var1=val1', 'attribute add' headers = self.get( headers={ @@ -272,12 +251,8 @@ class TestJavaApplication(TestApplicationJava): url='/?var1=val2', )['headers'] - self.assertEqual( - session_id, headers['X-Session-Id'], 'session same id' - ) - self.assertEqual( - headers['X-Attr-Replaced'], 'var1=val1', 'attribute replace' - ) + assert session_id == headers['X-Session-Id'], 'session same id' + assert headers['X-Attr-Replaced'] == 'var1=val1', 'attribute replace' headers = self.get( headers={ @@ -288,289 +263,219 @@ class TestJavaApplication(TestApplicationJava): url='/', )['headers'] - self.assertEqual( - session_id, headers['X-Session-Id'], 'session same id' - ) - self.assertEqual( - headers['X-Attr-Removed'], 'var1=val2', 'attribute remove' - ) + assert session_id == headers['X-Session-Id'], 'session same id' + assert headers['X-Attr-Removed'] == 'var1=val2', 'attribute remove' def test_java_application_jsp(self): self.load('jsp') headers = self.get(url='/index.jsp')['headers'] - self.assertEqual(headers['X-Unit-JSP'], 'ok', 'JSP Ok header') + assert headers['X-Unit-JSP'] == 'ok', 'JSP Ok header' def test_java_application_url_pattern(self): self.load('url_pattern') headers = self.get(url='/foo/bar/index.html')['headers'] - self.assertEqual(headers['X-Id'], 'servlet1', '#1 Servlet1 request') - self.assertEqual( - headers['X-Request-URI'], '/foo/bar/index.html', '#1 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/foo/bar', '#1 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], '/index.html', '#1 path info') + assert headers['X-Id'] == 'servlet1', '#1 Servlet1 request' + assert ( + headers['X-Request-URI'] == '/foo/bar/index.html' + ), '#1 request URI' + assert headers['X-Servlet-Path'] == '/foo/bar', '#1 servlet path' + assert headers['X-Path-Info'] == '/index.html', '#1 path info' headers = self.get(url='/foo/bar/index.bop')['headers'] - self.assertEqual(headers['X-Id'], 'servlet1', '#2 Servlet1 request') - self.assertEqual( - headers['X-Request-URI'], '/foo/bar/index.bop', '#2 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/foo/bar', '#2 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], '/index.bop', '#2 path info') + assert headers['X-Id'] == 'servlet1', '#2 Servlet1 request' + assert ( + headers['X-Request-URI'] == '/foo/bar/index.bop' + ), '#2 request URI' + assert headers['X-Servlet-Path'] == '/foo/bar', '#2 servlet path' + assert headers['X-Path-Info'] == '/index.bop', '#2 path info' headers = self.get(url='/baz')['headers'] - self.assertEqual(headers['X-Id'], 'servlet2', '#3 Servlet2 request') - self.assertEqual(headers['X-Request-URI'], '/baz', '#3 request URI') - self.assertEqual(headers['X-Servlet-Path'], '/baz', '#3 servlet path') - self.assertEqual(headers['X-Path-Info'], 'null', '#3 path info') + assert headers['X-Id'] == 'servlet2', '#3 Servlet2 request' + assert headers['X-Request-URI'] == '/baz', '#3 request URI' + assert headers['X-Servlet-Path'] == '/baz', '#3 servlet path' + assert headers['X-Path-Info'] == 'null', '#3 path info' headers = self.get(url='/baz/index.html')['headers'] - self.assertEqual(headers['X-Id'], 'servlet2', '#4 Servlet2 request') - self.assertEqual( - headers['X-Request-URI'], '/baz/index.html', '#4 request URI' - ) - self.assertEqual(headers['X-Servlet-Path'], '/baz', '#4 servlet path') - self.assertEqual(headers['X-Path-Info'], '/index.html', '#4 path info') + assert headers['X-Id'] == 'servlet2', '#4 Servlet2 request' + assert headers['X-Request-URI'] == '/baz/index.html', '#4 request URI' + assert headers['X-Servlet-Path'] == '/baz', '#4 servlet path' + assert headers['X-Path-Info'] == '/index.html', '#4 path info' headers = self.get(url='/catalog')['headers'] - self.assertEqual(headers['X-Id'], 'servlet3', '#5 Servlet3 request') - self.assertEqual( - headers['X-Request-URI'], '/catalog', '#5 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/catalog', '#5 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#5 path info') + assert headers['X-Id'] == 'servlet3', '#5 Servlet3 request' + assert headers['X-Request-URI'] == '/catalog', '#5 request URI' + assert headers['X-Servlet-Path'] == '/catalog', '#5 servlet path' + assert headers['X-Path-Info'] == 'null', '#5 path info' headers = self.get(url='/catalog/index.html')['headers'] - self.assertEqual(headers['X-Id'], 'default', '#6 default request') - self.assertEqual( - headers['X-Request-URI'], '/catalog/index.html', '#6 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/catalog/index.html', '#6 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#6 path info') + assert headers['X-Id'] == 'default', '#6 default request' + assert ( + headers['X-Request-URI'] == '/catalog/index.html' + ), '#6 request URI' + assert ( + headers['X-Servlet-Path'] == '/catalog/index.html' + ), '#6 servlet path' + assert headers['X-Path-Info'] == 'null', '#6 path info' headers = self.get(url='/catalog/racecar.bop')['headers'] - self.assertEqual(headers['X-Id'], 'servlet4', '#7 servlet4 request') - self.assertEqual( - headers['X-Request-URI'], '/catalog/racecar.bop', '#7 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], - '/catalog/racecar.bop', - '#7 servlet path', - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#7 path info') + assert headers['X-Id'] == 'servlet4', '#7 servlet4 request' + assert ( + headers['X-Request-URI'] == '/catalog/racecar.bop' + ), '#7 request URI' + assert ( + headers['X-Servlet-Path'] == '/catalog/racecar.bop' + ), '#7 servlet path' + assert headers['X-Path-Info'] == 'null', '#7 path info' headers = self.get(url='/index.bop')['headers'] - self.assertEqual(headers['X-Id'], 'servlet4', '#8 servlet4 request') - self.assertEqual( - headers['X-Request-URI'], '/index.bop', '#8 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/index.bop', '#8 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#8 path info') + assert headers['X-Id'] == 'servlet4', '#8 servlet4 request' + assert headers['X-Request-URI'] == '/index.bop', '#8 request URI' + assert headers['X-Servlet-Path'] == '/index.bop', '#8 servlet path' + assert headers['X-Path-Info'] == 'null', '#8 path info' headers = self.get(url='/foo/baz')['headers'] - self.assertEqual(headers['X-Id'], 'servlet0', '#9 servlet0 request') - self.assertEqual( - headers['X-Request-URI'], '/foo/baz', '#9 request URI' - ) - self.assertEqual(headers['X-Servlet-Path'], '/foo', '#9 servlet path') - self.assertEqual(headers['X-Path-Info'], '/baz', '#9 path info') + assert headers['X-Id'] == 'servlet0', '#9 servlet0 request' + assert headers['X-Request-URI'] == '/foo/baz', '#9 request URI' + assert headers['X-Servlet-Path'] == '/foo', '#9 servlet path' + assert headers['X-Path-Info'] == '/baz', '#9 path info' headers = self.get()['headers'] - self.assertEqual(headers['X-Id'], 'default', '#10 default request') - self.assertEqual(headers['X-Request-URI'], '/', '#10 request URI') - self.assertEqual(headers['X-Servlet-Path'], '/', '#10 servlet path') - self.assertEqual(headers['X-Path-Info'], 'null', '#10 path info') + assert headers['X-Id'] == 'default', '#10 default request' + assert headers['X-Request-URI'] == '/', '#10 request URI' + assert headers['X-Servlet-Path'] == '/', '#10 servlet path' + assert headers['X-Path-Info'] == 'null', '#10 path info' headers = self.get(url='/index.bop/')['headers'] - self.assertEqual(headers['X-Id'], 'default', '#11 default request') - self.assertEqual( - headers['X-Request-URI'], '/index.bop/', '#11 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/index.bop/', '#11 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#11 path info') + assert headers['X-Id'] == 'default', '#11 default request' + assert headers['X-Request-URI'] == '/index.bop/', '#11 request URI' + assert headers['X-Servlet-Path'] == '/index.bop/', '#11 servlet path' + assert headers['X-Path-Info'] == 'null', '#11 path info' def test_java_application_header(self): self.load('header') headers = self.get()['headers'] - self.assertEqual( - headers['X-Set-Utf8-Value'], '????', 'set Utf8 header value' - ) - self.assertEqual( - headers['X-Set-Utf8-Name-???'], 'x', 'set Utf8 header name' - ) - self.assertEqual( - headers['X-Add-Utf8-Value'], '????', 'add Utf8 header value' - ) - self.assertEqual( - headers['X-Add-Utf8-Name-???'], 'y', 'add Utf8 header name' - ) - self.assertEqual(headers['X-Add-Test'], 'v1', 'add null header') - self.assertEqual('X-Set-Test1' in headers, False, 'set null header') - self.assertEqual(headers['X-Set-Test2'], '', 'set empty header') + assert headers['X-Set-Utf8-Value'] == '????', 'set Utf8 header value' + assert headers['X-Set-Utf8-Name-???'] == 'x', 'set Utf8 header name' + assert headers['X-Add-Utf8-Value'] == '????', 'add Utf8 header value' + assert headers['X-Add-Utf8-Name-???'] == 'y', 'add Utf8 header name' + assert headers['X-Add-Test'] == 'v1', 'add null header' + assert ('X-Set-Test1' in headers) == False, 'set null header' + assert headers['X-Set-Test2'] == '', 'set empty header' def test_java_application_content_type(self): self.load('content_type') headers = self.get(url='/1')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=utf-8', - '#1 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=utf-8', - '#1 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#1 response charset' - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=utf-8' + ), '#1 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=utf-8' + ), '#1 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#1 response charset' headers = self.get(url='/2')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=iso-8859-1', - '#2 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=iso-8859-1', - '#2 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'iso-8859-1', - '#2 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#2 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#2 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'iso-8859-1' + ), '#2 response charset' headers = self.get(url='/3')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=windows-1251', - '#3 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=windows-1251', - '#3 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'windows-1251', - '#3 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=windows-1251' + ), '#3 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=windows-1251' + ), '#3 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'windows-1251' + ), '#3 response charset' headers = self.get(url='/4')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=windows-1251', - '#4 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=windows-1251', - '#4 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'windows-1251', - '#4 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=windows-1251' + ), '#4 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=windows-1251' + ), '#4 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'windows-1251' + ), '#4 response charset' headers = self.get(url='/5')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=iso-8859-1', - '#5 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=iso-8859-1', - '#5 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'iso-8859-1', - '#5 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#5 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#5 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'iso-8859-1' + ), '#5 response charset' headers = self.get(url='/6')['headers'] - self.assertEqual( - 'Content-Type' in headers, False, '#6 no Content-Type header' - ) - self.assertEqual( - 'X-Content-Type' in headers, False, '#6 no response Content-Type' - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#6 response charset' - ) + assert ( + 'Content-Type' in headers + ) == False, '#6 no Content-Type header' + assert ( + 'X-Content-Type' in headers + ) == False, '#6 no response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#6 response charset' headers = self.get(url='/7')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=utf-8', - '#7 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=utf-8', - '#7 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#7 response charset' - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=utf-8' + ), '#7 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=utf-8' + ), '#7 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#7 response charset' headers = self.get(url='/8')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/html;charset=utf-8', - '#8 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/html;charset=utf-8', - '#8 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#8 response charset' - ) + assert ( + headers['Content-Type'] == 'text/html;charset=utf-8' + ), '#8 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/html;charset=utf-8' + ), '#8 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#8 response charset' def test_java_application_welcome_files(self): self.load('welcome_files') @@ -579,126 +484,90 @@ class TestJavaApplication(TestApplicationJava): resp = self.get(url='/dir1') - self.assertEqual(resp['status'], 302, 'dir redirect expected') + assert resp['status'] == 302, 'dir redirect expected' resp = self.get(url='/dir1/') - self.assertEqual( - 'This is index.txt.' in resp['body'], True, 'dir1 index body' - ) - self.assertEqual( - resp['headers']['X-TXT-Filter'], '1', 'TXT Filter header' - ) + assert ( + 'This is index.txt.' in resp['body'] + ) == True, 'dir1 index body' + assert resp['headers']['X-TXT-Filter'] == '1', 'TXT Filter header' headers = self.get(url='/dir2/')['headers'] - self.assertEqual(headers['X-Unit-JSP'], 'ok', 'JSP Ok header') - self.assertEqual(headers['X-JSP-Filter'], '1', 'JSP Filter header') + assert headers['X-Unit-JSP'] == 'ok', 'JSP Ok header' + assert headers['X-JSP-Filter'] == '1', 'JSP Filter header' headers = self.get(url='/dir3/')['headers'] - self.assertEqual( - headers['X-App-Servlet'], '1', 'URL pattern overrides welcome file' - ) + assert ( + headers['X-App-Servlet'] == '1' + ), 'URL pattern overrides welcome file' headers = self.get(url='/dir4/')['headers'] - self.assertEqual( - 'X-App-Servlet' in headers, - False, - 'Static welcome file served first', - ) + assert ( + 'X-App-Servlet' in headers + ) == False, 'Static welcome file served first' headers = self.get(url='/dir5/')['headers'] - self.assertEqual( - headers['X-App-Servlet'], - '1', - 'Servlet for welcome file served when no static file found', - ) + assert ( + headers['X-App-Servlet'] == '1' + ), 'Servlet for welcome file served when no static file found' def test_java_application_request_listeners(self): self.load('request_listeners') headers = self.get(url='/test1')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test1', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '', 'request destroyed event' - ) - self.assertEqual(headers['X-Attr-Added'], '', 'attribute added event') - self.assertEqual( - headers['X-Attr-Removed'], '', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], '', 'attribute replaced event' - ) + assert ( + headers['X-Request-Initialized'] == '/test1' + ), 'request initialized event' + assert headers['X-Request-Destroyed'] == '', 'request destroyed event' + assert headers['X-Attr-Added'] == '', 'attribute added event' + assert headers['X-Attr-Removed'] == '', 'attribute removed event' + assert headers['X-Attr-Replaced'] == '', 'attribute replaced event' headers = self.get(url='/test2?var1=1')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test2', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '/test1', 'request destroyed event' - ) - self.assertEqual( - headers['X-Attr-Added'], 'var=1;', 'attribute added event' - ) - self.assertEqual( - headers['X-Attr-Removed'], 'var=1;', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], '', 'attribute replaced event' - ) + assert ( + headers['X-Request-Initialized'] == '/test2' + ), 'request initialized event' + assert ( + headers['X-Request-Destroyed'] == '/test1' + ), 'request destroyed event' + assert headers['X-Attr-Added'] == 'var=1;', 'attribute added event' + assert headers['X-Attr-Removed'] == 'var=1;', 'attribute removed event' + assert headers['X-Attr-Replaced'] == '', 'attribute replaced event' headers = self.get(url='/test3?var1=1&var2=2')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test3', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '/test2', 'request destroyed event' - ) - self.assertEqual( - headers['X-Attr-Added'], 'var=1;', 'attribute added event' - ) - self.assertEqual( - headers['X-Attr-Removed'], 'var=2;', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], 'var=1;', 'attribute replaced event' - ) + assert ( + headers['X-Request-Initialized'] == '/test3' + ), 'request initialized event' + assert ( + headers['X-Request-Destroyed'] == '/test2' + ), 'request destroyed event' + assert headers['X-Attr-Added'] == 'var=1;', 'attribute added event' + assert headers['X-Attr-Removed'] == 'var=2;', 'attribute removed event' + assert ( + headers['X-Attr-Replaced'] == 'var=1;' + ), 'attribute replaced event' headers = self.get(url='/test4?var1=1&var2=2&var3=3')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test4', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '/test3', 'request destroyed event' - ) - self.assertEqual( - headers['X-Attr-Added'], 'var=1;', 'attribute added event' - ) - self.assertEqual( - headers['X-Attr-Removed'], '', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], - 'var=1;var=2;', - 'attribute replaced event', - ) + assert ( + headers['X-Request-Initialized'] == '/test4' + ), 'request initialized event' + assert ( + headers['X-Request-Destroyed'] == '/test3' + ), 'request destroyed event' + assert headers['X-Attr-Added'] == 'var=1;', 'attribute added event' + assert headers['X-Attr-Removed'] == '', 'attribute removed event' + assert ( + headers['X-Attr-Replaced'] == 'var=1;var=2;' + ), 'attribute replaced event' def test_java_application_request_uri_forward(self): self.load('forward') @@ -708,105 +577,67 @@ class TestJavaApplication(TestApplicationJava): ) headers = resp['headers'] - self.assertEqual( - headers['X-REQUEST-Id'], 'fwd', 'initial request servlet mapping' - ) - self.assertEqual( - headers['X-Forward-To'], - '/data/test?uri=new_uri&a=2&b=3', - 'forwarding triggered', - ) - self.assertEqual( - headers['X-REQUEST-Param-uri'], - '/data/test?uri=new_uri&a=2&b=3', - 'original uri parameter', - ) - self.assertEqual( - headers['X-REQUEST-Param-a'], '1', 'original a parameter' - ) - self.assertEqual( - headers['X-REQUEST-Param-c'], '4', 'original c parameter' - ) - - self.assertEqual( - headers['X-FORWARD-Id'], 'data', 'forward request servlet mapping' - ) - self.assertEqual( - headers['X-FORWARD-Request-URI'], - '/data/test', - 'forward request uri', - ) - self.assertEqual( - headers['X-FORWARD-Servlet-Path'], - '/data', - 'forward request servlet path', - ) - self.assertEqual( - headers['X-FORWARD-Path-Info'], - '/test', - 'forward request path info', - ) - self.assertEqual( - headers['X-FORWARD-Query-String'], - 'uri=new_uri&a=2&b=3', - 'forward request query string', - ) - self.assertEqual( - headers['X-FORWARD-Param-uri'], - 'new_uri,/data/test?uri=new_uri&a=2&b=3', - 'forward uri parameter', - ) - self.assertEqual( - headers['X-FORWARD-Param-a'], '2,1', 'forward a parameter' - ) - self.assertEqual( - headers['X-FORWARD-Param-b'], '3', 'forward b parameter' - ) - self.assertEqual( - headers['X-FORWARD-Param-c'], '4', 'forward c parameter' - ) - - self.assertEqual( - headers['X-javax.servlet.forward.request_uri'], - '/fwd', - 'original request uri', - ) - self.assertEqual( - headers['X-javax.servlet.forward.context_path'], - '', - 'original request context path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.servlet_path'], - '/fwd', - 'original request servlet path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.path_info'], - 'null', - 'original request path info', - ) - self.assertEqual( - headers['X-javax.servlet.forward.query_string'], - 'uri=%2Fdata%2Ftest%3Furi%3Dnew_uri%26a%3D2%26b%3D3&a=1&c=4', - 'original request query', - ) - - self.assertEqual( - 'Before forwarding' in resp['body'], - False, - 'discarded data added before forward() call', - ) - self.assertEqual( - 'X-After-Forwarding' in headers, - False, - 'cannot add headers after forward() call', - ) - self.assertEqual( - 'After forwarding' in resp['body'], - False, - 'cannot add data after forward() call', - ) + assert ( + headers['X-REQUEST-Id'] == 'fwd' + ), 'initial request servlet mapping' + assert ( + headers['X-Forward-To'] == '/data/test?uri=new_uri&a=2&b=3' + ), 'forwarding triggered' + assert ( + headers['X-REQUEST-Param-uri'] == '/data/test?uri=new_uri&a=2&b=3' + ), 'original uri parameter' + assert headers['X-REQUEST-Param-a'] == '1', 'original a parameter' + assert headers['X-REQUEST-Param-c'] == '4', 'original c parameter' + + assert ( + headers['X-FORWARD-Id'] == 'data' + ), 'forward request servlet mapping' + assert ( + headers['X-FORWARD-Request-URI'] == '/data/test' + ), 'forward request uri' + assert ( + headers['X-FORWARD-Servlet-Path'] == '/data' + ), 'forward request servlet path' + assert ( + headers['X-FORWARD-Path-Info'] == '/test' + ), 'forward request path info' + assert ( + headers['X-FORWARD-Query-String'] == 'uri=new_uri&a=2&b=3' + ), 'forward request query string' + assert ( + headers['X-FORWARD-Param-uri'] + == 'new_uri,/data/test?uri=new_uri&a=2&b=3' + ), 'forward uri parameter' + assert headers['X-FORWARD-Param-a'] == '2,1', 'forward a parameter' + assert headers['X-FORWARD-Param-b'] == '3', 'forward b parameter' + assert headers['X-FORWARD-Param-c'] == '4', 'forward c parameter' + + assert ( + headers['X-javax.servlet.forward.request_uri'] == '/fwd' + ), 'original request uri' + assert ( + headers['X-javax.servlet.forward.context_path'] == '' + ), 'original request context path' + assert ( + headers['X-javax.servlet.forward.servlet_path'] == '/fwd' + ), 'original request servlet path' + assert ( + headers['X-javax.servlet.forward.path_info'] == 'null' + ), 'original request path info' + assert ( + headers['X-javax.servlet.forward.query_string'] + == 'uri=%2Fdata%2Ftest%3Furi%3Dnew_uri%26a%3D2%26b%3D3&a=1&c=4' + ), 'original request query' + + assert ( + 'Before forwarding' in resp['body'] + ) == False, 'discarded data added before forward() call' + assert ( + 'X-After-Forwarding' in headers + ) == False, 'cannot add headers after forward() call' + assert ( + 'After forwarding' in resp['body'] + ) == False, 'cannot add data after forward() call' def test_java_application_named_dispatcher_forward(self): self.load('forward') @@ -814,74 +645,52 @@ class TestJavaApplication(TestApplicationJava): resp = self.get(url='/fwd?disp=name&uri=data') headers = resp['headers'] - self.assertEqual( - headers['X-REQUEST-Id'], 'fwd', 'initial request servlet mapping' - ) - self.assertEqual( - headers['X-Forward-To'], 'data', 'forwarding triggered' - ) - - self.assertEqual( - headers['X-FORWARD-Id'], 'data', 'forward request servlet mapping' - ) - self.assertEqual( - headers['X-FORWARD-Request-URI'], '/fwd', 'forward request uri' - ) - self.assertEqual( - headers['X-FORWARD-Servlet-Path'], - '/fwd', - 'forward request servlet path', - ) - self.assertEqual( - headers['X-FORWARD-Path-Info'], 'null', 'forward request path info' - ) - self.assertEqual( - headers['X-FORWARD-Query-String'], - 'disp=name&uri=data', - 'forward request query string', - ) - - self.assertEqual( - headers['X-javax.servlet.forward.request_uri'], - 'null', - 'original request uri', - ) - self.assertEqual( - headers['X-javax.servlet.forward.context_path'], - 'null', - 'original request context path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.servlet_path'], - 'null', - 'original request servlet path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.path_info'], - 'null', - 'original request path info', - ) - self.assertEqual( - headers['X-javax.servlet.forward.query_string'], - 'null', - 'original request query', - ) - - self.assertEqual( - 'Before forwarding' in resp['body'], - False, - 'discarded data added before forward() call', - ) - self.assertEqual( - 'X-After-Forwarding' in headers, - False, - 'cannot add headers after forward() call', - ) - self.assertEqual( - 'After forwarding' in resp['body'], - False, - 'cannot add data after forward() call', - ) + assert ( + headers['X-REQUEST-Id'] == 'fwd' + ), 'initial request servlet mapping' + assert headers['X-Forward-To'] == 'data', 'forwarding triggered' + + assert ( + headers['X-FORWARD-Id'] == 'data' + ), 'forward request servlet mapping' + assert ( + headers['X-FORWARD-Request-URI'] == '/fwd' + ), 'forward request uri' + assert ( + headers['X-FORWARD-Servlet-Path'] == '/fwd' + ), 'forward request servlet path' + assert ( + headers['X-FORWARD-Path-Info'] == 'null' + ), 'forward request path info' + assert ( + headers['X-FORWARD-Query-String'] == 'disp=name&uri=data' + ), 'forward request query string' + + assert ( + headers['X-javax.servlet.forward.request_uri'] == 'null' + ), 'original request uri' + assert ( + headers['X-javax.servlet.forward.context_path'] == 'null' + ), 'original request context path' + assert ( + headers['X-javax.servlet.forward.servlet_path'] == 'null' + ), 'original request servlet path' + assert ( + headers['X-javax.servlet.forward.path_info'] == 'null' + ), 'original request path info' + assert ( + headers['X-javax.servlet.forward.query_string'] == 'null' + ), 'original request query' + + assert ( + 'Before forwarding' in resp['body'] + ) == False, 'discarded data added before forward() call' + assert ( + 'X-After-Forwarding' in headers + ) == False, 'cannot add headers after forward() call' + assert ( + 'After forwarding' in resp['body'] + ) == False, 'cannot add data after forward() call' def test_java_application_request_uri_include(self): self.load('include') @@ -890,55 +699,40 @@ class TestJavaApplication(TestApplicationJava): headers = resp['headers'] body = resp['body'] - self.assertEqual( - headers['X-REQUEST-Id'], 'inc', 'initial request servlet mapping' - ) - self.assertEqual( - headers['X-Include'], '/data/test', 'including triggered' - ) - - self.assertEqual( - 'X-INCLUDE-Id' in headers, - False, - 'unable to add headers in include request', - ) - - self.assertEqual( - 'javax.servlet.include.request_uri: /data/test' in body, - True, - 'include request uri', - ) - # self.assertEqual('javax.servlet.include.context_path: ' in body, - # 'include request context path') - self.assertEqual( - 'javax.servlet.include.servlet_path: /data' in body, - True, - 'include request servlet path', - ) - self.assertEqual( - 'javax.servlet.include.path_info: /test' in body, - True, - 'include request path info', - ) - self.assertEqual( - 'javax.servlet.include.query_string: null' in body, - True, - 'include request query', - ) - - self.assertEqual( - 'Before include' in body, - True, - 'preserve data added before include() call', - ) - self.assertEqual( - headers['X-After-Include'], - 'you-should-see-this', - 'add headers after include() call', - ) - self.assertEqual( - 'After include' in body, True, 'add data after include() call' - ) + assert ( + headers['X-REQUEST-Id'] == 'inc' + ), 'initial request servlet mapping' + assert headers['X-Include'] == '/data/test', 'including triggered' + + assert ( + 'X-INCLUDE-Id' in headers + ) == False, 'unable to add headers in include request' + + assert ( + 'javax.servlet.include.request_uri: /data/test' in body + ) == True, 'include request uri' + #assert ( + # 'javax.servlet.include.context_path: ' in body + #) == True, 'include request context path' + assert ( + 'javax.servlet.include.servlet_path: /data' in body + ) == True, 'include request servlet path' + assert ( + 'javax.servlet.include.path_info: /test' in body + ) == True, 'include request path info' + assert ( + 'javax.servlet.include.query_string: null' in body + ) == True, 'include request query' + + assert ( + 'Before include' in body + ) == True, 'preserve data added before include() call' + assert ( + headers['X-After-Include'] == 'you-should-see-this' + ), 'add headers after include() call' + assert ( + 'After include' in body + ) == True, 'add data after include() call' def test_java_application_named_dispatcher_include(self): self.load('include') @@ -947,144 +741,105 @@ class TestJavaApplication(TestApplicationJava): headers = resp['headers'] body = resp['body'] - self.assertEqual( - headers['X-REQUEST-Id'], 'inc', 'initial request servlet mapping' - ) - self.assertEqual(headers['X-Include'], 'data', 'including triggered') - - self.assertEqual( - 'X-INCLUDE-Id' in headers, - False, - 'unable to add headers in include request', - ) - - self.assertEqual( - 'javax.servlet.include.request_uri: null' in body, - True, - 'include request uri', - ) - # self.assertEqual('javax.servlet.include.context_path: null' in body, - # 'include request context path') - self.assertEqual( - 'javax.servlet.include.servlet_path: null' in body, - True, - 'include request servlet path', - ) - self.assertEqual( - 'javax.servlet.include.path_info: null' in body, - True, - 'include request path info', - ) - self.assertEqual( - 'javax.servlet.include.query_string: null' in body, - True, - 'include request query', - ) - - self.assertEqual( - 'Before include' in body, - True, - 'preserve data added before include() call', - ) - self.assertEqual( - headers['X-After-Include'], - 'you-should-see-this', - 'add headers after include() call', - ) - self.assertEqual( - 'After include' in body, True, 'add data after include() call' - ) + assert ( + headers['X-REQUEST-Id'] == 'inc' + ), 'initial request servlet mapping' + assert headers['X-Include'] == 'data', 'including triggered' + + assert ( + 'X-INCLUDE-Id' in headers + ) == False, 'unable to add headers in include request' + + assert ( + 'javax.servlet.include.request_uri: null' in body + ) == True, 'include request uri' + #assert ( + # 'javax.servlet.include.context_path: null' in body + #) == True, 'include request context path' + assert ( + 'javax.servlet.include.servlet_path: null' in body + ) == True, 'include request servlet path' + assert ( + 'javax.servlet.include.path_info: null' in body + ) == True, 'include request path info' + assert ( + 'javax.servlet.include.query_string: null' in body + ) == True, 'include request query' + + assert ( + 'Before include' in body + ) == True, 'preserve data added before include() call' + assert ( + headers['X-After-Include'] == 'you-should-see-this' + ), 'add headers after include() call' + assert ( + 'After include' in body + ) == True, 'add data after include() call' def test_java_application_path_translation(self): self.load('path_translation') headers = self.get(url='/pt/test?path=/')['headers'] - self.assertEqual( - headers['X-Servlet-Path'], '/pt', 'matched servlet path' - ) - self.assertEqual( - headers['X-Path-Info'], '/test', 'the rest of the path' - ) - self.assertEqual( - headers['X-Path-Translated'], - headers['X-Real-Path'] + headers['X-Path-Info'], - 'translated path is the app root + path info', - ) - self.assertEqual( - headers['X-Resource-Paths'].endswith('/WEB-INF/, /index.html]'), - True, - 'app root directory content', - ) - self.assertEqual( - headers['X-Resource-As-Stream'], - 'null', - 'no resource stream for root path', - ) + assert headers['X-Servlet-Path'] == '/pt', 'matched servlet path' + assert headers['X-Path-Info'] == '/test', 'the rest of the path' + assert ( + headers['X-Path-Translated'] + == headers['X-Real-Path'] + headers['X-Path-Info'] + ), 'translated path is the app root + path info' + assert ( + headers['X-Resource-Paths'].endswith('/WEB-INF/, /index.html]') + == True + ), 'app root directory content' + assert ( + headers['X-Resource-As-Stream'] == 'null' + ), 'no resource stream for root path' headers = self.get(url='/test?path=/none')['headers'] - self.assertEqual( - headers['X-Servlet-Path'], '/test', 'matched whole path' - ) - self.assertEqual( - headers['X-Path-Info'], - 'null', - 'the rest of the path is null, whole path matched', - ) - self.assertEqual( - headers['X-Path-Translated'], - 'null', - 'translated path is null because path info is null', - ) - self.assertEqual( - headers['X-Real-Path'].endswith('/none'), - True, - 'read path is not null', - ) - self.assertEqual( - headers['X-Resource-Paths'], 'null', 'no resource found' - ) - self.assertEqual( - headers['X-Resource-As-Stream'], 'null', 'no resource stream' - ) + assert headers['X-Servlet-Path'] == '/test', 'matched whole path' + assert ( + headers['X-Path-Info'] == 'null' + ), 'the rest of the path is null, whole path matched' + assert ( + headers['X-Path-Translated'] == 'null' + ), 'translated path is null because path info is null' + assert ( + headers['X-Real-Path'].endswith('/none') == True + ), 'read path is not null' + assert headers['X-Resource-Paths'] == 'null', 'no resource found' + assert headers['X-Resource-As-Stream'] == 'null', 'no resource stream' def test_java_application_query_string(self): self.load('query_string') - self.assertEqual( - self.get(url='/?a=b')['headers']['X-Query-String'], - 'a=b', - 'query string', - ) + assert ( + self.get(url='/?a=b')['headers']['X-Query-String'] == 'a=b' + ), 'query string' def test_java_application_query_empty(self): self.load('query_string') - self.assertEqual( - self.get(url='/?')['headers']['X-Query-String'], - '', - 'query string empty', - ) + assert ( + self.get(url='/?')['headers']['X-Query-String'] == '' + ), 'query string empty' def test_java_application_query_absent(self): self.load('query_string') - self.assertEqual( - self.get()['headers']['X-Query-String'], - 'null', - 'query string absent', - ) + assert ( + self.get()['headers']['X-Query-String'] == 'null' + ), 'query string absent' def test_java_application_empty(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'empty') + assert self.get()['status'] == 200, 'empty' def test_java_application_keepalive_body(self): self.load('mirror') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -1098,7 +853,7 @@ class TestJavaApplication(TestApplicationJava): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -1111,22 +866,22 @@ class TestJavaApplication(TestApplicationJava): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_java_application_http_10(self): self.load('empty') - self.assertEqual(self.get(http_10=True)['status'], 200, 'HTTP 1.0') + assert self.get(http_10=True)['status'] == 200, 'HTTP 1.0' def test_java_application_no_method(self): self.load('empty') - self.assertEqual(self.post()['status'], 405, 'no method') + assert self.post()['status'] == 405, 'no method' def test_java_application_get_header(self): self.load('get_header') - self.assertEqual( + assert ( self.get( headers={ 'X-Header': 'blah', @@ -1134,15 +889,14 @@ class TestJavaApplication(TestApplicationJava): 'Host': 'localhost', 'Connection': 'close', } - )['headers']['X-Reply'], - 'blah', - 'get header', - ) + )['headers']['X-Reply'] + == 'blah' + ), 'get header' def test_java_application_get_header_empty(self): self.load('get_header') - self.assertNotIn('X-Reply', self.get()['headers'], 'get header empty') + assert 'X-Reply' not in self.get()['headers'], 'get header empty' def test_java_application_get_headers(self): self.load('get_headers') @@ -1156,32 +910,28 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual(headers['X-Reply-0'], 'blah', 'get headers') - self.assertEqual(headers['X-Reply-1'], 'blah', 'get headers 2') + assert headers['X-Reply-0'] == 'blah', 'get headers' + assert headers['X-Reply-1'] == 'blah', 'get headers 2' def test_java_application_get_headers_empty(self): self.load('get_headers') - self.assertNotIn( - 'X-Reply-0', self.get()['headers'], 'get headers empty' - ) + assert 'X-Reply-0' not in self.get()['headers'], 'get headers empty' def test_java_application_get_header_names(self): self.load('get_header_names') headers = self.get()['headers'] - self.assertRegex( - headers['X-Reply-0'], r'(?:Host|Connection)', 'get header names' - ) - self.assertRegex( - headers['X-Reply-1'], r'(?:Host|Connection)', 'get header names 2' - ) - self.assertNotEqual( - headers['X-Reply-0'], - headers['X-Reply-1'], - 'get header names not equal', - ) + assert re.search( + r'(?:Host|Connection)', headers['X-Reply-0'] + ), 'get header names' + assert re.search( + r'(?:Host|Connection)', headers['X-Reply-1'] + ), 'get header names 2' + assert ( + headers['X-Reply-0'] != headers['X-Reply-1'] + ), 'get header names not equal' def test_java_application_header_int(self): self.load('header_int') @@ -1195,8 +945,8 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual(headers['X-Set-Int'], '1', 'set int header') - self.assertEqual(headers['X-Get-Int'], '2', 'get int header') + assert headers['X-Set-Int'] == '1', 'set int header' + assert headers['X-Get-Int'] == '2', 'get int header' def test_java_application_header_date(self): self.load('header_date') @@ -1212,20 +962,18 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual( - headers['X-Set-Date'], - 'Thu, 01 Jan 1970 00:00:01 GMT', - 'set date header', - ) - self.assertEqual(headers['X-Get-Date'], date, 'get date header') + assert ( + headers['X-Set-Date'] == 'Thu, 01 Jan 1970 00:00:01 GMT' + ), 'set date header' + assert headers['X-Get-Date'] == date, 'get date header' def test_java_application_multipart(self): self.load('multipart') reldst = '/uploads' - fulldst = self.testdir + reldst + fulldst = self.temp_dir + reldst os.mkdir(fulldst) - self.public_dir(fulldst) + public_dir(fulldst) fields = { 'file': { @@ -1252,16 +1000,13 @@ class TestJavaApplication(TestApplicationJava): body=body, ) - self.assertEqual(resp['status'], 200, 'multipart status') - self.assertRegex( - resp['body'], r'sample\.txt created', 'multipart body' - ) - self.assertIsNotNone( + assert resp['status'] == 200, 'multipart status' + assert re.search( + r'sample\.txt created', resp['body'] + ), 'multipart body' + assert ( self.search_in_log( r'^Data from sample file$', name=reldst + '/sample.txt' - ), - 'file created', - ) - -if __name__ == '__main__': - TestJavaApplication.main() + ) + is not None + ), 'file created' diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py index 4d39bdc3..fa227469 100644 --- a/test/test_java_isolation_rootfs.py +++ b/test/test_java_isolation_rootfs.py @@ -1,6 +1,6 @@ import os import subprocess -import unittest +import pytest from unit.applications.lang.java import TestApplicationJava @@ -8,15 +8,15 @@ from unit.applications.lang.java import TestApplicationJava class TestJavaIsolationRootfs(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} - def setUp(self): - if not self.is_su: - return + def setup_method(self, is_su): + super().setup_method() - super().setUp() + if not is_su: + return - os.makedirs(self.testdir + '/jars') - os.makedirs(self.testdir + '/tmp') - os.chmod(self.testdir + '/tmp', 0o777) + os.makedirs(self.temp_dir + '/jars') + os.makedirs(self.temp_dir + '/tmp') + os.chmod(self.temp_dir + '/tmp', 0o777) try: process = subprocess.Popen( @@ -24,7 +24,7 @@ class TestJavaIsolationRootfs(TestApplicationJava): "mount", "--bind", self.pardir + "/build", - self.testdir + "/jars", + self.temp_dir + "/jars", ], stderr=subprocess.STDOUT, ) @@ -32,54 +32,45 @@ class TestJavaIsolationRootfs(TestApplicationJava): process.communicate() except: - self.fail('Cann\'t run mount process.') + pytest.fail('Cann\'t run mount process.') - def tearDown(self): - if not self.is_su: + def teardown_method(self, is_su): + if not is_su: return try: process = subprocess.Popen( - ["umount", "--lazy", self.testdir + "/jars"], + ["umount", "--lazy", self.temp_dir + "/jars"], stderr=subprocess.STDOUT, ) process.communicate() except: - self.fail('Cann\'t run mount process.') + pytest.fail('Cann\'t run mount process.') # super teardown must happen after unmount to avoid deletion of /build - super().tearDown() + super().teardown_method() - def test_java_isolation_rootfs_chroot_war(self): - if not self.is_su: - print('require root') - raise unittest.SkipTest() + def test_java_isolation_rootfs_chroot_war(self, is_su): + if not is_su: + pytest.skip('require root') isolation = { - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('empty_war', isolation=isolation) - self.assertIn( - 'success', - self.conf( - '"/"', '/config/applications/empty_war/working_directory', - ), + assert 'success' in self.conf( + '"/"', '/config/applications/empty_war/working_directory', ) - self.assertIn( - 'success', self.conf('"/jars"', 'applications/empty_war/unit_jars') + assert 'success' in self.conf( + '"/jars"', 'applications/empty_war/unit_jars' ) - self.assertIn( - 'success', - self.conf('"/java/empty.war"', 'applications/empty_war/webapp'), + assert 'success' in self.conf( + '"/java/empty.war"', 'applications/empty_war/webapp' ) - self.assertEqual(self.get()['status'], 200, 'war') - - -if __name__ == '__main__': - TestJavaIsolationRootfs.main() + assert self.get()['status'] == 200, 'war' diff --git a/test/test_java_websockets.py b/test/test_java_websockets.py index d78f7263..1bbefa1e 100644 --- a/test/test_java_websockets.py +++ b/test/test_java_websockets.py @@ -1,9 +1,10 @@ +import pytest import struct import time -import unittest from unit.applications.lang.java import TestApplicationJava from unit.applications.websockets import TestApplicationWebsocket +from conftest import option, skip_alert class TestJavaWebsockets(TestApplicationJava): @@ -11,23 +12,17 @@ class TestJavaWebsockets(TestApplicationJava): ws = TestApplicationWebsocket() - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' - ), - 'clear keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' + ), 'clear keepalive_interval' - self.skip_alerts.extend( - [r'socket close\(\d+\) failed'] - ) + skip_alert(r'socket close\(\d+\) failed') def close_connection(self, sock): - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) @@ -36,9 +31,9 @@ class TestJavaWebsockets(TestApplicationJava): def check_close(self, sock, code=1000, no_close=False): frame = self.ws.frame_read(sock) - self.assertEqual(frame['fin'], True, 'close fin') - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'close opcode') - self.assertEqual(frame['code'], code, 'close code') + assert frame['fin'] == True, 'close fin' + assert frame['opcode'] == self.ws.OP_CLOSE, 'close opcode' + assert frame['code'] == code, 'close code' if not no_close: sock.close() @@ -49,9 +44,9 @@ class TestJavaWebsockets(TestApplicationJava): else: data = frame['data'].decode('utf-8') - self.assertEqual(frame['fin'], fin, 'fin') - self.assertEqual(frame['opcode'], opcode, 'opcode') - self.assertEqual(data, payload, 'payload') + assert frame['fin'] == fin, 'fin' + assert frame['opcode'] == opcode, 'opcode' + assert data == payload, 'payload' def test_java_websockets_handshake(self): self.load('websockets_mirror') @@ -59,14 +54,12 @@ class TestJavaWebsockets(TestApplicationJava): resp, sock, key = self.ws.upgrade() sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' def test_java_websockets_mirror(self): self.load('websockets_mirror') @@ -78,12 +71,12 @@ class TestJavaWebsockets(TestApplicationJava): self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror') + assert message == frame['data'].decode('utf-8'), 'mirror' self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror 2') + assert message == frame['data'].decode('utf-8'), 'mirror 2' sock.close() @@ -98,8 +91,8 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'no mask opcode') - self.assertEqual(frame['code'], 1002, 'no mask close code') + assert frame['opcode'] == self.ws.OP_CLOSE, 'no mask opcode' + assert frame['code'] == 1002, 'no mask close code' sock.close() @@ -116,11 +109,9 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) - self.assertEqual( - message + ' ' + message, - frame['data'].decode('utf-8'), - 'mirror framing', - ) + assert message + ' ' + message == frame['data'].decode( + 'utf-8' + ), 'mirror framing' sock.close() @@ -136,20 +127,16 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) frame.pop('data') - self.assertDictEqual( - frame, - { - 'fin': True, - 'rsv1': False, - 'rsv2': False, - 'rsv3': False, - 'opcode': self.ws.OP_CLOSE, - 'mask': 0, - 'code': 1002, - 'reason': 'Fragmented control frame', - }, - 'close frame', - ) + assert frame == { + 'fin': True, + 'rsv1': False, + 'rsv2': False, + 'rsv3': False, + 'opcode': self.ws.OP_CLOSE, + 'mask': 0, + 'code': 1002, + 'reason': 'Fragmented control frame', + }, 'close frame' sock.close() @@ -168,13 +155,13 @@ class TestJavaWebsockets(TestApplicationJava): frame1 = self.ws.frame_read(sock1) frame2 = self.ws.frame_read(sock2) - self.assertEqual(message1, frame1['data'].decode('utf-8'), 'client 1') - self.assertEqual(message2, frame2['data'].decode('utf-8'), 'client 2') + assert message1 == frame1['data'].decode('utf-8'), 'client 1' + assert message2 == frame2['data'].decode('utf-8'), 'client 2' sock1.close() sock2.close() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_handshake_upgrade_absent( self ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 @@ -190,7 +177,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'upgrade absent') + assert resp['status'] == 400, 'upgrade absent' def test_java_websockets_handshake_case_insensitive(self): self.load('websockets_mirror') @@ -207,9 +194,9 @@ class TestJavaWebsockets(TestApplicationJava): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') + assert resp['status'] == 101, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_handshake_connection_absent(self): # FAIL self.load('websockets_mirror') @@ -223,7 +210,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_handshake_version_absent(self): self.load('websockets_mirror') @@ -238,9 +225,9 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 426, 'status') + assert resp['status'] == 426, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_handshake_key_invalid(self): self.load('websockets_mirror') @@ -255,7 +242,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'key length') + assert resp['status'] == 400, 'key length' key = self.ws.key() resp = self.get( @@ -269,9 +256,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual( - 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') @@ -287,7 +272,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_handshake_http_10(self): self.load('websockets_mirror') @@ -304,7 +289,7 @@ class TestJavaWebsockets(TestApplicationJava): http_10=True, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_handshake_uri_invalid(self): self.load('websockets_mirror') @@ -321,7 +306,7 @@ class TestJavaWebsockets(TestApplicationJava): url='!', ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_protocol_absent(self): self.load('websockets_mirror') @@ -338,14 +323,12 @@ class TestJavaWebsockets(TestApplicationJava): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' # autobahn-testsuite # @@ -442,12 +425,12 @@ class TestJavaWebsockets(TestApplicationJava): _, sock, _ = self.ws.upgrade() self.ws.frame_write(sock, self.ws.OP_PONG, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_7' # 2_8 self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_8') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_8' # 2_9 @@ -487,7 +470,7 @@ class TestJavaWebsockets(TestApplicationJava): self.close_connection(sock) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_3_1__3_7(self): self.load('websockets_mirror') @@ -513,7 +496,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_2') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_2' sock.close() # 3_3 @@ -531,7 +514,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_3') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_3' sock.close() # 3_4 @@ -549,7 +532,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_4') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_4' sock.close() # 3_5 @@ -735,7 +718,7 @@ class TestJavaWebsockets(TestApplicationJava): # 5_4 self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_4') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_4' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) frame = self.ws.frame_read(sock) @@ -772,7 +755,7 @@ class TestJavaWebsockets(TestApplicationJava): ping_payload = 'ping payload' self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_7' self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) @@ -956,7 +939,7 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_20') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_20' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') self.check_frame( @@ -1089,7 +1072,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1101,7 +1084,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_TEXT, payload) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1114,7 +1097,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1129,7 +1112,7 @@ class TestJavaWebsockets(TestApplicationJava): self.recvall(sock, read_timeout=1) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1249,27 +1232,23 @@ class TestJavaWebsockets(TestApplicationJava): self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) self.check_close(sock, 1002) - def test_java_websockets_9_1_1__9_6_6(self): - if not self.unsafe: - self.skipTest("unsafe, long run") + def test_java_websockets_9_1_1__9_6_6(self, is_unsafe): + if not is_unsafe: + pytest.skip('unsafe, long run') self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - { - 'http': { - 'websocket': { - 'max_frame_size': 33554432, - 'keepalive_interval': 0, - } + assert 'success' in self.conf( + { + 'http': { + 'websocket': { + 'max_frame_size': 33554432, + 'keepalive_interval': 0, } - }, - 'settings', - ), - 'increase max_frame_size and keepalive_interval', - ) + } + }, + 'settings', + ), 'increase max_frame_size and keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1310,7 +1289,7 @@ class TestJavaWebsockets(TestApplicationJava): check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 - if self.system != 'Darwin' and self.system != 'FreeBSD': + 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 @@ -1366,13 +1345,9 @@ class TestJavaWebsockets(TestApplicationJava): def test_java_websockets_max_frame_size(self): self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' - ), - 'configure max_frame_size', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' + ), 'configure max_frame_size' _, sock, _ = self.ws.upgrade() @@ -1392,13 +1367,9 @@ class TestJavaWebsockets(TestApplicationJava): def test_java_websockets_read_timeout(self): self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'read_timeout': 5}}}, 'settings' - ), - 'configure read_timeout', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'read_timeout': 5}}}, 'settings' + ), 'configure read_timeout' _, sock, _ = self.ws.upgrade() @@ -1412,13 +1383,9 @@ class TestJavaWebsockets(TestApplicationJava): def test_java_websockets_keepalive_interval(self): self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' - ), - 'configure keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' + ), 'configure keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1431,7 +1398,3 @@ class TestJavaWebsockets(TestApplicationJava): self.check_frame(frame, True, self.ws.OP_PING, '') # PING frame sock.close() - - -if __name__ == '__main__': - TestJavaWebsockets.main() diff --git a/test/test_node_application.py b/test/test_node_application.py index e46cc6a1..c2b0ed69 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -1,6 +1,8 @@ -import unittest +import pytest +import re from unit.applications.lang.node import TestApplicationNode +from conftest import waitforfiles class TestNodeApplication(TestApplicationNode): @@ -10,16 +12,14 @@ class TestNodeApplication(TestApplicationNode): self.load('basic') resp = self.get() - self.assertEqual( - resp['headers']['Content-Type'], 'text/plain', 'basic header' - ) - self.assertEqual(resp['body'], 'Hello World\n', 'basic body') + assert resp['headers']['Content-Type'] == 'text/plain', 'basic header' + assert resp['body'] == 'Hello World\n', 'basic body' def test_node_application_seq(self): self.load('basic') - self.assertEqual(self.get()['status'], 200, 'seq') - self.assertEqual(self.get()['status'], 200, 'seq 2') + assert self.get()['status'] == 200, 'seq' + assert self.get()['status'] == 200, 'seq 2' def test_node_application_variables(self): self.load('variables') @@ -36,51 +36,44 @@ class TestNodeApplication(TestApplicationNode): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' raw_headers = headers.pop('Request-Raw-Headers') - self.assertRegex( - raw_headers, + assert re.search( r'^(?:Host|localhost|Content-Type|' 'text\/html|Custom-Header|blah|Content-Length|17|Connection|' 'close|,)+$', - 'raw headers', - ) - - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + raw_headers, + ), 'raw headers' + + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + }, 'headers' + assert resp['body'] == body, 'body' def test_node_application_get_variables(self): self.load('get_variables') resp = self.get(url='/?var1=val1&var2=&var3') - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'GET variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'GET variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' + assert resp['headers']['X-Var-2'] == '', 'GET variables 2' + assert resp['headers']['X-Var-3'] == '', 'GET variables 3' def test_node_application_post_variables(self): self.load('post_variables') @@ -94,24 +87,24 @@ class TestNodeApplication(TestApplicationNode): body='var1=val1&var2=&var3', ) - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'POST variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'POST variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' + assert resp['headers']['X-Var-2'] == '', 'POST variables 2' + assert resp['headers']['X-Var-3'] == '', 'POST variables 3' def test_node_application_404(self): self.load('404') resp = self.get() - self.assertEqual(resp['status'], 404, '404 status') - self.assertRegex( - resp['body'], r'404 Not Found', '404 body' - ) + assert resp['status'] == 404, '404 status' + assert re.search( + r'404 Not Found', resp['body'] + ), '404 body' def test_node_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -125,7 +118,7 @@ class TestNodeApplication(TestApplicationNode): read_timeout=1, ) - self.assertEqual(resp['body'], '0123456789' * 500, 'keep-alive 1') + assert resp['body'] == '0123456789' * 500, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -138,47 +131,34 @@ class TestNodeApplication(TestApplicationNode): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_node_application_write_buffer(self): self.load('write_buffer') - self.assertEqual( - self.get()['body'], 'buffer', 'write buffer' - ) + assert self.get()['body'] == 'buffer', 'write buffer' def test_node_application_write_callback(self): self.load('write_callback') - self.assertEqual( - self.get()['body'], - 'helloworld', - 'write callback order', - ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback'), - 'write callback', - ) + assert self.get()['body'] == 'helloworld', 'write callback order' + assert waitforfiles(self.temp_dir + '/node/callback'), 'write callback' def test_node_application_write_before_write_head(self): self.load('write_before_write_head') - self.assertEqual(self.get()['status'], 200, 'write before writeHead') + assert self.get()['status'] == 200, 'write before writeHead' def test_node_application_double_end(self): self.load('double_end') - self.assertEqual(self.get()['status'], 200, 'double end') - self.assertEqual(self.get()['status'], 200, 'double end 2') + assert self.get()['status'] == 200, 'double end' + assert self.get()['status'] == 200, 'double end 2' def test_node_application_write_return(self): self.load('write_return') - self.assertEqual( - self.get()['body'], - 'bodytrue', - 'write return', - ) + assert self.get()['body'] == 'bodytrue', 'write return' def test_node_application_remove_header(self): self.load('remove_header') @@ -190,69 +170,61 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', } ) - self.assertEqual(resp['headers']['Was-Header'], 'true', 'was header') - self.assertEqual(resp['headers']['Has-Header'], 'false', 'has header') - self.assertFalse('X-Header' in resp['headers'], 'remove header') + assert resp['headers']['Was-Header'] == 'true', 'was header' + assert resp['headers']['Has-Header'] == 'false', 'has header' + assert not ('X-Header' in resp['headers']), 'remove header' def test_node_application_remove_header_nonexisting(self): self.load('remove_header') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Remove': 'blah', 'Connection': 'close', } - )['headers']['Has-Header'], - 'true', - 'remove header nonexisting', - ) + )['headers']['Has-Header'] + == 'true' + ), 'remove header nonexisting' def test_node_application_update_header(self): self.load('update_header') - self.assertEqual( - self.get()['headers']['X-Header'], 'new', 'update header' - ) + assert self.get()['headers']['X-Header'] == 'new', 'update header' def test_node_application_set_header_array(self): self.load('set_header_array') - self.assertListEqual( - self.get()['headers']['Set-Cookie'], - ['tc=one,two,three', 'tc=four,five,six'], - 'set header array', - ) + assert self.get()['headers']['Set-Cookie'] == [ + 'tc=one,two,three', + 'tc=four,five,six', + ], 'set header array' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_application_status_message(self): self.load('status_message') - self.assertRegex( - self.get(raw_resp=True), r'200 blah', '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') - self.assertEqual( - self.get()['headers']['X-Type'], 'number', 'get header type' - ) + assert self.get()['headers']['X-Type'] == 'number', 'get header type' def test_node_application_header_name_case(self): self.load('header_name_case') headers = self.get()['headers'] - self.assertEqual(headers['X-HEADER'], '3', 'header value') - self.assertNotIn('X-Header', headers, 'insensitive') - self.assertNotIn('X-header', headers, 'insensitive 2') + assert headers['X-HEADER'] == '3', 'header value' + assert 'X-Header' not in headers, 'insensitive' + assert 'X-header' not in headers, 'insensitive 2' def test_node_application_promise_handler(self): self.load('promise_handler') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -260,19 +232,15 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', }, body='callback', - )['status'], - 200, - 'promise handler request', - ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback'), - 'promise handler', - ) + )['status'] + == 200 + ), 'promise handler request' + assert waitforfiles(self.temp_dir + '/node/callback'), 'promise handler' def test_node_application_promise_handler_write_after_end(self): self.load('promise_handler') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -281,15 +249,14 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', }, body='callback', - )['status'], - 200, - 'promise handler request write after end', - ) + )['status'] + == 200 + ), 'promise handler request write after end' def test_node_application_promise_end(self): self.load('promise_end') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -297,13 +264,10 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', }, body='end', - )['status'], - 200, - 'promise end request', - ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback'), 'promise end' - ) + )['status'] + == 200 + ), 'promise end request' + assert waitforfiles(self.temp_dir + '/node/callback'), 'promise end' def test_node_application_promise_multiple_calls(self): self.load('promise_handler') @@ -317,10 +281,9 @@ class TestNodeApplication(TestApplicationNode): body='callback1', ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback1'), - 'promise first call', - ) + assert waitforfiles( + self.temp_dir + '/node/callback1' + ), 'promise first call' self.post( headers={ @@ -331,65 +294,55 @@ class TestNodeApplication(TestApplicationNode): body='callback2', ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback2'), - 'promise second call', - ) + assert waitforfiles( + self.temp_dir + '/node/callback2' + ), 'promise second call' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_application_header_name_valid(self): self.load('header_name_valid') - self.assertNotIn('status', self.get(), 'header name valid') + assert 'status' not in self.get(), 'header name valid' def test_node_application_header_value_object(self): self.load('header_value_object') - self.assertIn('X-Header', self.get()['headers'], 'header value object') + assert 'X-Header' in self.get()['headers'], 'header value object' def test_node_application_get_header_names(self): self.load('get_header_names') - self.assertListEqual( - self.get()['headers']['X-Names'], - ['date', 'x-header'], - 'get header names', - ) + assert self.get()['headers']['X-Names'] == [ + 'date', + 'x-header', + ], 'get header names' def test_node_application_has_header(self): self.load('has_header') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Header': 'length', 'Connection': 'close', } - )['headers']['X-Has-Header'], - 'false', - 'has header length', - ) + )['headers']['X-Has-Header'] + == 'false' + ), 'has header length' - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Header': 'Date', 'Connection': 'close', } - )['headers']['X-Has-Header'], - 'false', - 'has header date', - ) + )['headers']['X-Has-Header'] + == 'false' + ), 'has header date' def test_node_application_write_multiple(self): self.load('write_multiple') - self.assertEqual( - self.get()['body'], 'writewrite2end', 'write multiple' - ) - - -if __name__ == '__main__': - TestNodeApplication.main() + assert self.get()['body'] == 'writewrite2end', 'write multiple' diff --git a/test/test_node_websockets.py b/test/test_node_websockets.py index 1928d8c9..5af2f6f3 100644 --- a/test/test_node_websockets.py +++ b/test/test_node_websockets.py @@ -1,9 +1,10 @@ +import pytest import struct import time -import unittest from unit.applications.lang.node import TestApplicationNode from unit.applications.websockets import TestApplicationWebsocket +from conftest import option, skip_alert class TestNodeWebsockets(TestApplicationNode): @@ -11,23 +12,17 @@ class TestNodeWebsockets(TestApplicationNode): ws = TestApplicationWebsocket() - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' - ), - 'clear keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' + ), 'clear keepalive_interval' - self.skip_alerts.extend( - [r'socket close\(\d+\) failed'] - ) + skip_alert(r'socket close\(\d+\) failed') def close_connection(self, sock): - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) @@ -36,9 +31,9 @@ class TestNodeWebsockets(TestApplicationNode): def check_close(self, sock, code=1000, no_close=False): frame = self.ws.frame_read(sock) - self.assertEqual(frame['fin'], True, 'close fin') - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'close opcode') - self.assertEqual(frame['code'], code, 'close code') + assert frame['fin'] == True, 'close fin' + assert frame['opcode'] == self.ws.OP_CLOSE, 'close opcode' + assert frame['code'] == code, 'close code' if not no_close: sock.close() @@ -49,9 +44,9 @@ class TestNodeWebsockets(TestApplicationNode): else: data = frame['data'].decode('utf-8') - self.assertEqual(frame['fin'], fin, 'fin') - self.assertEqual(frame['opcode'], opcode, 'opcode') - self.assertEqual(data, payload, 'payload') + assert frame['fin'] == fin, 'fin' + assert frame['opcode'] == opcode, 'opcode' + assert data == payload, 'payload' def test_node_websockets_handshake(self): self.load('websockets/mirror') @@ -59,14 +54,12 @@ class TestNodeWebsockets(TestApplicationNode): resp, sock, key = self.ws.upgrade() sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' def test_node_websockets_mirror(self): self.load('websockets/mirror') @@ -78,12 +71,12 @@ class TestNodeWebsockets(TestApplicationNode): self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror') + assert message == frame['data'].decode('utf-8'), 'mirror' self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror 2') + assert message == frame['data'].decode('utf-8'), 'mirror 2' sock.close() @@ -98,8 +91,8 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'no mask opcode') - self.assertEqual(frame['code'], 1002, 'no mask close code') + assert frame['opcode'] == self.ws.OP_CLOSE, 'no mask opcode' + assert frame['code'] == 1002, 'no mask close code' sock.close() @@ -116,11 +109,9 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) - self.assertEqual( - message + ' ' + message, - frame['data'].decode('utf-8'), - 'mirror framing', - ) + assert message + ' ' + message == frame['data'].decode( + 'utf-8' + ), 'mirror framing' sock.close() @@ -136,20 +127,16 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) frame.pop('data') - self.assertDictEqual( - frame, - { - 'fin': True, - 'rsv1': False, - 'rsv2': False, - 'rsv3': False, - 'opcode': self.ws.OP_CLOSE, - 'mask': 0, - 'code': 1002, - 'reason': 'Fragmented control frame', - }, - 'close frame', - ) + assert frame == { + 'fin': True, + 'rsv1': False, + 'rsv2': False, + 'rsv3': False, + 'opcode': self.ws.OP_CLOSE, + 'mask': 0, + 'code': 1002, + 'reason': 'Fragmented control frame', + }, 'close frame' sock.close() @@ -168,7 +155,7 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) data += frame['data'].decode('utf-8') - self.assertEqual(message, data, 'large') + assert message == data, 'large' sock.close() @@ -187,13 +174,13 @@ class TestNodeWebsockets(TestApplicationNode): frame1 = self.ws.frame_read(sock1) frame2 = self.ws.frame_read(sock2) - self.assertEqual(message1, frame1['data'].decode('utf-8'), 'client 1') - self.assertEqual(message2, frame2['data'].decode('utf-8'), 'client 2') + assert message1 == frame1['data'].decode('utf-8'), 'client 1' + assert message2 == frame2['data'].decode('utf-8'), 'client 2' sock1.close() sock2.close() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_handshake_upgrade_absent( self ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 @@ -209,7 +196,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'upgrade absent') + assert resp['status'] == 400, 'upgrade absent' def test_node_websockets_handshake_case_insensitive(self): self.load('websockets/mirror') @@ -226,9 +213,9 @@ class TestNodeWebsockets(TestApplicationNode): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') + assert resp['status'] == 101, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_handshake_connection_absent(self): # FAIL self.load('websockets/mirror') @@ -242,7 +229,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_handshake_version_absent(self): self.load('websockets/mirror') @@ -257,9 +244,9 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 426, 'status') + assert resp['status'] == 426, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_handshake_key_invalid(self): self.load('websockets/mirror') @@ -274,7 +261,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'key length') + assert resp['status'] == 400, 'key length' key = self.ws.key() resp = self.get( @@ -288,9 +275,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual( - 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') @@ -306,7 +291,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_handshake_http_10(self): self.load('websockets/mirror') @@ -323,7 +308,7 @@ class TestNodeWebsockets(TestApplicationNode): http_10=True, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_handshake_uri_invalid(self): self.load('websockets/mirror') @@ -340,7 +325,7 @@ class TestNodeWebsockets(TestApplicationNode): url='!', ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_protocol_absent(self): self.load('websockets/mirror') @@ -357,14 +342,12 @@ class TestNodeWebsockets(TestApplicationNode): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' # autobahn-testsuite # @@ -461,12 +444,12 @@ class TestNodeWebsockets(TestApplicationNode): _, sock, _ = self.ws.upgrade() self.ws.frame_write(sock, self.ws.OP_PONG, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_7' # 2_8 self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_8') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_8' # 2_9 @@ -506,7 +489,7 @@ class TestNodeWebsockets(TestApplicationNode): self.close_connection(sock) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_3_1__3_7(self): self.load('websockets/mirror') @@ -532,7 +515,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_2') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_2' sock.close() # 3_3 @@ -550,7 +533,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_3') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_3' sock.close() # 3_4 @@ -568,7 +551,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_4') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_4' sock.close() # 3_5 @@ -754,7 +737,7 @@ class TestNodeWebsockets(TestApplicationNode): # 5_4 self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_4') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_4' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) frame = self.ws.frame_read(sock) @@ -791,7 +774,7 @@ class TestNodeWebsockets(TestApplicationNode): ping_payload = 'ping payload' self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_7' self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) @@ -975,7 +958,7 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_20') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_20' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') self.check_frame( @@ -1108,7 +1091,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1120,7 +1103,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_TEXT, payload) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1133,7 +1116,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1148,7 +1131,7 @@ class TestNodeWebsockets(TestApplicationNode): self.recvall(sock, read_timeout=1) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1268,27 +1251,23 @@ class TestNodeWebsockets(TestApplicationNode): self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) self.check_close(sock, 1002) - def test_node_websockets_9_1_1__9_6_6(self): - if not self.unsafe: - self.skipTest("unsafe, long run") + def test_node_websockets_9_1_1__9_6_6(self, is_unsafe): + if not is_unsafe: + pytest.skip('unsafe, long run') self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - { - 'http': { - 'websocket': { - 'max_frame_size': 33554432, - 'keepalive_interval': 0, - } + assert 'success' in self.conf( + { + 'http': { + 'websocket': { + 'max_frame_size': 33554432, + 'keepalive_interval': 0, } - }, - 'settings', - ), - 'increase max_frame_size and keepalive_interval', - ) + } + }, + 'settings', + ), 'increase max_frame_size and keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1329,7 +1308,7 @@ class TestNodeWebsockets(TestApplicationNode): check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 - if self.system != 'Darwin' and self.system != 'FreeBSD': + 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 @@ -1385,13 +1364,9 @@ class TestNodeWebsockets(TestApplicationNode): def test_node_websockets_max_frame_size(self): self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' - ), - 'configure max_frame_size', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' + ), 'configure max_frame_size' _, sock, _ = self.ws.upgrade() @@ -1411,13 +1386,9 @@ class TestNodeWebsockets(TestApplicationNode): def test_node_websockets_read_timeout(self): self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'read_timeout': 5}}}, 'settings' - ), - 'configure read_timeout', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'read_timeout': 5}}}, 'settings' + ), 'configure read_timeout' _, sock, _ = self.ws.upgrade() @@ -1431,13 +1402,9 @@ class TestNodeWebsockets(TestApplicationNode): def test_node_websockets_keepalive_interval(self): self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' - ), - 'configure keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' + ), 'configure keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1450,7 +1417,3 @@ class TestNodeWebsockets(TestApplicationNode): self.check_frame(frame, True, self.ws.OP_PING, '') # PING frame sock.close() - - -if __name__ == '__main__': - TestNodeWebsockets.main() diff --git a/test/test_perl_application.py b/test/test_perl_application.py index dbf6abf7..bb63eceb 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -1,6 +1,8 @@ -import unittest +import pytest +import re from unit.applications.lang.perl import TestApplicationPerl +from conftest import skip_alert class TestPerlApplication(TestApplicationPerl): @@ -21,149 +23,130 @@ class TestPerlApplication(TestApplicationPerl): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) - - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - 'Psgi-Version': '11', - 'Psgi-Url-Scheme': 'http', - 'Psgi-Multithread': '', - 'Psgi-Multiprocess': '1', - 'Psgi-Run-Once': '', - 'Psgi-Nonblocking': '', - 'Psgi-Streaming': '1', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + 'Psgi-Version': '11', + 'Psgi-Url-Scheme': 'http', + 'Psgi-Multithread': '', + 'Psgi-Multiprocess': '1', + 'Psgi-Run-Once': '', + 'Psgi-Nonblocking': '', + 'Psgi-Streaming': '1', + }, 'headers' + assert resp['body'] == body, 'body' def test_perl_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String header', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String header' def test_perl_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_perl_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_perl_application_server_port(self): self.load('server_port') - self.assertEqual( - self.get()['headers']['Server-Port'], '7080', 'Server-Port header' - ) + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' def test_perl_application_input_read_empty(self): self.load('input_read_empty') - self.assertEqual(self.get()['body'], '', 'read empty') + assert self.get()['body'] == '', 'read empty' def test_perl_application_input_read_parts(self): self.load('input_read_parts') - self.assertEqual( - self.post(body='0123456789')['body'], - '0123456789', - 'input read parts', - ) + assert ( + self.post(body='0123456789')['body'] == '0123456789' + ), 'input read parts' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_perl_application_input_read_offset(self): self.load('input_read_offset') - self.assertEqual( - self.post(body='0123456789')['body'], '4567', 'read offset' - ) + assert self.post(body='0123456789')['body'] == '4567', 'read offset' def test_perl_application_input_copy(self): self.load('input_copy') body = '0123456789' - self.assertEqual(self.post(body=body)['body'], body, 'input copy') + assert self.post(body=body)['body'] == body, 'input copy' def test_perl_application_errors_print(self): self.load('errors_print') - self.assertEqual(self.get()['body'], '1', 'errors result') + assert self.get()['body'] == '1', 'errors result' self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application'), - 'errors print', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application') + is not None + ), 'errors print' def test_perl_application_header_equal_names(self): self.load('header_equal_names') - self.assertListEqual( - self.get()['headers']['Set-Cookie'], - ['tc=one,two,three', 'tc=four,five,six'], - 'header equal names', - ) + assert self.get()['headers']['Set-Cookie'] == [ + 'tc=one,two,three', + 'tc=four,five,six', + ], 'header equal names' def test_perl_application_header_pairs(self): self.load('header_pairs') - self.assertEqual(self.get()['headers']['blah'], 'blah', 'header pairs') + assert self.get()['headers']['blah'] == 'blah', 'header pairs' def test_perl_application_body_empty(self): self.load('body_empty') - self.assertEqual(self.get()['body'], '', 'body empty') + assert self.get()['body'] == '', 'body empty' def test_perl_application_body_array(self): self.load('body_array') - self.assertEqual(self.get()['body'], '0123456789', 'body array') + assert self.get()['body'] == '0123456789', 'body array' def test_perl_application_body_large(self): self.load('variables') @@ -172,31 +155,29 @@ class TestPerlApplication(TestApplicationPerl): resp = self.post(body=body)['body'] - self.assertEqual(resp, body, 'body large') + assert resp == body, 'body large' def test_perl_application_body_io_empty(self): self.load('body_io_empty') - self.assertEqual(self.get()['status'], 200, 'body io empty') + assert self.get()['status'] == 200, 'body io empty' def test_perl_application_body_io_file(self): self.load('body_io_file') - self.assertEqual(self.get()['body'], 'body\n', 'body io file') + assert self.get()['body'] == 'body\n', 'body io file' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet') def test_perl_application_syntax_error(self): - self.skip_alerts.extend( - [r'PSGI: Failed to parse script'] - ) + skip_alert(r'PSGI: Failed to parse script') self.load('syntax_error') - self.assertEqual(self.get()['status'], 500, 'syntax error') + assert self.get()['status'] == 500, 'syntax error' def test_perl_keepalive_body(self): self.load('variables') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -210,7 +191,7 @@ class TestPerlApplication(TestApplicationPerl): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -223,39 +204,35 @@ class TestPerlApplication(TestApplicationPerl): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_perl_body_io_fake(self): self.load('body_io_fake') - self.assertEqual(self.get()['body'], '21', 'body io fake') + assert self.get()['body'] == '21', 'body io fake' - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+IOFake getline\(\) \$\/ is \d+'), - 'body io fake $/ value', - ) + assert ( + self.wait_for_record(r'\[error\].+IOFake getline\(\) \$\/ is \d+') + is not None + ), 'body io fake $/ value' - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+IOFake close\(\) called'), - 'body io fake close', - ) + assert ( + self.wait_for_record(r'\[error\].+IOFake close\(\) called') + is not None + ), 'body io fake close' def test_perl_delayed_response(self): self.load('delayed_response') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], 'Hello World!', 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == 'Hello World!', 'body' def test_perl_streaming_body(self): self.load('streaming_body') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], 'Hello World!', 'body') - - -if __name__ == '__main__': - TestPerlApplication.main() + assert resp['status'] == 200, 'status' + assert resp['body'] == 'Hello World!', 'body' diff --git a/test/test_php_application.py b/test/test_php_application.py index d8bfade2..18a5c085 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -1,10 +1,11 @@ import os +import pytest import re import shutil import time -import unittest from unit.applications.lang.php import TestApplicationPHP +from conftest import option class TestPHPApplication(TestApplicationPHP): prerequisites = {'modules': {'php': 'all'}} @@ -12,30 +13,21 @@ class TestPHPApplication(TestApplicationPHP): def before_disable_functions(self): body = self.get()['body'] - self.assertRegex(body, r'time: \d+', 'disable_functions before time') - self.assertRegex(body, r'exec: \/\w+', 'disable_functions before exec') + assert re.search(r'time: \d+', body), 'disable_functions before time' + assert re.search(r'exec: \/\w+', body), 'disable_functions before exec' def set_opcache(self, app, val): - self.assertIn( - 'success', - self.conf( - { - "admin": { - "opcache.enable": val, - "opcache.enable_cli": val, - }, - }, - 'applications/' + app + '/options', - ), + assert 'success' in self.conf( + {"admin": {"opcache.enable": val, "opcache.enable_cli": val,},}, + 'applications/' + app + '/options', ) opcache = self.get()['headers']['X-OPcache'] if not opcache or opcache == '-1': - print('opcache is not supported') - raise unittest.SkipTest() + pytest.skip('opcache is not supported') - self.assertEqual(opcache, val, 'opcache value') + assert opcache == val, 'opcache value' def test_php_application_variables(self): self.load('variables') @@ -50,140 +42,122 @@ class TestPHPApplication(TestApplicationPHP): 'Connection': 'close', }, body=body, - url='/index.php/blah?var=val' + url='/index.php/blah?var=val', ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' if 'X-Powered-By' in headers: headers.pop('X-Powered-By') headers.pop('Content-type') - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Request-Method': 'POST', - 'Path-Info': '/blah', - 'Request-Uri': '/index.php/blah?var=val', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Request-Method': 'POST', + 'Path-Info': '/blah', + 'Request-Uri': '/index.php/blah?var=val', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + }, 'headers' + assert resp['body'] == body, 'body' def test_php_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'query string', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'query string' def test_php_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_php_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' def test_php_application_phpinfo(self): self.load('phpinfo') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertNotEqual(resp['body'], '', 'body not empty') + assert resp['status'] == 200, 'status' + assert resp['body'] != '', 'body not empty' def test_php_application_header_status(self): self.load('header') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Connection': 'close', 'X-Header': 'HTTP/1.1 404 Not Found', } - )['status'], - 404, - 'status', - ) + )['status'] + == 404 + ), 'status' - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Connection': 'close', 'X-Header': 'http/1.1 404 Not Found', } - )['status'], - 404, - 'status case insensitive', - ) + )['status'] + == 404 + ), 'status case insensitive' - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Connection': 'close', 'X-Header': 'HTTP/ 404 Not Found', } - )['status'], - 404, - 'status version empty', - ) - + )['status'] + == 404 + ), 'status version empty' def test_php_application_404(self): self.load('404') resp = self.get() - self.assertEqual(resp['status'], 404, '404 status') - self.assertRegex( - resp['body'], r'404 Not Found', '404 body' - ) + assert resp['status'] == 404, '404 status' + assert re.search( + r'404 Not Found', resp['body'] + ), '404 body' def test_php_application_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -197,7 +171,7 @@ class TestPHPApplication(TestApplicationPHP): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -210,22 +184,22 @@ class TestPHPApplication(TestApplicationPHP): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_php_application_conditional(self): self.load('conditional') - self.assertRegex(self.get()['body'], r'True', 'conditional true') - self.assertRegex(self.post()['body'], r'False', 'conditional false') + assert re.search(r'True', self.get()['body']), 'conditional true' + assert re.search(r'False', self.post()['body']), 'conditional false' def test_php_application_get_variables(self): self.load('get_variables') resp = self.get(url='/?var1=val1&var2=&var3') - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(resp['headers']['X-Var-2'], '1', 'GET variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '1', 'GET variables 3') - self.assertEqual(resp['headers']['X-Var-4'], '', 'GET variables 4') + assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' + assert resp['headers']['X-Var-2'] == '1', 'GET variables 2' + assert resp['headers']['X-Var-3'] == '1', 'GET variables 3' + assert resp['headers']['X-Var-4'] == '', 'GET variables 4' def test_php_application_post_variables(self): self.load('post_variables') @@ -238,9 +212,9 @@ class TestPHPApplication(TestApplicationPHP): }, body='var1=val1&var2=', ) - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(resp['headers']['X-Var-2'], '1', 'POST variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'POST variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' + assert resp['headers']['X-Var-2'] == '1', 'POST variables 2' + assert resp['headers']['X-Var-3'] == '', 'POST variables 3' def test_php_application_cookies(self): self.load('cookies') @@ -253,41 +227,32 @@ class TestPHPApplication(TestApplicationPHP): } ) - self.assertEqual(resp['headers']['X-Cookie-1'], 'val', 'cookie') - self.assertEqual(resp['headers']['X-Cookie-2'], 'val2', 'cookie') + assert resp['headers']['X-Cookie-1'] == 'val', 'cookie' + assert resp['headers']['X-Cookie-2'] == 'val2', 'cookie' def test_php_application_ini_precision(self): self.load('ini_precision') - self.assertNotEqual( - self.get()['headers']['X-Precision'], '4', 'ini value default' - ) + assert self.get()['headers']['X-Precision'] != '4', 'ini value default' self.conf( {"file": "ini/php.ini"}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get()['headers']['X-File'], - self.current_dir + '/php/ini_precision/ini/php.ini', - 'ini file', - ) - self.assertEqual( - self.get()['headers']['X-Precision'], '4', 'ini value' - ) + assert ( + self.get()['headers']['X-File'] + == option.test_dir + '/php/ini_precision/ini/php.ini' + ), 'ini file' + assert self.get()['headers']['X-Precision'] == '4', 'ini value' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_php_application_ini_admin_user(self): self.load('ini_precision') - self.assertIn( - 'error', - self.conf( - {"user": {"precision": "4"}, "admin": {"precision": "5"}}, - 'applications/ini_precision/options', - ), - 'ini admin user', - ) + assert 'error' in self.conf( + {"user": {"precision": "4"}, "admin": {"precision": "5"}}, + 'applications/ini_precision/options', + ), 'ini admin user' def test_php_application_ini_admin(self): self.load('ini_precision') @@ -297,9 +262,7 @@ class TestPHPApplication(TestApplicationPHP): 'applications/ini_precision/options', ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value admin' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value admin' def test_php_application_ini_user(self): self.load('ini_precision') @@ -309,9 +272,7 @@ class TestPHPApplication(TestApplicationPHP): 'applications/ini_precision/options', ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value user' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value user' def test_php_application_ini_user_2(self): self.load('ini_precision') @@ -320,17 +281,13 @@ class TestPHPApplication(TestApplicationPHP): {"file": "ini/php.ini"}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get()['headers']['X-Precision'], '4', 'ini user file' - ) + assert self.get()['headers']['X-Precision'] == '4', 'ini user file' self.conf( {"precision": "5"}, 'applications/ini_precision/options/user' ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value user' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value user' def test_php_application_ini_set_admin(self): self.load('ini_precision') @@ -339,11 +296,9 @@ class TestPHPApplication(TestApplicationPHP): {"admin": {"precision": "5"}}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get(url='/?precision=6')['headers']['X-Precision'], - '5', - 'ini set admin', - ) + assert ( + self.get(url='/?precision=6')['headers']['X-Precision'] == '5' + ), 'ini set admin' def test_php_application_ini_set_user(self): self.load('ini_precision') @@ -352,11 +307,9 @@ class TestPHPApplication(TestApplicationPHP): {"user": {"precision": "5"}}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get(url='/?precision=6')['headers']['X-Precision'], - '6', - 'ini set user', - ) + assert ( + self.get(url='/?precision=6')['headers']['X-Precision'] == '6' + ), 'ini set user' def test_php_application_ini_repeat(self): self.load('ini_precision') @@ -365,13 +318,9 @@ class TestPHPApplication(TestApplicationPHP): {"user": {"precision": "5"}}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value' - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value repeat' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value repeat' def test_php_application_disable_functions_exec(self): self.load('time_exec') @@ -385,8 +334,8 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertRegex(body, r'time: \d+', 'disable_functions time') - self.assertNotRegex(body, r'exec: \/\w+', 'disable_functions exec') + assert re.search(r'time: \d+', body), 'disable_functions time' + assert not re.search(r'exec: \/\w+', body), 'disable_functions exec' def test_php_application_disable_functions_comma(self): self.load('time_exec') @@ -400,10 +349,12 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertNotRegex(body, r'time: \d+', 'disable_functions comma time') - self.assertNotRegex( - body, r'exec: \/\w+', 'disable_functions comma exec' - ) + assert not re.search( + r'time: \d+', body + ), 'disable_functions comma time' + assert not re.search( + r'exec: \/\w+', body + ), 'disable_functions comma exec' def test_php_application_disable_functions_space(self): self.load('time_exec') @@ -417,10 +368,12 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertNotRegex(body, r'time: \d+', 'disable_functions space time') - self.assertNotRegex( - body, r'exec: \/\w+', 'disable_functions space exec' - ) + assert not re.search( + r'time: \d+', body + ), 'disable_functions space time' + assert not re.search( + r'exec: \/\w+', body + ), 'disable_functions space exec' def test_php_application_disable_functions_user(self): self.load('time_exec') @@ -434,10 +387,10 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertRegex(body, r'time: \d+', 'disable_functions user time') - self.assertNotRegex( - body, r'exec: \/\w+', 'disable_functions user exec' - ) + assert re.search(r'time: \d+', body), 'disable_functions user time' + assert not re.search( + r'exec: \/\w+', body + ), 'disable_functions user exec' def test_php_application_disable_functions_nonexistent(self): self.load('time_exec') @@ -451,187 +404,165 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertRegex( - body, r'time: \d+', 'disable_functions nonexistent time' - ) - self.assertRegex( - body, r'exec: \/\w+', 'disable_functions nonexistent exec' - ) + assert re.search( + r'time: \d+', body + ), 'disable_functions nonexistent time' + assert re.search( + r'exec: \/\w+', body + ), 'disable_functions nonexistent exec' def test_php_application_disable_classes(self): self.load('date_time') - self.assertRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' self.conf( {"admin": {"disable_classes": "DateTime"}}, 'applications/date_time/options', ) - self.assertNotRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert not re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' def test_php_application_disable_classes_user(self): self.load('date_time') - self.assertRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' self.conf( {"user": {"disable_classes": "DateTime"}}, 'applications/date_time/options', ) - self.assertNotRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert not re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' def test_php_application_error_log(self): self.load('error_log') - self.assertEqual(self.get()['status'], 200, 'status') + assert self.get()['status'] == 200, 'status' time.sleep(1) - self.assertEqual(self.get()['status'], 200, 'status 2') + assert self.get()['status'] == 200, 'status 2' self.stop() pattern = r'\d{4}\/\d\d\/\d\d\s\d\d:.+\[notice\].+Error in application' - self.assertIsNotNone(self.wait_for_record(pattern), 'errors print') + assert self.wait_for_record(pattern) is not None, 'errors print' - with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: errs = re.findall(pattern, f.read()) - self.assertEqual(len(errs), 2, 'error_log count') + assert len(errs) == 2, 'error_log count' date = errs[0].split('[')[0] date2 = errs[1].split('[')[0] - self.assertNotEqual(date, date2, 'date diff') + assert date != date2, 'date diff' def test_php_application_script(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/script"}}, - "applications": { - "script": { - "type": "php", - "processes": {"spare": 0}, - "root": self.current_dir + "/php/script", - "script": "phpinfo.php", - } - }, - } - ), - 'configure script', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/script"}}, + "applications": { + "script": { + "type": "php", + "processes": {"spare": 0}, + "root": option.test_dir + "/php/script", + "script": "phpinfo.php", + } + }, + } + ), 'configure script' resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertNotEqual(resp['body'], '', 'body not empty') + assert resp['status'] == 200, 'status' + assert resp['body'] != '', 'body not empty' def test_php_application_index_default(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, - "applications": { - "phpinfo": { - "type": "php", - "processes": {"spare": 0}, - "root": self.current_dir + "/php/phpinfo", - } - }, - } - ), - 'configure index default', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, + "applications": { + "phpinfo": { + "type": "php", + "processes": {"spare": 0}, + "root": option.test_dir + "/php/phpinfo", + } + }, + } + ), 'configure index default' resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertNotEqual(resp['body'], '', 'body not empty') + assert resp['status'] == 200, 'status' + assert resp['body'] != '', 'body not empty' def test_php_application_extension_check(self): self.load('phpinfo') - self.assertNotEqual( - self.get(url='/index.wrong')['status'], 200, 'status' - ) + assert self.get(url='/index.wrong')['status'] != 200, 'status' - new_root = self.testdir + "/php" + new_root = self.temp_dir + "/php" os.mkdir(new_root) - shutil.copy(self.current_dir + '/php/phpinfo/index.wrong', new_root) - - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, - "applications": { - "phpinfo": { - "type": "php", - "processes": {"spare": 0}, - "root": new_root, - "working_directory": new_root, - } - }, - } - ), - 'configure new root', - ) + shutil.copy(option.test_dir + '/php/phpinfo/index.wrong', new_root) + + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, + "applications": { + "phpinfo": { + "type": "php", + "processes": {"spare": 0}, + "root": new_root, + "working_directory": new_root, + } + }, + } + ), 'configure new root' resp = self.get() - self.assertNotEqual( - str(resp['status']) + resp['body'], '200', 'status new root' - ) + assert str(resp['status']) + resp['body'] != '200', 'status new root' def run_php_application_cwd_root_tests(self): - self.assertIn( - 'success', self.conf_delete('applications/cwd/working_directory') + assert 'success' in self.conf_delete( + 'applications/cwd/working_directory' ) - script_cwd = self.current_dir + '/php/cwd' + script_cwd = option.test_dir + '/php/cwd' resp = self.get() - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], script_cwd, 'default cwd') - - self.assertIn( - 'success', - self.conf( - '"' + self.current_dir + '"', - 'applications/cwd/working_directory', - ), + assert resp['status'] == 200, 'status ok' + assert resp['body'] == script_cwd, 'default cwd' + + assert 'success' in self.conf( + '"' + option.test_dir + '"', 'applications/cwd/working_directory', ) resp = self.get() - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], script_cwd, 'wdir cwd') + assert resp['status'] == 200, 'status ok' + assert resp['body'] == script_cwd, 'wdir cwd' resp = self.get(url='/?chdir=/') - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], '/', 'cwd after chdir') + assert resp['status'] == 200, 'status ok' + assert resp['body'] == '/', 'cwd after chdir' # cwd must be restored resp = self.get() - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], script_cwd, 'cwd restored') + assert resp['status'] == 200, 'status ok' + assert resp['body'] == script_cwd, 'cwd restored' resp = self.get(url='/subdir/') - self.assertEqual( - resp['body'], script_cwd + '/subdir', 'cwd subdir', - ) + assert resp['body'] == script_cwd + '/subdir', 'cwd subdir' def test_php_application_cwd_root(self): self.load('cwd') @@ -650,26 +581,20 @@ class TestPHPApplication(TestApplicationPHP): def run_php_application_cwd_script_tests(self): self.load('cwd') - script_cwd = self.current_dir + '/php/cwd' + script_cwd = option.test_dir + '/php/cwd' - self.assertIn( - 'success', self.conf_delete('applications/cwd/working_directory') + assert 'success' in self.conf_delete( + 'applications/cwd/working_directory' ) - self.assertIn( - 'success', self.conf('"index.php"', 'applications/cwd/script') - ) + assert 'success' in self.conf('"index.php"', 'applications/cwd/script') - self.assertEqual( - self.get()['body'], script_cwd, 'default cwd', - ) + assert self.get()['body'] == script_cwd, 'default cwd' - self.assertEqual( - self.get(url='/?chdir=/')['body'], '/', 'cwd after chdir', - ) + assert self.get(url='/?chdir=/')['body'] == '/', 'cwd after chdir' # cwd must be restored - self.assertEqual(self.get()['body'], script_cwd, 'cwd restored') + assert self.get()['body'] == script_cwd, 'cwd restored' def test_php_application_cwd_script(self): self.load('cwd') @@ -688,14 +613,10 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_path_relative(self): self.load('open') - self.assertEqual(self.get()['body'], 'test', 'relative path') - - self.assertNotEqual( - self.get(url='/?chdir=/')['body'], 'test', 'relative path w/ chdir' - ) - - self.assertEqual(self.get()['body'], 'test', 'relative path 2') + assert self.get()['body'] == 'test', 'relative path' + assert ( + self.get(url='/?chdir=/')['body'] != 'test' + ), 'relative path w/ chdir' -if __name__ == '__main__': - TestPHPApplication.main() + assert self.get()['body'] == 'test', 'relative path 2' diff --git a/test/test_php_basic.py b/test/test_php_basic.py index 16483c4a..1420ec21 100644 --- a/test/test_php_basic.py +++ b/test/test_php_basic.py @@ -23,133 +23,97 @@ class TestPHPBasic(TestControl): conf = self.conf_get() - self.assertEqual(conf['listeners'], {}, 'listeners') - self.assertEqual( - conf['applications'], - { - "app": { - "type": "php", - "processes": {"spare": 0}, - "root": "/app", - "index": "index.php", - } - }, - 'applications', - ) - - self.assertEqual( - self.conf_get('applications'), - { - "app": { - "type": "php", - "processes": {"spare": 0}, - "root": "/app", - "index": "index.php", - } - }, - 'applications prefix', - ) - - self.assertEqual( - self.conf_get('applications/app'), - { + assert conf['listeners'] == {}, 'listeners' + assert conf['applications'] == { + "app": { "type": "php", "processes": {"spare": 0}, "root": "/app", "index": "index.php", - }, - 'applications prefix 2', - ) + } + }, 'applications' - self.assertEqual(self.conf_get('applications/app/type'), 'php', 'type') - self.assertEqual( - self.conf_get('applications/app/processes/spare'), - 0, - 'spare processes', - ) + assert self.conf_get('applications') == { + "app": { + "type": "php", + "processes": {"spare": 0}, + "root": "/app", + "index": "index.php", + } + }, 'applications prefix' + + assert self.conf_get('applications/app') == { + "type": "php", + "processes": {"spare": 0}, + "root": "/app", + "index": "index.php", + }, 'applications prefix 2' + + assert self.conf_get('applications/app/type') == 'php', 'type' + assert ( + self.conf_get('applications/app/processes/spare') == 0 + ), 'spare processes' def test_php_get_listeners(self): self.conf(self.conf_basic) - self.assertEqual( - self.conf_get()['listeners'], - {"*:7080": {"pass": "applications/app"}}, - 'listeners', - ) + assert self.conf_get()['listeners'] == { + "*:7080": {"pass": "applications/app"} + }, 'listeners' - self.assertEqual( - self.conf_get('listeners'), - {"*:7080": {"pass": "applications/app"}}, - 'listeners prefix', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"} + }, 'listeners prefix' - self.assertEqual( - self.conf_get('listeners/*:7080'), - {"pass": "applications/app"}, - 'listeners prefix 2', - ) + assert self.conf_get('listeners/*:7080') == { + "pass": "applications/app" + }, 'listeners prefix 2' def test_php_change_listener(self): self.conf(self.conf_basic) self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners') - self.assertEqual( - self.conf_get('listeners'), - {"*:7081": {"pass": "applications/app"}}, - 'change listener', - ) + assert self.conf_get('listeners') == { + "*:7081": {"pass": "applications/app"} + }, 'change listener' def test_php_add_listener(self): self.conf(self.conf_basic) self.conf({"pass": "applications/app"}, 'listeners/*:7082') - self.assertEqual( - self.conf_get('listeners'), - { - "*:7080": {"pass": "applications/app"}, - "*:7082": {"pass": "applications/app"}, - }, - 'add listener', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"}, + "*:7082": {"pass": "applications/app"}, + }, 'add listener' def test_php_change_application(self): self.conf(self.conf_basic) self.conf('30', 'applications/app/processes/max') - self.assertEqual( - self.conf_get('applications/app/processes/max'), - 30, - 'change application max', - ) + assert ( + self.conf_get('applications/app/processes/max') == 30 + ), 'change application max' self.conf('"/www"', 'applications/app/root') - self.assertEqual( - self.conf_get('applications/app/root'), - '/www', - 'change application root', - ) + assert ( + self.conf_get('applications/app/root') == '/www' + ), 'change application root' def test_php_delete(self): self.conf(self.conf_basic) - self.assertIn('error', self.conf_delete('applications/app')) - self.assertIn('success', self.conf_delete('listeners/*:7080')) - self.assertIn('success', self.conf_delete('applications/app')) - self.assertIn('error', self.conf_delete('applications/app')) + assert 'error' in self.conf_delete('applications/app') + assert 'success' in self.conf_delete('listeners/*:7080') + assert 'success' in self.conf_delete('applications/app') + assert 'error' in self.conf_delete('applications/app') def test_php_delete_blocks(self): self.conf(self.conf_basic) - self.assertIn('success', self.conf_delete('listeners')) - self.assertIn('success', self.conf_delete('applications')) - - self.assertIn('success', self.conf(self.conf_app, 'applications')) - self.assertIn( - 'success', - self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners'), - 'applications restore', - ) - + assert 'success' in self.conf_delete('listeners') + assert 'success' in self.conf_delete('applications') -if __name__ == '__main__': - TestPHPBasic.main() + assert 'success' in self.conf(self.conf_app, 'applications') + assert 'success' in self.conf( + {"*:7081": {"pass": "applications/app"}}, 'listeners' + ), 'applications restore' diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index 3004a7b8..f4170f1b 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -1,7 +1,8 @@ -import unittest +import pytest from unit.applications.lang.php import TestApplicationPHP from unit.feature.isolation import TestFeatureIsolation +from conftest import option class TestPHPIsolation(TestApplicationPHP): @@ -10,171 +11,128 @@ class TestPHPIsolation(TestApplicationPHP): isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() - def test_php_isolation_rootfs(self): + def test_php_isolation_rootfs(self, is_su): isolation_features = self.available['features']['isolation'].keys() if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') isolation = { - 'namespaces': {'credential': not self.is_su, 'mount': True}, - 'rootfs': self.current_dir, + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': option.test_dir, } self.load('phpinfo', isolation=isolation) - self.assertIn( - 'success', self.conf('"/php/phpinfo"', 'applications/phpinfo/root') + assert 'success' in self.conf( + '"/php/phpinfo"', 'applications/phpinfo/root' ) - self.assertIn( - 'success', - self.conf( - '"/php/phpinfo"', 'applications/phpinfo/working_directory' - ), + assert 'success' in self.conf( + '"/php/phpinfo"', 'applications/phpinfo/working_directory' ) - self.assertEqual(self.get()['status'], 200, 'empty rootfs') + assert self.get()['status'] == 200, 'empty rootfs' - def test_php_isolation_rootfs_extensions(self): + def test_php_isolation_rootfs_extensions(self, is_su): isolation_features = self.available['features']['isolation'].keys() - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') isolation = { - 'rootfs': self.current_dir, - 'namespaces': { - 'credential': not self.is_su, - 'mount': not self.is_su, - }, + 'rootfs': option.test_dir, + 'namespaces': {'credential': not is_su, 'mount': not is_su}, } self.load('list-extensions', isolation=isolation) - self.assertIn( - 'success', - self.conf( - '"/php/list-extensions"', 'applications/list-extensions/root' - ), + assert 'success' in self.conf( + '"/php/list-extensions"', 'applications/list-extensions/root' ) - self.assertIn( - 'success', - self.conf( - {'file': '/php/list-extensions/php.ini'}, - 'applications/list-extensions/options', - ), + assert 'success' in self.conf( + {'file': '/php/list-extensions/php.ini'}, + 'applications/list-extensions/options', ) - self.assertIn( - 'success', - self.conf( - '"/php/list-extensions"', - 'applications/list-extensions/working_directory', - ), + assert 'success' in self.conf( + '"/php/list-extensions"', + 'applications/list-extensions/working_directory', ) extensions = self.getjson()['body'] - self.assertIn('json', extensions, 'json in extensions list') - self.assertIn('unit', extensions, 'unit in extensions list') + assert 'json' in extensions, 'json in extensions list' + assert 'unit' in extensions, 'unit in extensions list' - - def test_php_isolation_rootfs_no_language_libs(self): + def test_php_isolation_rootfs_no_language_libs(self, is_su): isolation_features = self.available['features']['isolation'].keys() - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') isolation = { - 'rootfs': self.current_dir, + 'rootfs': option.test_dir, 'automount': {'language_deps': False}, - 'namespaces': { - 'credential': not self.is_su, - 'mount': not self.is_su, - }, + 'namespaces': {'credential': not is_su, 'mount': not is_su}, } self.load('list-extensions', isolation=isolation) - self.assertIn( - 'success', - self.conf( - '"/php/list-extensions"', 'applications/list-extensions/root' - ), + assert 'success' in self.conf( + '"/php/list-extensions"', 'applications/list-extensions/root' ) - self.assertIn( - 'success', - self.conf( - {'file': '/php/list-extensions/php.ini'}, - 'applications/list-extensions/options', - ), + assert 'success' in self.conf( + {'file': '/php/list-extensions/php.ini'}, + 'applications/list-extensions/options', ) - self.assertIn( - 'success', - self.conf( - '"/php/list-extensions"', - 'applications/list-extensions/working_directory', - ), + assert 'success' in self.conf( + '"/php/list-extensions"', + 'applications/list-extensions/working_directory', ) extensions = self.getjson()['body'] - self.assertIn('unit', extensions, 'unit in extensions list') - self.assertNotIn('json', extensions, 'json not in extensions list') + assert 'unit' in extensions, 'unit in extensions list' + assert 'json' not in extensions, 'json not in extensions list' - self.assertIn( - 'success', - self.conf( - {'language_deps': True}, - 'applications/list-extensions/isolation/automount', - ), + assert 'success' in self.conf( + {'language_deps': True}, + 'applications/list-extensions/isolation/automount', ) extensions = self.getjson()['body'] - self.assertIn('unit', extensions, 'unit in extensions list 2') - self.assertIn('json', extensions, 'json in extensions list 2') - + assert 'unit' in extensions, 'unit in extensions list 2' + assert 'json' in extensions, 'json in extensions list 2' -if __name__ == '__main__': - TestPHPIsolation.main() diff --git a/test/test_php_targets.py b/test/test_php_targets.py index 0657554a..2eadf071 100644 --- a/test/test_php_targets.py +++ b/test/test_php_targets.py @@ -1,128 +1,98 @@ from unit.applications.lang.php import TestApplicationPHP +from conftest import option + class TestPHPTargets(TestApplicationPHP): prerequisites = {'modules': {'php': 'any'}} def test_php_application_targets(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [ - { - "match": {"uri": "/1"}, - "action": {"pass": "applications/targets/1"}, - }, - { - "match": {"uri": "/2"}, - "action": {"pass": "applications/targets/2"}, - }, - {"action": {"pass": "applications/targets/default"}}, - ], - "applications": { + 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"}, + }, + {"action": {"pass": "applications/targets/default"}}, + ], + "applications": { + "targets": { + "type": "php", + "processes": {"spare": 0}, "targets": { - "type": "php", - "processes": {"spare": 0}, - "targets": { - "1": { - "script": "1.php", - "root": self.current_dir + "/php/targets", - }, - "2": { - "script": "2.php", - "root": self.current_dir - + "/php/targets/2", - }, - "default": { - "index": "index.php", - "root": self.current_dir + "/php/targets", - }, + "1": { + "script": "1.php", + "root": option.test_dir + "/php/targets", }, - } - }, - } - ), + "2": { + "script": "2.php", + "root": option.test_dir + "/php/targets/2", + }, + "default": { + "index": "index.php", + "root": option.test_dir + "/php/targets", + }, + }, + } + }, + } ) - self.assertEqual(self.get(url='/1')['body'], '1') - self.assertEqual(self.get(url='/2')['body'], '2') - self.assertEqual(self.get(url='/blah')['status'], 503) # TODO 404 - self.assertEqual(self.get(url='/')['body'], 'index') + assert self.get(url='/1')['body'] == '1' + assert self.get(url='/2')['body'] == '2' + assert self.get(url='/blah')['status'] == 503 # TODO 404 + assert self.get(url='/')['body'] == 'index' - self.assertIn( - 'success', - self.conf( - "\"1.php\"", 'applications/targets/targets/default/index' - ), - 'change targets index', - ) - self.assertEqual(self.get(url='/')['body'], '1') + assert 'success' in self.conf( + "\"1.php\"", 'applications/targets/targets/default/index' + ), 'change targets index' + assert self.get(url='/')['body'] == '1' - self.assertIn( - 'success', - self.conf_delete('applications/targets/targets/default/index'), - 'remove targets index', - ) - self.assertEqual(self.get(url='/')['body'], 'index') + assert 'success' in self.conf_delete( + 'applications/targets/targets/default/index' + ), 'remove targets index' + assert self.get(url='/')['body'] == 'index' def test_php_application_targets_error(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "applications/targets/default"} - }, - "applications": { + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "applications/targets/default"} + }, + "applications": { + "targets": { + "type": "php", + "processes": {"spare": 0}, "targets": { - "type": "php", - "processes": {"spare": 0}, - "targets": { - "default": { - "index": "index.php", - "root": self.current_dir + "/php/targets", - }, + "default": { + "index": "index.php", + "root": option.test_dir + "/php/targets", }, - } - }, - } - ), - 'initial configuration', - ) - self.assertEqual(self.get()['status'], 200) - - self.assertIn( - 'error', - self.conf( - {"pass": "applications/targets/blah"}, 'listeners/*:7080' - ), - 'invalid targets pass', - ) - self.assertIn( - 'error', - self.conf( - '"' + self.current_dir + '/php/targets\"', - 'applications/targets/root', - ), - 'invalid root', - ) - self.assertIn( - 'error', - self.conf('"index.php"', 'applications/targets/index'), - 'invalid index', - ) - self.assertIn( - 'error', - self.conf('"index.php"', 'applications/targets/script'), - 'invalid script', - ) - self.assertIn( - 'error', - self.conf_delete('applications/targets/default/root'), - 'root remove', - ) - + }, + } + }, + } + ), 'initial configuration' + assert self.get()['status'] == 200 -if __name__ == '__main__': - TestPHPTargets.main() + assert 'error' in self.conf( + {"pass": "applications/targets/blah"}, 'listeners/*:7080' + ), 'invalid targets pass' + assert 'error' in self.conf( + '"' + option.test_dir + '/php/targets\"', + 'applications/targets/root', + ), 'invalid root' + assert 'error' in self.conf( + '"index.php"', 'applications/targets/index' + ), 'invalid index' + assert 'error' in self.conf( + '"index.php"', 'applications/targets/script' + ), 'invalid script' + assert 'error' in self.conf_delete( + 'applications/targets/default/root' + ), 'root remove' diff --git a/test/test_proxy.py b/test/test_proxy.py index feec1ac4..1476670c 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -1,9 +1,10 @@ +import pytest import re import socket import time -import unittest from unit.applications.lang.python import TestApplicationPython +from conftest import option, skip_alert class TestProxy(TestApplicationPython): @@ -42,7 +43,7 @@ Content-Length: 10 to_send = req - m = re.search('X-Len: (\d+)', data) + m = re.search(r'X-Len: (\d+)', data) if m: to_send += b'X' * int(m.group(1)) @@ -56,145 +57,127 @@ Content-Length: 10 def post_http10(self, *args, **kwargs): return self.post(*args, http_10=True, **kwargs) - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() self.run_process(self.run_server, self.SERVER_PORT) self.waitforsocket(self.SERVER_PORT) - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": {"pass": "applications/mirror"}, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "applications/mirror"}, + }, + "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "applications": { + "mirror": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/mirror", + "working_directory": option.test_dir + + "/python/mirror", + "module": "wsgi", }, - "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], - "applications": { - "mirror": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/mirror", - "working_directory": self.current_dir - + "/python/mirror", - "module": "wsgi", - }, - "custom_header": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/custom_header", - "working_directory": self.current_dir - + "/python/custom_header", - "module": "wsgi", - }, - "delayed": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/delayed", - "working_directory": self.current_dir - + "/python/delayed", - "module": "wsgi", - }, + "custom_header": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/custom_header", + "working_directory": option.test_dir + + "/python/custom_header", + "module": "wsgi", }, - } - ), - 'proxy initial configuration', - ) + "delayed": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/delayed", + "working_directory": option.test_dir + + "/python/delayed", + "module": "wsgi", + }, + }, + } + ), 'proxy initial configuration' def test_proxy_http10(self): for _ in range(10): - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_chain(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes/first"}, - "*:7081": {"pass": "routes/second"}, - "*:7082": {"pass": "routes/third"}, - "*:7083": {"pass": "routes/fourth"}, - "*:7084": {"pass": "routes/fifth"}, - "*:7085": {"pass": "applications/mirror"}, - }, - "routes": { - "first": [ - {"action": {"proxy": "http://127.0.0.1:7081"}} - ], - "second": [ - {"action": {"proxy": "http://127.0.0.1:7082"}} - ], - "third": [ - {"action": {"proxy": "http://127.0.0.1:7083"}} - ], - "fourth": [ - {"action": {"proxy": "http://127.0.0.1:7084"}} - ], - "fifth": [ - {"action": {"proxy": "http://127.0.0.1:7085"}} - ], - }, - "applications": { - "mirror": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/mirror", - "working_directory": self.current_dir - + "/python/mirror", - "module": "wsgi", - } - }, - } - ), - 'proxy chain configuration', - ) + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes/first"}, + "*:7081": {"pass": "routes/second"}, + "*:7082": {"pass": "routes/third"}, + "*:7083": {"pass": "routes/fourth"}, + "*:7084": {"pass": "routes/fifth"}, + "*:7085": {"pass": "applications/mirror"}, + }, + "routes": { + "first": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "second": [{"action": {"proxy": "http://127.0.0.1:7082"}}], + "third": [{"action": {"proxy": "http://127.0.0.1:7083"}}], + "fourth": [{"action": {"proxy": "http://127.0.0.1:7084"}}], + "fifth": [{"action": {"proxy": "http://127.0.0.1:7085"}}], + }, + "applications": { + "mirror": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/mirror", + "working_directory": option.test_dir + + "/python/mirror", + "module": "wsgi", + } + }, + } + ), 'proxy chain configuration' - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_body(self): payload = '0123456789' for _ in range(10): resp = self.post_http10(body=payload) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4096 for _ in range(10): resp = self.post_http10(body=payload) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4097 for _ in range(10): resp = self.post_http10(body=payload) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4096 * 256 for _ in range(10): resp = self.post_http10(body=payload, read_buffer_size=4096 * 128) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4096 * 257 for _ in range(10): resp = self.post_http10(body=payload, read_buffer_size=4096 * 128) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' self.conf({'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings') payload = '0123456789abcdef' * 32 * 64 * 1024 resp = self.post_http10(body=payload, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' def test_proxy_parallel(self): payload = 'X' * 4096 * 257 @@ -216,62 +199,53 @@ Content-Length: 10 resp = self._resp_to_dict(resp) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload + str(i), 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload + str(i), 'body' def test_proxy_header(self): - self.assertIn( - 'success', - self.conf( - {"pass": "applications/custom_header"}, 'listeners/*:7081' - ), - 'custom_header configure', - ) + assert 'success' in self.conf( + {"pass": "applications/custom_header"}, 'listeners/*:7081' + ), 'custom_header configure' header_value = 'blah' - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header' header_value = '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header 2', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header 2' header_value = 'X' * 4096 - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header 3', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header 3' header_value = 'X' * 8191 - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header 4', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header 4' header_value = 'X' * 8192 - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['status'], - 431, - 'custom header 5', - ) + )['status'] + == 431 + ), 'custom header 5' def test_proxy_fragmented(self): _, sock = self.http( @@ -286,9 +260,9 @@ Content-Length: 10 sock.sendall("t\r\n\r\n".encode()) - self.assertRegex( - self.recvall(sock).decode(), '200 OK', 'fragmented send' - ) + assert re.search( + '200 OK', self.recvall(sock).decode() + ), 'fragmented send' sock.close() def test_proxy_fragmented_close(self): @@ -328,8 +302,8 @@ Content-Length: 10 resp = self._resp_to_dict(self.recvall(sock).decode()) sock.close() - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], "X" * 30000, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == "X" * 30000, 'body' def test_proxy_fragmented_body_close(self): _, sock = self.http( @@ -349,70 +323,48 @@ Content-Length: 10 sock.close() def test_proxy_nowhere(self): - self.assertIn( - 'success', - self.conf( - [{"action": {"proxy": "http://127.0.0.1:7082"}}], 'routes' - ), - 'proxy path changed', - ) + assert 'success' in self.conf( + [{"action": {"proxy": "http://127.0.0.1:7082"}}], 'routes' + ), 'proxy path changed' - self.assertEqual(self.get_http10()['status'], 502, 'status') + assert self.get_http10()['status'] == 502, 'status' def test_proxy_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "[::1]:7081": {'application': 'mirror'}, - }, - 'listeners', - ), - 'add ipv6 listener configure', - ) + assert 'success' in self.conf( + { + "*:7080": {"pass": "routes"}, + "[::1]:7081": {'application': 'mirror'}, + }, + 'listeners', + ), 'add ipv6 listener configure' - self.assertIn( - 'success', - self.conf([{"action": {"proxy": "http://[::1]:7081"}}], 'routes'), - 'proxy ipv6 configure', - ) + assert 'success' in self.conf( + [{"action": {"proxy": "http://[::1]:7081"}}], 'routes' + ), 'proxy ipv6 configure' - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_unix(self): - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "unix:" + addr: {'application': 'mirror'}, - }, - 'listeners', - ), - 'add unix listener configure', - ) + assert 'success' in self.conf( + { + "*:7080": {"pass": "routes"}, + "unix:" + addr: {'application': 'mirror'}, + }, + 'listeners', + ), 'add unix listener configure' - self.assertIn( - 'success', - self.conf( - [{"action": {"proxy": 'http://unix:' + addr}}], 'routes' - ), - 'proxy unix configure', - ) + assert 'success' in self.conf( + [{"action": {"proxy": 'http://unix:' + addr}}], 'routes' + ), 'proxy unix configure' - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_delayed(self): - self.assertIn( - 'success', - self.conf( - {"pass": "applications/delayed"}, 'listeners/*:7081' - ), - 'delayed configure', - ) + assert 'success' in self.conf( + {"pass": "applications/delayed"}, 'listeners/*:7081' + ), 'delayed configure' body = '0123456789' * 1000 resp = self.post_http10( @@ -426,8 +378,8 @@ Content-Length: 10 body=body, ) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], body, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == body, 'body' resp = self.post_http10( headers={ @@ -440,17 +392,13 @@ Content-Length: 10 body=body, ) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], body, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == body, 'body' def test_proxy_delayed_close(self): - self.assertIn( - 'success', - self.conf( - {"pass": "applications/delayed"}, 'listeners/*:7081' - ), - 'delayed configure', - ) + assert 'success' in self.conf( + {"pass": "applications/delayed"}, 'listeners/*:7081' + ), 'delayed configure' _, sock = self.post_http10( headers={ @@ -465,9 +413,7 @@ Content-Length: 10 no_recv=True, ) - self.assertRegex( - sock.recv(100).decode(), '200 OK', 'first' - ) + assert re.search('200 OK', sock.recv(100).decode()), 'first' sock.close() _, sock = self.post_http10( @@ -483,51 +429,42 @@ Content-Length: 10 no_recv=True, ) - self.assertRegex( - sock.recv(100).decode(), '200 OK', 'second' - ) + assert re.search('200 OK', sock.recv(100).decode()), 'second' sock.close() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_proxy_content_length(self): - self.assertIn( - 'success', - self.conf( - [ - { - "action": { - "proxy": "http://127.0.0.1:" - + str(self.SERVER_PORT) - } + assert 'success' in self.conf( + [ + { + "action": { + "proxy": "http://127.0.0.1:" + str(self.SERVER_PORT) } - ], - 'routes', - ), - 'proxy backend configure', - ) + } + ], + 'routes', + ), 'proxy backend configure' resp = self.get_http10() - self.assertEqual(len(resp['body']), 0, 'body lt Content-Length 0') + assert len(resp['body']) == 0, 'body lt Content-Length 0' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '5'}) - self.assertEqual(len(resp['body']), 5, 'body lt Content-Length 5') + assert len(resp['body']) == 5, 'body lt Content-Length 5' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '9'}) - self.assertEqual(len(resp['body']), 9, 'body lt Content-Length 9') + assert len(resp['body']) == 9, 'body lt Content-Length 9' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '11'}) - self.assertEqual(len(resp['body']), 10, 'body gt Content-Length 11') + assert len(resp['body']) == 10, 'body gt Content-Length 11' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '15'}) - self.assertEqual(len(resp['body']), 10, 'body gt Content-Length 15') + assert len(resp['body']) == 10, 'body gt Content-Length 15' def test_proxy_invalid(self): def check_proxy(proxy): - self.assertIn( - 'error', - 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') @@ -544,12 +481,10 @@ Content-Length: 10 check_proxy('http://[::7080') def test_proxy_loop(self): - self.skip_alerts.extend( - [ - r'socket.*failed', - r'accept.*failed', - r'new connections are not accepted', - ] + skip_alert( + r'socket.*failed', + r'accept.*failed', + r'new connections are not accepted', ) self.conf( { @@ -563,9 +498,8 @@ Content-Length: 10 "mirror": { "type": "python", "processes": {"spare": 0}, - "path": self.current_dir + "/python/mirror", - "working_directory": self.current_dir - + "/python/mirror", + "path": option.test_dir + "/python/mirror", + "working_directory": option.test_dir + "/python/mirror", "module": "wsgi", }, }, @@ -574,6 +508,3 @@ Content-Length: 10 self.get_http10(no_recv=True) self.get_http10(read_timeout=1) - -if __name__ == '__main__': - TestProxy.main() diff --git a/test/test_proxy_chunked.py b/test/test_proxy_chunked.py index f344b69a..93746703 100644 --- a/test/test_proxy_chunked.py +++ b/test/test_proxy_chunked.py @@ -12,7 +12,7 @@ class TestProxyChunked(TestApplicationPython): SERVER_PORT = 7999 @staticmethod - def run_server(server_port, testdir): + def run_server(server_port, temp_dir): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -81,68 +81,62 @@ class TestProxyChunked(TestApplicationPython): def get_http10(self, *args, **kwargs): return self.get(*args, http_10=True, **kwargs) - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.run_process(self.run_server, self.SERVER_PORT, self.testdir) + self.run_process(self.run_server, self.SERVER_PORT, self.temp_dir) self.waitforsocket(self.SERVER_PORT) - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes"},}, - "routes": [ - { - "action": { - "proxy": "http://127.0.0.1:" - + str(self.SERVER_PORT) - } + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"},}, + "routes": [ + { + "action": { + "proxy": "http://127.0.0.1:" + + str(self.SERVER_PORT) } - ], - } - ), - 'proxy initial configuration', - ) + } + ], + } + ), 'proxy initial configuration' def test_proxy_chunked(self): for _ in range(10): - self.assertEqual( - self.get_http10(body='\r\n\r\n0\r\n\r\n')['status'], 200 - ) + assert self.get_http10(body='\r\n\r\n0\r\n\r\n')['status'] == 200 def test_proxy_chunked_body(self): part = '0123456789abcdef' - self.assertEqual( + assert ( self.get_http10(body=self.chunks([('1000', part + ' X 256')]))[ 'body' - ], - part * 256, + ] + == part * 256 ) - self.assertEqual( + assert ( self.get_http10(body=self.chunks([('100000', part + ' X 65536')]))[ 'body' - ], - part * 65536, + ] + == part * 65536 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks([('1000000', part + ' X 1048576')]), read_buffer_size=4096 * 4096, - )['body'], - part * 1048576, + )['body'] + == part * 1048576 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [('1000', part + ' X 256'), ('1000', part + ' X 256')] ) - )['body'], - part * 256 * 2, + )['body'] + == part * 256 * 2 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [ @@ -150,10 +144,10 @@ class TestProxyChunked(TestApplicationPython): ('100000', part + ' X 65536'), ] ) - )['body'], - part * 65536 * 2, + )['body'] + == part * 65536 * 2 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [ @@ -162,42 +156,40 @@ class TestProxyChunked(TestApplicationPython): ] ), read_buffer_size=4096 * 4096, - )['body'], - part * 1048576 * 2, + )['body'] + == part * 1048576 * 2 ) def test_proxy_chunked_fragmented(self): part = '0123456789abcdef' - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [('1', hex(i % 16)[2:]) for i in range(4096)] ), - )['body'], - part * 256, + )['body'] + == part * 256 ) def test_proxy_chunked_send(self): - self.assertEqual( - self.get_http10(body='\r\n\r\n@0@\r\n\r\n')['status'], 200 - ) - self.assertEqual( + assert self.get_http10(body='\r\n\r\n@0@\r\n\r\n')['status'] == 200 + assert ( self.get_http10( body='\r@\n\r\n2\r@\na@b\r\n2\r\ncd@\r\n0\r@\n\r\n' - )['body'], - 'abcd', + )['body'] + == 'abcd' ) - self.assertEqual( + assert ( self.get_http10( body='\r\n\r\n2\r#\na#b\r\n##2\r\n#cd\r\n0\r\n#\r#\n' - )['body'], - 'abcd', + )['body'] + == 'abcd' ) def test_proxy_chunked_invalid(self): def check_invalid(body): - self.assertNotEqual(self.get_http10(body=body)['status'], 200) + assert self.get_http10(body=body)['status'] != 200 check_invalid('\r\n\r0') check_invalid('\r\n\r\n\r0') @@ -209,41 +201,38 @@ class TestProxyChunked(TestApplicationPython): check_invalid('\r\n\r\n0\r\nX') resp = self.get_http10(body='\r\n\r\n65#\r\nA X 100') - self.assertEqual(resp['status'], 200, 'incomplete chunk status') - self.assertNotEqual(resp['body'][-5:], '0\r\n\r\n', 'incomplete chunk') + assert resp['status'] == 200, 'incomplete chunk status' + assert resp['body'][-5:] != '0\r\n\r\n', 'incomplete chunk' resp = self.get_http10(body='\r\n\r\n64#\r\nA X 100') - self.assertEqual(resp['status'], 200, 'no zero chunk status') - self.assertNotEqual(resp['body'][-5:], '0\r\n\r\n', 'no zero chunk') + assert resp['status'] == 200, 'no zero chunk status' + assert resp['body'][-5:] != '0\r\n\r\n', 'no zero chunk' - self.assertEqual( - self.get_http10(body='\r\n\r\n80000000\r\nA X 100')['status'], 200, + assert ( + self.get_http10(body='\r\n\r\n80000000\r\nA X 100')['status'] + == 200 ) - self.assertEqual( + assert ( self.get_http10(body='\r\n\r\n10000000000000000\r\nA X 100')[ 'status' - ], - 502, + ] + == 502 ) - self.assertGreaterEqual( + assert ( len( self.get_http10( body='\r\n\r\n1000000\r\nA X 1048576\r\n1000000\r\nA X 100', read_buffer_size=4096 * 4096, )['body'] - ), - 1048576, + ) + >= 1048576 ) - self.assertGreaterEqual( + assert ( len( self.get_http10( body='\r\n\r\n1000000\r\nA X 1048576\r\nXXX\r\nA X 100', read_buffer_size=4096 * 4096, )['body'] - ), - 1048576, + ) + >= 1048576 ) - - -if __name__ == '__main__': - TestProxyChunked.main() diff --git a/test/test_python_application.py b/test/test_python_application.py index 4b8983ff..d1079116 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -1,17 +1,18 @@ import grp +import pytest import pwd import re import time -import unittest from unit.applications.lang.python import TestApplicationPython +from conftest import skip_alert class TestPythonApplication(TestApplicationPython): prerequisites = {'modules': {'python': 'all'}} def findall(self, pattern): - with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) def test_python_application_variables(self): @@ -29,135 +30,109 @@ class TestPythonApplication(TestApplicationPython): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - 'Wsgi-Version': '(1, 0)', - 'Wsgi-Url-Scheme': 'http', - 'Wsgi-Multithread': 'False', - 'Wsgi-Multiprocess': 'True', - 'Wsgi-Run-Once': 'False', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + 'Wsgi-Version': '(1, 0)', + 'Wsgi-Url-Scheme': 'http', + 'Wsgi-Multithread': 'False', + 'Wsgi-Multiprocess': 'True', + 'Wsgi-Run-Once': 'False', + }, 'headers' + assert resp['body'] == body, 'body' def test_python_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String header', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String header' def test_python_application_query_string_space(self): self.load('query_string') resp = self.get(url='/ ?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String space', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String space' resp = self.get(url='/ %20?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String space 2', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String space 2' resp = self.get(url='/ %20 ?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String space 3', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String space 3' resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - ' var1= val1 & var2=val2', - 'Query-String space 4', - ) + assert ( + resp['headers']['Query-String'] == ' var1= val1 & var2=val2' + ), 'Query-String space 4' def test_python_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_python_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_server_port(self): self.load('server_port') - self.assertEqual( - self.get()['headers']['Server-Port'], '7080', 'Server-Port header' - ) + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_working_directory_invalid(self): self.load('empty') - self.assertIn( - 'success', - self.conf('"/blah"', 'applications/empty/working_directory'), - 'configure invalid working_directory', - ) + assert 'success' in self.conf( + '"/blah"', 'applications/empty/working_directory' + ), 'configure invalid working_directory' - self.assertEqual(self.get()['status'], 500, 'status') + assert self.get()['status'] == 500, 'status' def test_python_application_204_transfer_encoding(self): self.load('204_no_content') - self.assertNotIn( - 'Transfer-Encoding', - self.get()['headers'], - '204 header transfer encoding', - ) + assert ( + 'Transfer-Encoding' not in self.get()['headers'] + ), '204 header transfer encoding' def test_python_application_ctx_iter_atexit(self): self.load('ctx_iter_atexit') @@ -171,21 +146,21 @@ class TestPythonApplication(TestApplicationPython): body='0123456789', ) - self.assertEqual(resp['status'], 200, 'ctx iter status') - self.assertEqual(resp['body'], '0123456789', 'ctx iter body') + assert resp['status'] == 200, 'ctx iter status' + assert resp['body'] == '0123456789', 'ctx iter body' self.conf({"listeners": {}, "applications": {}}) self.stop() - self.assertIsNotNone( - self.wait_for_record(r'RuntimeError'), 'ctx iter atexit' - ) + assert ( + self.wait_for_record(r'RuntimeError') is not None + ), 'ctx iter atexit' def test_python_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -199,7 +174,7 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -212,19 +187,17 @@ class TestPythonApplication(TestApplicationPython): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_python_keepalive_reconfigure(self): - self.skip_alerts.extend( - [ - r'pthread_mutex.+failed', - r'failed to apply', - r'process \d+ exited on signal', - ] + skip_alert( + r'pthread_mutex.+failed', + r'failed to apply', + r'process \d+ exited on signal', ) self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' conns = 3 @@ -242,12 +215,10 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive open') - self.assertIn( - 'success', - self.conf(str(i + 1), 'applications/mirror/processes'), - 'reconfigure', - ) + assert resp['body'] == body, 'keep-alive open' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure' socks.append(sock) @@ -264,12 +235,10 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive request') - self.assertIn( - 'success', - self.conf(str(i + 1), 'applications/mirror/processes'), - 'reconfigure 2', - ) + assert resp['body'] == body, 'keep-alive request' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 2' for i in range(conns): resp = self.post( @@ -282,17 +251,15 @@ class TestPythonApplication(TestApplicationPython): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive close') - self.assertIn( - 'success', - self.conf(str(i + 1), 'applications/mirror/processes'), - 'reconfigure 3', - ) + assert resp['body'] == body, 'keep-alive close' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 3' def test_python_keepalive_reconfigure_2(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' @@ -307,11 +274,11 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'reconfigure 2 keep-alive 1') + assert resp['body'] == body, 'reconfigure 2 keep-alive 1' self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' (resp, sock) = self.post( headers={ @@ -324,23 +291,21 @@ class TestPythonApplication(TestApplicationPython): body=body, ) - self.assertEqual(resp['status'], 200, 'reconfigure 2 keep-alive 2') - self.assertEqual(resp['body'], '', 'reconfigure 2 keep-alive 2 body') + assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' + assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' - self.assertIn( - 'success', - self.conf({"listeners": {}, "applications": {}}), - 'reconfigure 2 clear configuration', - ) + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 2 clear configuration' resp = self.get(sock=sock) - self.assertEqual(resp, {}, 'reconfigure 2 keep-alive 3') + assert resp == {}, 'reconfigure 2 keep-alive 3' def test_python_keepalive_reconfigure_3(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' (_, sock) = self.http( b"""GET / HTTP/1.1 @@ -350,13 +315,11 @@ class TestPythonApplication(TestApplicationPython): no_recv=True, ) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 - self.assertIn( - 'success', - self.conf({"listeners": {}, "applications": {}}), - 'reconfigure 3 clear configuration', - ) + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 3 clear configuration' resp = self.http( b"""Host: localhost @@ -367,7 +330,7 @@ Connection: close raw=True, ) - self.assertEqual(resp['status'], 200, 'reconfigure 3') + assert resp['status'] == 200, 'reconfigure 3' def test_python_atexit(self): self.load('atexit') @@ -378,25 +341,24 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'At exit called\.'), 'atexit' - ) + assert self.wait_for_record(r'At exit called\.') is not None, 'atexit' def test_python_process_switch(self): self.load('delayed') - self.assertIn( - 'success', - self.conf('2', 'applications/delayed/processes'), - 'configure 2 processes', - ) + assert 'success' in self.conf( + '2', 'applications/delayed/processes' + ), 'configure 2 processes' - self.get(headers={ - 'Host': 'localhost', - 'Content-Length': '0', - 'X-Delay': '5', - 'Connection': 'close', - }, no_recv=True) + self.get( + headers={ + 'Host': 'localhost', + 'Content-Length': '0', + 'X-Delay': '5', + 'Connection': 'close', + }, + no_recv=True, + ) headers_delay_1 = { 'Connection': 'close', @@ -414,11 +376,11 @@ Connection: close self.get(headers=headers_delay_1) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_start_response_exit(self): self.load('start_response_exit') - self.assertEqual(self.get()['status'], 500, 'start response exit') + assert self.get()['status'] == 500, 'start response exit' def test_python_application_input_iter(self): self.load('input_iter') @@ -429,10 +391,8 @@ next line last line''' resp = self.post(body=body) - self.assertEqual(resp['body'], body, 'input iter') - self.assertEqual( - resp['headers']['X-Lines-Count'], '4', 'input iter lines' - ) + assert resp['body'] == body, 'input iter' + assert resp['headers']['X-Lines-Count'] == '4', 'input iter lines' def test_python_application_input_readline(self): self.load('input_readline') @@ -443,10 +403,8 @@ next line last line''' resp = self.post(body=body) - self.assertEqual(resp['body'], body, 'input readline') - self.assertEqual( - resp['headers']['X-Lines-Count'], '4', 'input readline lines' - ) + assert resp['body'] == body, 'input readline' + assert resp['headers']['X-Lines-Count'] == '4', 'input readline lines' def test_python_application_input_readline_size(self): self.load('input_readline_size') @@ -456,12 +414,10 @@ next line last line''' - self.assertEqual( - self.post(body=body)['body'], body, 'input readline size' - ) - self.assertEqual( - self.post(body='0123')['body'], '0123', 'input readline size less' - ) + assert self.post(body=body)['body'] == body, 'input readline size' + assert ( + self.post(body='0123')['body'] == '0123' + ), 'input readline size less' def test_python_application_input_readlines(self): self.load('input_readlines') @@ -472,10 +428,8 @@ next line last line''' resp = self.post(body=body) - self.assertEqual(resp['body'], body, 'input readlines') - self.assertEqual( - resp['headers']['X-Lines-Count'], '4', 'input readlines lines' - ) + assert resp['body'] == body, 'input readlines' + assert resp['headers']['X-Lines-Count'] == '4', 'input readlines lines' def test_python_application_input_readlines_huge(self): self.load('input_readlines') @@ -489,11 +443,9 @@ last line: 987654321 * 512 ) - self.assertEqual( - self.post(body=body, read_buffer_size=16384)['body'], - body, - 'input readlines huge', - ) + assert ( + self.post(body=body, read_buffer_size=16384)['body'] == body + ), 'input readlines huge' def test_python_application_input_read_length(self): self.load('input_read_length') @@ -509,7 +461,7 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], body[:5], 'input read length lt body') + assert resp['body'] == body[:5], 'input read length lt body' resp = self.post( headers={ @@ -520,7 +472,7 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], body, 'input read length gt body') + assert resp['body'] == body, 'input read length gt body' resp = self.post( headers={ @@ -531,7 +483,7 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], '', 'input read length zero') + assert resp['body'] == '', 'input read length zero' resp = self.post( headers={ @@ -542,9 +494,9 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], body, 'input read length negative') + assert resp['body'] == body, 'input read length negative' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_errors_write(self): self.load('errors_write') @@ -552,43 +504,41 @@ last line: 987654321 self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application\.'), - 'errors write', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application\.') + is not None + ), 'errors write' def test_python_application_body_array(self): self.load('body_array') - self.assertEqual(self.get()['body'], '0123456789', 'body array') + assert self.get()['body'] == '0123456789', 'body array' def test_python_application_body_io(self): self.load('body_io') - self.assertEqual(self.get()['body'], '0123456789', 'body io') + assert self.get()['body'] == '0123456789', 'body io' def test_python_application_body_io_file(self): self.load('body_io_file') - self.assertEqual(self.get()['body'], 'body\n', 'body io file') + assert self.get()['body'] == 'body\n', 'body io file' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_syntax_error(self): - self.skip_alerts.append(r'Python failed to import module "wsgi"') + skip_alert(r'Python failed to import module "wsgi"') self.load('syntax_error') - self.assertEqual(self.get()['status'], 500, 'syntax error') + assert self.get()['status'] == 500, 'syntax error' def test_python_application_loading_error(self): - self.skip_alerts.append(r'Python failed to import module "blah"') + skip_alert(r'Python failed to import module "blah"') self.load('empty') - self.assertIn( - 'success', self.conf('"blah"', 'applications/empty/module'), - ) + assert 'success' in self.conf('"blah"', 'applications/empty/module') - self.assertEqual(self.get()['status'], 503, 'loading error') + assert self.get()['status'] == 503, 'loading error' def test_python_application_close(self): self.load('close') @@ -597,7 +547,7 @@ last line: 987654321 self.stop() - self.assertIsNotNone(self.wait_for_record(r'Close called\.'), 'close') + assert self.wait_for_record(r'Close called\.') is not None, 'close' def test_python_application_close_error(self): self.load('close_error') @@ -606,9 +556,9 @@ last line: 987654321 self.stop() - self.assertIsNotNone( - self.wait_for_record(r'Close called\.'), 'close error' - ) + assert ( + self.wait_for_record(r'Close called\.') is not None + ), 'close error' def test_python_application_not_iterable(self): self.load('not_iterable') @@ -617,17 +567,17 @@ last line: 987654321 self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'\[error\].+the application returned not an iterable object' - ), - 'not iterable', - ) + ) + is not None + ), 'not iterable' def test_python_application_write(self): self.load('write') - self.assertEqual(self.get()['body'], '0123456789', 'write') + assert self.get()['body'] == '0123456789', 'write' def test_python_application_threading(self): """wait_for_record() timeouts after 5s while every thread works at @@ -639,9 +589,9 @@ last line: 987654321 for _ in range(10): self.get(no_recv=True) - self.assertIsNotNone( - self.wait_for_record(r'\(5\) Thread: 100'), 'last thread finished' - ) + assert ( + self.wait_for_record(r'\(5\) Thread: 100') is not None + ), 'last thread finished' def test_python_application_iter_exception(self): self.load('iter_exception') @@ -656,43 +606,38 @@ last line: 987654321 'Connection': 'close', } ) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], 'XXXXXXX', 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == 'XXXXXXX', 'body' # Exception before start_response(). - self.assertEqual(self.get()['status'], 503, 'error') + assert self.get()['status'] == 503, 'error' - self.assertIsNotNone(self.wait_for_record(r'Traceback'), 'traceback') - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'first exception\'\)'), - 'first exception raise', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 1, 'traceback count 1' - ) + assert self.wait_for_record(r'Traceback') is not None, 'traceback' + assert ( + self.wait_for_record(r'raise Exception\(\'first exception\'\)') + is not None + ), 'first exception raise' + assert len(self.findall(r'Traceback')) == 1, 'traceback count 1' # Exception after start_response(), before first write(). - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Skip': '1', 'Connection': 'close', } - )['status'], - 503, - 'error 2', - ) + )['status'] + == 503 + ), 'error 2' - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'second exception\'\)'), - 'exception raise second', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 2, 'traceback count 2' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'second exception\'\)') + is not None + ), 'exception raise second' + assert len(self.findall(r'Traceback')) == 2, 'traceback count 2' # Exception after first write(), before first __next__(). @@ -705,15 +650,13 @@ last line: 987654321 start=True, ) - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'third exception\'\)'), - 'exception raise third', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 3, 'traceback count 3' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'third exception\'\)') + is not None + ), 'exception raise third' + assert len(self.findall(r'Traceback')) == 3, 'traceback count 3' - self.assertDictEqual(self.get(sock=sock), {}, 'closed connection') + assert self.get(sock=sock) == {}, 'closed connection' # Exception after first write(), before first __next__(), # chunked (incomplete body). @@ -725,13 +668,11 @@ last line: 987654321 'X-Chunked': '1', 'Connection': 'close', }, - raw_resp=True + raw_resp=True, ) if resp: - self.assertNotEqual(resp[-5:], '0\r\n\r\n', 'incomplete body') - self.assertEqual( - len(self.findall(r'Traceback')), 4, 'traceback count 4' - ) + assert resp[-5:] != '0\r\n\r\n', 'incomplete body' + assert len(self.findall(r'Traceback')) == 4, 'traceback count 4' # Exception in __next__(). @@ -744,15 +685,13 @@ last line: 987654321 start=True, ) - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'next exception\'\)'), - 'exception raise next', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 5, 'traceback count 5' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'next exception\'\)') + is not None + ), 'exception raise next' + assert len(self.findall(r'Traceback')) == 5, 'traceback count 5' - self.assertDictEqual(self.get(sock=sock), {}, 'closed connection 2') + assert self.get(sock=sock) == {}, 'closed connection 2' # Exception in __next__(), chunked (incomplete body). @@ -763,40 +702,34 @@ last line: 987654321 'X-Chunked': '1', 'Connection': 'close', }, - raw_resp=True + raw_resp=True, ) if resp: - self.assertNotEqual(resp[-5:], '0\r\n\r\n', 'incomplete body 2') - self.assertEqual( - len(self.findall(r'Traceback')), 6, 'traceback count 6' - ) + assert resp[-5:] != '0\r\n\r\n', 'incomplete body 2' + assert len(self.findall(r'Traceback')) == 6, 'traceback count 6' # Exception before start_response() and in close(). - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Not-Skip-Close': '1', 'Connection': 'close', } - )['status'], - 503, - 'error', - ) + )['status'] + == 503 + ), 'error' - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'close exception\'\)'), - 'exception raise close', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 8, 'traceback count 8' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'close exception\'\)') + is not None + ), 'exception raise close' + assert len(self.findall(r'Traceback')) == 8, 'traceback count 8' - def test_python_user_group(self): - if not self.is_su: - print("requires root") - raise unittest.SkipTest() + def test_python_user_group(self, is_su): + if not is_su: + pytest.skip('requires root') nobody_uid = pwd.getpwnam('nobody').pw_uid @@ -811,40 +744,38 @@ last line: 987654321 self.load('user_group') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'nobody uid') - self.assertEqual(obj['GID'], group_id, 'nobody gid') + assert obj['UID'] == nobody_uid, 'nobody uid' + assert obj['GID'] == group_id, 'nobody gid' self.load('user_group', user='nobody') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'nobody uid user=nobody') - self.assertEqual(obj['GID'], group_id, 'nobody gid user=nobody') + assert obj['UID'] == nobody_uid, 'nobody uid user=nobody' + assert obj['GID'] == group_id, 'nobody gid user=nobody' self.load('user_group', user='nobody', group=group) obj = self.getjson()['body'] - self.assertEqual( - obj['UID'], nobody_uid, 'nobody uid user=nobody group=%s' % group + assert obj['UID'] == nobody_uid, ( + 'nobody uid user=nobody group=%s' % group ) - self.assertEqual( - obj['GID'], group_id, 'nobody gid user=nobody group=%s' % group + assert obj['GID'] == group_id, ( + 'nobody gid user=nobody group=%s' % group ) self.load('user_group', group=group) obj = self.getjson()['body'] - self.assertEqual( - obj['UID'], nobody_uid, 'nobody uid group=%s' % group - ) + assert obj['UID'] == nobody_uid, 'nobody uid group=%s' % group - self.assertEqual(obj['GID'], group_id, 'nobody gid group=%s' % group) + assert obj['GID'] == group_id, 'nobody gid group=%s' % group self.load('user_group', user='root') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'root uid user=root') - self.assertEqual(obj['GID'], 0, 'root gid user=root') + assert obj['UID'] == 0, 'root uid user=root' + assert obj['GID'] == 0, 'root gid user=root' group = 'root' @@ -858,14 +789,11 @@ last line: 987654321 self.load('user_group', user='root', group='root') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'root uid user=root group=root') - self.assertEqual(obj['GID'], 0, 'root gid user=root group=root') + assert obj['UID'] == 0, 'root uid user=root group=root' + assert obj['GID'] == 0, 'root gid user=root group=root' self.load('user_group', group='root') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'root uid group=root') - self.assertEqual(obj['GID'], 0, 'root gid group=root') - -if __name__ == '__main__': - TestPythonApplication.main() + assert obj['UID'] == nobody_uid, 'root uid group=root' + assert obj['GID'] == 0, 'root gid group=root' diff --git a/test/test_python_basic.py b/test/test_python_basic.py index d6445ac2..0cc70e51 100644 --- a/test/test_python_basic.py +++ b/test/test_python_basic.py @@ -19,142 +19,104 @@ class TestPythonBasic(TestControl): } def test_python_get_empty(self): - self.assertEqual(self.conf_get(), {'listeners': {}, 'applications': {}}) - self.assertEqual(self.conf_get('listeners'), {}) - self.assertEqual(self.conf_get('applications'), {}) + assert self.conf_get() == {'listeners': {}, 'applications': {}} + assert self.conf_get('listeners') == {} + assert self.conf_get('applications') == {} def test_python_get_applications(self): self.conf(self.conf_app, 'applications') conf = self.conf_get() - self.assertEqual(conf['listeners'], {}, 'listeners') - self.assertEqual( - conf['applications'], - { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - 'applications', - ) - - self.assertEqual( - self.conf_get('applications'), - { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - 'applications prefix', - ) - - self.assertEqual( - self.conf_get('applications/app'), - { + assert conf['listeners'] == {}, 'listeners' + assert conf['applications'] == { + "app": { "type": "python", "processes": {"spare": 0}, "path": "/app", "module": "wsgi", - }, - 'applications prefix 2', - ) + } + }, 'applications' - self.assertEqual( - self.conf_get('applications/app/type'), 'python', 'type' - ) - self.assertEqual( - self.conf_get('applications/app/processes/spare'), 0, 'spare' - ) + assert self.conf_get('applications') == { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, 'applications prefix' + + assert self.conf_get('applications/app') == { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + }, 'applications prefix 2' + + assert self.conf_get('applications/app/type') == 'python', 'type' + assert self.conf_get('applications/app/processes/spare') == 0, 'spare' def test_python_get_listeners(self): self.conf(self.conf_basic) - self.assertEqual( - self.conf_get()['listeners'], - {"*:7080": {"pass": "applications/app"}}, - 'listeners', - ) + assert self.conf_get()['listeners'] == { + "*:7080": {"pass": "applications/app"} + }, 'listeners' - self.assertEqual( - self.conf_get('listeners'), - {"*:7080": {"pass": "applications/app"}}, - 'listeners prefix', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"} + }, 'listeners prefix' - self.assertEqual( - self.conf_get('listeners/*:7080'), - {"pass": "applications/app"}, - 'listeners prefix 2', - ) + assert self.conf_get('listeners/*:7080') == { + "pass": "applications/app" + }, 'listeners prefix 2' def test_python_change_listener(self): self.conf(self.conf_basic) self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners') - self.assertEqual( - self.conf_get('listeners'), - {"*:7081": {"pass": "applications/app"}}, - 'change listener', - ) + assert self.conf_get('listeners') == { + "*:7081": {"pass": "applications/app"} + }, 'change listener' def test_python_add_listener(self): self.conf(self.conf_basic) self.conf({"pass": "applications/app"}, 'listeners/*:7082') - self.assertEqual( - self.conf_get('listeners'), - { - "*:7080": {"pass": "applications/app"}, - "*:7082": {"pass": "applications/app"}, - }, - 'add listener', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"}, + "*:7082": {"pass": "applications/app"}, + }, 'add listener' def test_python_change_application(self): self.conf(self.conf_basic) self.conf('30', 'applications/app/processes/max') - self.assertEqual( - self.conf_get('applications/app/processes/max'), - 30, - 'change application max', - ) + assert ( + self.conf_get('applications/app/processes/max') == 30 + ), 'change application max' self.conf('"/www"', 'applications/app/path') - self.assertEqual( - self.conf_get('applications/app/path'), - '/www', - 'change application path', - ) + assert ( + self.conf_get('applications/app/path') == '/www' + ), 'change application path' def test_python_delete(self): self.conf(self.conf_basic) - self.assertIn('error', self.conf_delete('applications/app')) - self.assertIn('success', self.conf_delete('listeners/*:7080')) - self.assertIn('success', self.conf_delete('applications/app')) - self.assertIn('error', self.conf_delete('applications/app')) + assert 'error' in self.conf_delete('applications/app') + assert 'success' in self.conf_delete('listeners/*:7080') + assert 'success' in self.conf_delete('applications/app') + assert 'error' in self.conf_delete('applications/app') def test_python_delete_blocks(self): self.conf(self.conf_basic) - self.assertIn('success', self.conf_delete('listeners')) - self.assertIn('success', self.conf_delete('applications')) - - self.assertIn('success', self.conf(self.conf_app, 'applications')) - self.assertIn( - 'success', - self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners'), - 'applications restore', - ) - + assert 'success' in self.conf_delete('listeners') + assert 'success' in self.conf_delete('applications') -if __name__ == '__main__': - TestPythonBasic.main() + assert 'success' in self.conf(self.conf_app, 'applications') + assert 'success' in self.conf( + {"*:7081": {"pass": "applications/app"}}, 'listeners' + ), 'applications restore' diff --git a/test/test_python_environment.py b/test/test_python_environment.py index a03b96e6..2d7d1595 100644 --- a/test/test_python_environment.py +++ b/test/test_python_environment.py @@ -7,97 +7,81 @@ class TestPythonEnvironment(TestApplicationPython): def test_python_environment_name_null(self): self.load('environment') - self.assertIn( - 'error', - self.conf( - {"va\0r": "val1"}, 'applications/environment/environment' - ), - 'name null', - ) + assert 'error' in self.conf( + {"va\0r": "val1"}, 'applications/environment/environment' + ), 'name null' def test_python_environment_name_equals(self): self.load('environment') - self.assertIn( - 'error', - self.conf( - {"var=": "val1"}, 'applications/environment/environment' - ), - 'name equals', - ) + assert 'error' in self.conf( + {"var=": "val1"}, 'applications/environment/environment' + ), 'name equals' def test_python_environment_value_null(self): self.load('environment') - self.assertIn( - 'error', - self.conf( - {"var": "\0val"}, 'applications/environment/environment' - ), - 'value null', - ) + assert 'error' in self.conf( + {"var": "\0val"}, 'applications/environment/environment' + ), 'value null' def test_python_environment_update(self): self.load('environment') self.conf({"var": "val1"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var', 'Connection': 'close', } - )['body'], - 'val1,', - 'set', - ) + )['body'] + == 'val1,' + ), 'set' self.conf({"var": "val2"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var', 'Connection': 'close', } - )['body'], - 'val2,', - 'update', - ) + )['body'] + == 'val2,' + ), 'update' def test_python_environment_replace(self): self.load('environment') self.conf({"var1": "val1"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1', 'Connection': 'close', } - )['body'], - 'val1,', - 'set', - ) + )['body'] + == 'val1,' + ), 'set' self.conf({"var2": "val2"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1,var2', 'Connection': 'close', } - )['body'], - 'val2,', - 'replace', - ) + )['body'] + == 'val2,' + ), 'replace' def test_python_environment_clear(self): self.load('environment') @@ -107,31 +91,29 @@ class TestPythonEnvironment(TestApplicationPython): 'applications/environment/environment', ) - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1,var2', 'Connection': 'close', } - )['body'], - 'val1,val2,', - 'set', - ) + )['body'] + == 'val1,val2,' + ), 'set' self.conf({}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1,var2', 'Connection': 'close', } - )['body'], - '', - 'clear', - ) + )['body'] + == '' + ), 'clear' def test_python_environment_replace_default(self): self.load('environment') @@ -144,36 +126,30 @@ class TestPythonEnvironment(TestApplicationPython): } )['body'] - self.assertGreater(len(home_default), 1, 'get default') + assert len(home_default) > 1, 'get default' self.conf({"HOME": "/"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'HOME', 'Connection': 'close', } - )['body'], - '/,', - 'replace default', - ) + )['body'] + == '/,' + ), 'replace default' self.conf({}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'HOME', 'Connection': 'close', } - )['body'], - home_default, - 'restore default', - ) - - -if __name__ == '__main__': - TestPythonEnvironment.main() + )['body'] + == home_default + ), 'restore default' diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 1bed64ba..564ec79c 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -1,4 +1,4 @@ -import unittest +import pytest from unit.applications.lang.python import TestApplicationPython from unit.feature.isolation import TestFeatureIsolation @@ -10,70 +10,58 @@ class TestPythonIsolation(TestApplicationPython): isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() - def test_python_isolation_rootfs(self): + def test_python_isolation_rootfs(self, is_su): isolation_features = self.available['features']['isolation'].keys() if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') isolation = { - 'namespaces': {'credential': not self.is_su, 'mount': True}, - 'rootfs': self.testdir, + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': self.temp_dir, } self.load('empty', isolation=isolation) - self.assertEqual(self.get()['status'], 200, 'python rootfs') + assert self.get()['status'] == 200, 'python rootfs' self.load('ns_inspect', isolation=isolation) - self.assertEqual( - self.getjson(url='/?path=' + self.testdir)['body']['FileExists'], - False, - 'testdir does not exists in rootfs', - ) - - self.assertEqual( - self.getjson(url='/?path=/proc/self')['body']['FileExists'], - False, - 'no /proc/self', - ) - - self.assertEqual( - self.getjson(url='/?path=/dev/pts')['body']['FileExists'], - False, - 'no /dev/pts', - ) - - self.assertEqual( - self.getjson(url='/?path=/sys/kernel')['body']['FileExists'], - False, - 'no /sys/kernel', - ) + assert ( + self.getjson(url='/?path=' + self.temp_dir)['body']['FileExists'] + == False + ), 'temp_dir does not exists in rootfs' - ret = self.getjson(url='/?path=/app/python/ns_inspect') + assert ( + self.getjson(url='/?path=/proc/self')['body']['FileExists'] + == False + ), 'no /proc/self' + + assert ( + self.getjson(url='/?path=/dev/pts')['body']['FileExists'] == False + ), 'no /dev/pts' - self.assertEqual( - ret['body']['FileExists'], True, 'application exists in rootfs', - ) + assert ( + self.getjson(url='/?path=/sys/kernel')['body']['FileExists'] + == False + ), 'no /sys/kernel' + ret = self.getjson(url='/?path=/app/python/ns_inspect') -if __name__ == '__main__': - TestPythonIsolation.main() + assert ( + ret['body']['FileExists'] == True + ), 'application exists in rootfs' diff --git a/test/test_python_isolation_chroot.py b/test/test_python_isolation_chroot.py index 7761128e..315fee9f 100644 --- a/test/test_python_isolation_chroot.py +++ b/test/test_python_isolation_chroot.py @@ -1,4 +1,4 @@ -import unittest +import pytest from unit.applications.lang.python import TestApplicationPython from unit.feature.isolation import TestFeatureIsolation @@ -7,51 +7,41 @@ from unit.feature.isolation import TestFeatureIsolation class TestPythonIsolation(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def test_python_isolation_chroot(self): - if not self.is_su: - print('requires root') - raise unittest.SkipTest() + def test_python_isolation_chroot(self, is_su): + if not is_su: + pytest.skip('requires root') isolation = { - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('empty', isolation=isolation) - self.assertEqual(self.get()['status'], 200, 'python chroot') + assert self.get()['status'] == 200, 'python chroot' self.load('ns_inspect', isolation=isolation) - self.assertEqual( - self.getjson(url='/?path=' + self.testdir)['body']['FileExists'], - False, - 'testdir does not exists in rootfs', - ) - - self.assertEqual( - self.getjson(url='/?path=/proc/self')['body']['FileExists'], - False, - 'no /proc/self', - ) - - self.assertEqual( - self.getjson(url='/?path=/dev/pts')['body']['FileExists'], - False, - 'no /dev/pts', - ) - - self.assertEqual( - self.getjson(url='/?path=/sys/kernel')['body']['FileExists'], - False, - 'no /sys/kernel', - ) + assert ( + self.getjson(url='/?path=' + self.temp_dir)['body']['FileExists'] + == False + ), 'temp_dir does not exists in rootfs' - ret = self.getjson(url='/?path=/app/python/ns_inspect') + assert ( + self.getjson(url='/?path=/proc/self')['body']['FileExists'] + == False + ), 'no /proc/self' + + assert ( + self.getjson(url='/?path=/dev/pts')['body']['FileExists'] == False + ), 'no /dev/pts' - self.assertEqual( - ret['body']['FileExists'], True, 'application exists in rootfs', - ) + assert ( + self.getjson(url='/?path=/sys/kernel')['body']['FileExists'] + == False + ), 'no /sys/kernel' + ret = self.getjson(url='/?path=/app/python/ns_inspect') -if __name__ == '__main__': - TestPythonIsolation.main() + assert ( + ret['body']['FileExists'] == True + ), 'application exists in rootfs' diff --git a/test/test_python_procman.py b/test/test_python_procman.py index c327ab14..7e727fa8 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -1,7 +1,7 @@ +import pytest import re import subprocess import time -import unittest from unit.applications.lang.python import TestApplicationPython @@ -9,10 +9,10 @@ from unit.applications.lang.python import TestApplicationPython class TestPythonProcman(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.app_name = "app-" + self.testdir.split('/')[-1] + self.app_name = "app-" + self.temp_dir.split('/')[-1] self.app_proc = 'applications/' + self.app_name + '/processes' self.load('empty', self.app_name) @@ -23,7 +23,7 @@ class TestPythonProcman(TestApplicationPython): pids = set() for m in re.findall('.*' + self.app_name, output.decode()): - pids.add(re.search('^\s*(\d+)', m).group(1)) + pids.add(re.search(r'^\s*(\d+)', m).group(1)) return pids @@ -31,35 +31,35 @@ class TestPythonProcman(TestApplicationPython): if path is None: path = self.app_proc - self.assertIn('success', self.conf(conf, path), 'configure processes') + assert 'success' in self.conf(conf, path), 'configure processes' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_processes_idle_timeout_zero(self): self.conf_proc({"spare": 0, "max": 2, "idle_timeout": 0}) self.get() - self.assertEqual(len(self.pids_for_process()), 0, 'idle timeout 0') + assert len(self.pids_for_process()) == 0, 'idle timeout 0' def test_python_prefork(self): self.conf_proc('2') pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'prefork 2') + assert len(pids) == 2, 'prefork 2' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'prefork still 2') + assert self.pids_for_process() == pids, 'prefork still 2' self.conf_proc('4') pids = self.pids_for_process() - self.assertEqual(len(pids), 4, 'prefork 4') + assert len(pids) == 4, 'prefork 4' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'prefork still 4') + assert self.pids_for_process() == pids, 'prefork still 4' self.stop_all() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_prefork_same_processes(self): self.conf_proc('2') pids = self.pids_for_process() @@ -67,25 +67,23 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc('4') pids_new = self.pids_for_process() - self.assertTrue(pids.issubset(pids_new), 'prefork same processes') + assert pids.issubset(pids_new), 'prefork same processes' def test_python_ondemand(self): self.conf_proc({"spare": 0, "max": 8, "idle_timeout": 1}) - self.assertEqual(len(self.pids_for_process()), 0, 'on-demand 0') + assert len(self.pids_for_process()) == 0, 'on-demand 0' self.get() pids = self.pids_for_process() - self.assertEqual(len(pids), 1, 'on-demand 1') + assert len(pids) == 1, 'on-demand 1' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'on-demand still 1') + assert self.pids_for_process() == pids, 'on-demand still 1' time.sleep(1) - self.assertEqual( - len(self.pids_for_process()), 0, 'on-demand stop idle' - ) + assert len(self.pids_for_process()) == 0, 'on-demand stop idle' self.stop_all() @@ -93,27 +91,25 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc({"spare": 2, "max": 8, "idle_timeout": 1}) pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'updown 2') + assert len(pids) == 2, 'updown 2' self.get() pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 3, 'updown 3') - self.assertTrue(pids.issubset(pids_new), 'updown 3 only 1 new') + assert len(pids_new) == 3, 'updown 3' + assert pids.issubset(pids_new), 'updown 3 only 1 new' self.get() - self.assertSetEqual( - self.pids_for_process(), pids_new, 'updown still 3' - ) + assert self.pids_for_process() == pids_new, 'updown still 3' time.sleep(1) pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'updown stop idle') + assert len(pids) == 2, 'updown stop idle' self.get() pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 3, 'updown again 3') - self.assertTrue(pids.issubset(pids_new), 'updown again 3 only 1 new') + assert len(pids_new) == 3, 'updown again 3' + assert pids.issubset(pids_new), 'updown again 3 only 1 new' self.stop_all() @@ -121,20 +117,20 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc({"spare": 2, "max": 6, "idle_timeout": 1}) pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'reconf 2') + assert len(pids) == 2, 'reconf 2' self.get() pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 3, 'reconf 3') - self.assertTrue(pids.issubset(pids_new), 'reconf 3 only 1 new') + assert len(pids_new) == 3, 'reconf 3' + assert pids.issubset(pids_new), 'reconf 3 only 1 new' self.conf_proc('6', self.app_proc + '/spare') pids = self.pids_for_process() - self.assertEqual(len(pids), 6, 'reconf 6') + assert len(pids) == 6, 'reconf 6' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'reconf still 6') + assert self.pids_for_process() == pids, 'reconf still 6' self.stop_all() @@ -143,7 +139,7 @@ class TestPythonProcman(TestApplicationPython): self.get() pids = self.pids_for_process() - self.assertEqual(len(pids), 1, 'idle timeout 1') + assert len(pids) == 1, 'idle timeout 1' time.sleep(1) @@ -152,14 +148,12 @@ class TestPythonProcman(TestApplicationPython): time.sleep(1) pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 1, 'idle timeout still 1') - self.assertSetEqual( - self.pids_for_process(), pids, 'idle timeout still 1 same pid' - ) + assert len(pids_new) == 1, 'idle timeout still 1' + assert self.pids_for_process() == pids, 'idle timeout still 1 same pid' time.sleep(1) - self.assertEqual(len(self.pids_for_process()), 0, 'idle timed out') + assert len(self.pids_for_process()) == 0, 'idle timed out' def test_python_processes_connection_keepalive(self): self.conf_proc({"spare": 0, "max": 6, "idle_timeout": 2}) @@ -169,15 +163,11 @@ class TestPythonProcman(TestApplicationPython): start=True, read_timeout=1, ) - self.assertEqual( - len(self.pids_for_process()), 1, 'keepalive connection 1' - ) + assert len(self.pids_for_process()) == 1, 'keepalive connection 1' time.sleep(2) - self.assertEqual( - len(self.pids_for_process()), 0, 'keepalive connection 0' - ) + assert len(self.pids_for_process()) == 0, 'keepalive connection 0' sock.close() @@ -185,43 +175,29 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc('1') path = '/' + self.app_proc - self.assertIn('error', self.conf_get(path + '/max')) - self.assertIn('error', self.conf_get(path + '/spare')) - self.assertIn('error', self.conf_get(path + '/idle_timeout')) + assert 'error' in self.conf_get(path + '/max') + assert 'error' in self.conf_get(path + '/spare') + assert 'error' in self.conf_get(path + '/idle_timeout') def test_python_processes_invalid(self): - self.assertIn( - 'error', self.conf({"spare": -1}, self.app_proc), 'negative spare', - ) - self.assertIn( - 'error', self.conf({"max": -1}, self.app_proc), 'negative max', - ) - self.assertIn( - 'error', - self.conf({"idle_timeout": -1}, self.app_proc), - 'negative idle_timeout', - ) - self.assertIn( - 'error', - self.conf({"spare": 2}, self.app_proc), - 'spare gt max default', - ) - self.assertIn( - 'error', - self.conf({"spare": 2, "max": 1}, self.app_proc), - 'spare gt max', - ) - self.assertIn( - 'error', - self.conf({"spare": 0, "max": 0}, self.app_proc), - 'max zero', - ) + assert 'error' in self.conf( + {"spare": -1}, self.app_proc + ), 'negative spare' + assert 'error' in self.conf({"max": -1}, self.app_proc), 'negative max' + assert 'error' in self.conf( + {"idle_timeout": -1}, self.app_proc + ), 'negative idle_timeout' + assert 'error' in self.conf( + {"spare": 2}, self.app_proc + ), 'spare gt max default' + assert 'error' in self.conf( + {"spare": 2, "max": 1}, self.app_proc + ), 'spare gt max' + assert 'error' in self.conf( + {"spare": 0, "max": 0}, self.app_proc + ), 'max zero' def stop_all(self): self.conf({"listeners": {}, "applications": {}}) - self.assertEqual(len(self.pids_for_process()), 0, 'stop all') - - -if __name__ == '__main__': - TestPythonProcman.main() + assert len(self.pids_for_process()) == 0, 'stop all' diff --git a/test/test_respawn.py b/test/test_respawn.py index f1c71a20..d40e78a4 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -3,6 +3,7 @@ import subprocess import time from unit.applications.lang.python import TestApplicationPython +from conftest import skip_alert class TestRespawn(TestApplicationPython): @@ -11,21 +12,20 @@ class TestRespawn(TestApplicationPython): PATTERN_ROUTER = 'unit: router' PATTERN_CONTROLLER = 'unit: controller' - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.app_name = "app-" + self.testdir.split('/')[-1] + self.app_name = "app-" + self.temp_dir.split('/')[-1] self.load('empty', self.app_name) - self.assertIn( - 'success', - self.conf('1', 'applications/' + self.app_name + '/processes') + assert 'success' in self.conf( + '1', 'applications/' + self.app_name + '/processes' ) def pid_by_name(self, name): output = subprocess.check_output(['ps', 'ax']).decode() - m = re.search('\s*(\d+).*' + name, output) + m = re.search(r'\s*(\d+).*' + name, output) return m if m is None else m.group(1) def kill_pids(self, *pids): @@ -44,27 +44,26 @@ class TestRespawn(TestApplicationPython): def smoke_test(self): for _ in range(5): - self.assertIn( - 'success', - self.conf('1', 'applications/' + self.app_name + '/processes') + assert 'success' in self.conf( + '1', 'applications/' + self.app_name + '/processes' ) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 # Check if the only one router, controller, # and application processes running. output = subprocess.check_output(['ps', 'ax']).decode() - self.assertEqual(len(re.findall(self.PATTERN_ROUTER, output)), 1) - self.assertEqual(len(re.findall(self.PATTERN_CONTROLLER, output)), 1) - self.assertEqual(len(re.findall(self.app_name, output)), 1) + assert len(re.findall(self.PATTERN_ROUTER, output)) == 1 + assert len(re.findall(self.PATTERN_CONTROLLER, output)) == 1 + assert len(re.findall(self.app_name, output)) == 1 def test_respawn_router(self): pid = self.pid_by_name(self.PATTERN_ROUTER) self.kill_pids(pid) - self.skip_alerts.append(r'process %s exited on signal 9' % pid) + skip_alert(r'process %s exited on signal 9' % pid) - self.assertIsNotNone(self.wait_for_process(self.PATTERN_ROUTER)) + assert self.wait_for_process(self.PATTERN_ROUTER) is not None self.smoke_test() @@ -72,11 +71,11 @@ class TestRespawn(TestApplicationPython): pid = self.pid_by_name(self.PATTERN_CONTROLLER) self.kill_pids(pid) - self.skip_alerts.append(r'process %s exited on signal 9' % pid) + skip_alert(r'process %s exited on signal 9' % pid) - self.assertIsNotNone(self.wait_for_process(self.PATTERN_CONTROLLER)) + assert self.wait_for_process(self.PATTERN_CONTROLLER) is not None - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 self.smoke_test() @@ -84,12 +83,8 @@ class TestRespawn(TestApplicationPython): pid = self.pid_by_name(self.app_name) self.kill_pids(pid) - self.skip_alerts.append(r'process %s exited on signal 9' % pid) + skip_alert(r'process %s exited on signal 9' % pid) - self.assertIsNotNone(self.wait_for_process(self.app_name)) + assert self.wait_for_process(self.app_name) is not None self.smoke_test() - - -if __name__ == '__main__': - TestRespawn.main() diff --git a/test/test_return.py b/test/test_return.py index a89d97e6..64050022 100644 --- a/test/test_return.py +++ b/test/test_return.py @@ -6,8 +6,8 @@ from unit.applications.proto import TestApplicationProto class TestReturn(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() self._load_conf( { @@ -35,59 +35,61 @@ Connection: close def test_return(self): resp = self.get() - self.assertEqual(resp['status'], 200) - self.assertIn('Server', resp['headers']) - self.assertIn('Date', resp['headers']) - self.assertEqual(resp['headers']['Content-Length'], '0') - self.assertEqual(resp['headers']['Connection'], 'close') - self.assertEqual(resp['body'], '', 'body') + assert resp['status'] == 200 + assert 'Server' in resp['headers'] + assert 'Date' in resp['headers'] + assert resp['headers']['Content-Length'] == '0' + assert resp['headers']['Connection'] == 'close' + assert resp['body'] == '', 'body' resp = self.post(body='blah') - self.assertEqual(resp['status'], 200) - self.assertEqual(resp['body'], '', 'body') + assert resp['status'] == 200 + assert resp['body'] == '', 'body' resp = self.get_resps_sc() - self.assertEqual(len(re.findall('200 OK', resp)), 10) - self.assertEqual(len(re.findall('Connection:', resp)), 1) - self.assertEqual(len(re.findall('Connection: close', resp)), 1) + assert len(re.findall('200 OK', resp)) == 10 + assert len(re.findall('Connection:', resp)) == 1 + assert len(re.findall('Connection: close', resp)) == 1 resp = self.get(http_10=True) - self.assertEqual(resp['status'], 200) - self.assertIn('Server', resp['headers']) - self.assertIn('Date', resp['headers']) - self.assertEqual(resp['headers']['Content-Length'], '0') - self.assertNotIn('Connection', resp['headers']) - self.assertEqual(resp['body'], '', 'body') + assert resp['status'] == 200 + assert 'Server' in resp['headers'] + assert 'Date' in resp['headers'] + assert resp['headers']['Content-Length'] == '0' + assert 'Connection' not in resp['headers'] + assert resp['body'] == '', 'body' def test_return_update(self): - self.assertIn('success', self.conf('0', 'routes/0/action/return')) + assert 'success' in self.conf('0', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 0) - self.assertEqual(resp['body'], '') + assert resp['status'] == 0 + assert resp['body'] == '' - self.assertIn('success', self.conf('404', 'routes/0/action/return')) + assert 'success' in self.conf('404', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 404) - self.assertNotEqual(resp['body'], '') + assert resp['status'] == 404 + assert resp['body'] != '' - self.assertIn('success', self.conf('598', 'routes/0/action/return')) + assert 'success' in self.conf('598', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 598) - self.assertNotEqual(resp['body'], '') + assert resp['status'] == 598 + assert resp['body'] != '' - self.assertIn('success', self.conf('999', 'routes/0/action/return')) + assert 'success' in self.conf('999', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 999) - self.assertEqual(resp['body'], '') + assert resp['status'] == 999 + assert resp['body'] == '' def test_return_location(self): reserved = ":/?#[]@!$&'()*+,;=" - unreserved = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - "0123456789-._~") + unreserved = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~" + ) unsafe = " \"%<>\\^`{|}" unsafe_enc = "%20%22%25%3C%3E%5C%5E%60%7B%7C%7D" @@ -95,15 +97,11 @@ Connection: close if expect is None: expect = location - self.assertIn( - 'success', - self.conf( - {"return": 301, "location": location}, 'routes/0/action' - ), - 'configure location' - ) + assert 'success' in self.conf( + {"return": 301, "location": location}, 'routes/0/action' + ), 'configure location' - self.assertEqual(self.get()['headers']['Location'], expect) + assert self.get()['headers']['Location'] == expect # FAIL: can't specify empty header value. # check_location("") @@ -145,39 +143,29 @@ Connection: close check_location("/%20?%20#%20 ", "/%2520?%2520#%2520%20") def test_return_location_edit(self): - self.assertIn( - 'success', - self.conf( - {"return": 302, "location": "blah"}, 'routes/0/action' - ), - 'configure init location' - ) - self.assertEqual(self.get()['headers']['Location'], 'blah') - - self.assertIn( - 'success', - self.conf_delete('routes/0/action/location'), - 'location delete' - ) - self.assertNotIn('Location', self.get()['headers']) - - self.assertIn( - 'success', - self.conf('"blah"', 'routes/0/action/location'), - 'location restore' - ) - self.assertEqual(self.get()['headers']['Location'], 'blah') - - self.assertIn( - 'error', - self.conf_post('"blah"', 'routes/0/action/location'), - 'location method not allowed' - ) - self.assertEqual(self.get()['headers']['Location'], 'blah') + assert 'success' in self.conf( + {"return": 302, "location": "blah"}, 'routes/0/action' + ), 'configure init location' + assert self.get()['headers']['Location'] == 'blah' + + assert 'success' in self.conf_delete( + 'routes/0/action/location' + ), 'location delete' + assert 'Location' not in self.get()['headers'] + + assert 'success' in self.conf( + '"blah"', 'routes/0/action/location' + ), 'location restore' + assert self.get()['headers']['Location'] == 'blah' + + assert 'error' in self.conf_post( + '"blah"', 'routes/0/action/location' + ), 'location method not allowed' + assert self.get()['headers']['Location'] == 'blah' def test_return_invalid(self): def check_error(conf): - self.assertIn('error', self.conf(conf, 'routes/0/action')) + assert 'error' in self.conf(conf, 'routes/0/action') check_error({"return": "200"}) check_error({"return": []}) @@ -186,13 +174,9 @@ Connection: close check_error({"return": -1}) check_error({"return": 200, "share": "/blah"}) - self.assertIn( - 'error', self.conf('001', 'routes/0/action/return'), 'leading zero' - ) + assert 'error' in self.conf( + '001', 'routes/0/action/return' + ), 'leading zero' check_error({"return": 301, "location": 0}) check_error({"return": 301, "location": []}) - - -if __name__ == '__main__': - TestReturn.main() diff --git a/test/test_routing.py b/test/test_routing.py index 269e8efc..4107f57e 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1,117 +1,103 @@ # -*- coding: utf-8 -*- -import unittest +import pytest from unit.applications.proto import TestApplicationProto +from conftest import option, skip_alert class TestRouting(TestApplicationProto): prerequisites = {'modules': {'python': 'any'}} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [ - { - "match": {"method": "GET"}, - "action": {"return": 200}, - } - ], - "applications": {}, - } - ), - 'routing configure', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + {"match": {"method": "GET"}, "action": {"return": 200},} + ], + "applications": {}, + } + ), 'routing configure' def route(self, route): return self.conf([route], 'routes') def route_match(self, match): - self.assertIn( - 'success', - self.route({"match": match, "action": {"return": 200}}), - 'route match configure', - ) + assert 'success' in self.route( + {"match": match, "action": {"return": 200}} + ), 'route match configure' def route_match_invalid(self, match): - self.assertIn( - 'error', - self.route({"match": match, "action": {"return": 200}}), - 'route match configure invalid', - ) + assert 'error' in self.route( + {"match": match, "action": {"return": 200}} + ), 'route match configure invalid' def host(self, host, status): - self.assertEqual( - self.get(headers={'Host': host, 'Connection': 'close'})[ - 'status' - ], - status, - 'match host', - ) + assert ( + self.get(headers={'Host': host, 'Connection': 'close'})['status'] + == status + ), 'match host' def cookie(self, cookie, status): - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Cookie': cookie, 'Connection': 'close', }, - )['status'], - status, - 'match cookie', - ) + )['status'] + == status + ), 'match cookie' def test_routes_match_method_positive(self): - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_positive_many(self): self.route_match({"method": ["GET", "POST"]}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 200, 'POST') - self.assertEqual(self.delete()['status'], 404, 'DELETE') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 200, 'POST' + assert self.delete()['status'] == 404, 'DELETE' def test_routes_match_method_negative(self): self.route_match({"method": "!GET"}) - self.assertEqual(self.get()['status'], 404, 'GET') - self.assertEqual(self.post()['status'], 200, 'POST') + assert self.get()['status'] == 404, 'GET' + assert self.post()['status'] == 200, 'POST' def test_routes_match_method_negative_many(self): self.route_match({"method": ["!GET", "!POST"]}) - self.assertEqual(self.get()['status'], 404, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') - self.assertEqual(self.delete()['status'], 200, 'DELETE') + assert self.get()['status'] == 404, 'GET' + assert self.post()['status'] == 404, 'POST' + assert self.delete()['status'] == 200, 'DELETE' def test_routes_match_method_wildcard_left(self): self.route_match({"method": "*ET"}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_wildcard_right(self): self.route_match({"method": "GE*"}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_wildcard_left_right(self): self.route_match({"method": "*GET*"}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_wildcard(self): self.route_match({"method": "*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_invalid(self): self.route_match_invalid({"method": "**"}) @@ -124,32 +110,32 @@ class TestRouting(TestApplicationProto): def test_routes_match_empty_exact(self): self.route_match({"uri": ""}) - self.assertEqual(self.get()['status'], 404) + assert self.get()['status'] == 404 self.route_match({"uri": "/"}) - self.assertEqual(self.get()['status'], 200) - self.assertEqual(self.get(url='/blah')['status'], 404) + assert self.get()['status'] == 200 + assert self.get(url='/blah')['status'] == 404 def test_routes_match_negative(self): self.route_match({"uri": "!"}) - self.assertEqual(self.get()['status'], 404) + assert self.get()['status'] == 404 self.route_match({"uri": "!/"}) - self.assertEqual(self.get()['status'], 404) - self.assertEqual(self.get(url='/blah')['status'], 200) + assert self.get()['status'] == 404 + assert self.get(url='/blah')['status'] == 200 self.route_match({"uri": "!*blah"}) - self.assertEqual(self.get()['status'], 200) - self.assertEqual(self.get(url='/bla')['status'], 200) - self.assertEqual(self.get(url='/blah')['status'], 404) - self.assertEqual(self.get(url='/blah1')['status'], 200) + assert self.get()['status'] == 200 + assert self.get(url='/bla')['status'] == 200 + assert self.get(url='/blah')['status'] == 404 + assert self.get(url='/blah1')['status'] == 200 self.route_match({"uri": "!/blah*1*"}) - self.assertEqual(self.get()['status'], 200) - self.assertEqual(self.get(url='/blah')['status'], 200) - self.assertEqual(self.get(url='/blah1')['status'], 404) - self.assertEqual(self.get(url='/blah12')['status'], 404) - self.assertEqual(self.get(url='/blah2')['status'], 200) + assert self.get()['status'] == 200 + assert self.get(url='/blah')['status'] == 200 + assert self.get(url='/blah1')['status'] == 404 + assert self.get(url='/blah12')['status'] == 404 + assert self.get(url='/blah2')['status'] == 200 def test_routes_match_wildcard_middle(self): self.route_match({"host": "ex*le"}) @@ -162,110 +148,105 @@ class TestRouting(TestApplicationProto): def test_routes_match_method_case_insensitive(self): self.route_match({"method": "get"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_left_case_insensitive(self): self.route_match({"method": "*get"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' self.route_match({"method": "*et"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_middle_case_insensitive(self): self.route_match({"method": "g*t"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_right_case_insensitive(self): self.route_match({"method": "get*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' self.route_match({"method": "ge*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_substring_case_insensitive(self): self.route_match({"method": "*et*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_left_case_sensitive(self): self.route_match({"uri": "*blah"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_routes_match_wildcard_middle_case_sensitive(self): self.route_match({"uri": "/b*h"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_route_match_wildcards_ordered(self): self.route_match({"uri": "/a*x*y*"}) - self.assertEqual(self.get(url='/axy')['status'], 200, '/axy') - self.assertEqual(self.get(url='/ayx')['status'], 404, '/ayx') + assert self.get(url='/axy')['status'] == 200, '/axy' + assert self.get(url='/ayx')['status'] == 404, '/ayx' def test_route_match_wildcards_adjust_start(self): self.route_match({"uri": "/bla*bla*"}) - self.assertEqual(self.get(url='/bla_foo')['status'], 404, '/bla_foo') + assert self.get(url='/bla_foo')['status'] == 404, '/bla_foo' def test_route_match_wildcards_adjust_start_substr(self): self.route_match({"uri": "*bla*bla*"}) - self.assertEqual(self.get(url='/bla_foo')['status'], 404, '/bla_foo') + assert self.get(url='/bla_foo')['status'] == 404, '/bla_foo' def test_route_match_wildcards_adjust_end(self): self.route_match({"uri": "/bla*bla"}) - self.assertEqual(self.get(url='/foo_bla')['status'], 404, '/foo_bla') + assert self.get(url='/foo_bla')['status'] == 404, '/foo_bla' def test_routes_match_wildcard_right_case_sensitive(self): self.route_match({"uri": "/bla*"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_routes_match_wildcard_substring_case_sensitive(self): self.route_match({"uri": "*bla*"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_routes_match_many_wildcard_substrings_case_sensitive(self): self.route_match({"uri": "*a*B*c*"}) - self.assertEqual(self.get(url='/blah-a-B-c-blah')['status'], 200) - self.assertEqual(self.get(url='/a-B-c')['status'], 200) - self.assertEqual(self.get(url='/aBc')['status'], 200) - self.assertEqual(self.get(url='/aBCaBbc')['status'], 200) - self.assertEqual(self.get(url='/ABc')['status'], 404) + assert self.get(url='/blah-a-B-c-blah')['status'] == 200 + assert self.get(url='/a-B-c')['status'] == 200 + assert self.get(url='/aBc')['status'] == 200 + assert self.get(url='/aBCaBbc')['status'] == 200 + assert self.get(url='/ABc')['status'] == 404 def test_routes_pass_encode(self): def check_pass(path, name): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "applications/" + path} - }, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + '/python/empty', - "working_directory": self.current_dir - + '/python/empty', - "module": "wsgi", - } - }, - } - ), + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/" + path}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + '/python/empty', + "working_directory": option.test_dir + + '/python/empty', + "module": "wsgi", + } + }, + } ) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 check_pass("%25", "%") check_pass("blah%2Fblah", "blah/blah") @@ -273,25 +254,20 @@ class TestRouting(TestApplicationProto): check_pass("%20blah%252Fblah%7E", " blah%2Fblah~") def check_pass_error(path, name): - self.assertIn( - 'error', - self.conf( - { - "listeners": { - "*:7080": {"pass": "applications/" + path} - }, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + '/python/empty', - "working_directory": self.current_dir - + '/python/empty', - "module": "wsgi", - } - }, - } - ), + assert 'error' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/" + path}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + '/python/empty', + "working_directory": option.test_dir + + '/python/empty', + "module": "wsgi", + } + }, + } ) check_pass_error("%", "%") @@ -305,8 +281,8 @@ class TestRouting(TestApplicationProto): "empty": { "type": "python", "processes": {"spare": 0}, - "path": self.current_dir + '/python/empty', - "working_directory": self.current_dir + "path": option.test_dir + '/python/empty', + "working_directory": option.test_dir + '/python/empty', "module": "wsgi", } @@ -314,179 +290,135 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get(port=7081)['status'], 200, 'routes absent') + assert self.get(port=7081)['status'] == 200, 'routes absent' def test_routes_pass_invalid(self): - self.assertIn( - 'error', - self.conf({"pass": "routes/blah"}, 'listeners/*:7080'), - 'routes invalid', - ) + assert 'error' in self.conf( + {"pass": "routes/blah"}, 'listeners/*:7080' + ), 'routes invalid' def test_route_empty(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/main"}}, - "routes": {"main": []}, - "applications": {}, - } - ), - 'route empty configure', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/main"}}, + "routes": {"main": []}, + "applications": {}, + } + ), 'route empty configure' - self.assertEqual(self.get()['status'], 404, 'route empty') + assert self.get()['status'] == 404, 'route empty' def test_routes_route_empty(self): - self.assertIn( - 'success', - self.conf({}, 'listeners'), - 'routes empty listeners configure', - ) + assert 'success' in self.conf( + {}, 'listeners' + ), 'routes empty listeners configure' - self.assertIn( - 'success', self.conf({}, 'routes'), 'routes empty configure' - ) + assert 'success' in self.conf({}, 'routes'), 'routes empty configure' def test_routes_route_match_absent(self): - self.assertIn( - 'success', - self.conf([{"action": {"return": 200}}], 'routes'), - 'route match absent configure', - ) + assert 'success' in self.conf( + [{"action": {"return": 200}}], 'routes' + ), 'route match absent configure' - self.assertEqual(self.get()['status'], 200, 'route match absent') + assert self.get()['status'] == 200, 'route match absent' def test_routes_route_action_absent(self): - self.skip_alerts.append(r'failed to apply new conf') + skip_alert(r'failed to apply new conf') - self.assertIn( - 'error', - self.conf([{"match": {"method": "GET"}}], 'routes'), - 'route pass absent configure', - ) + assert 'error' in self.conf( + [{"match": {"method": "GET"}}], 'routes' + ), 'route pass absent configure' def test_routes_route_pass_absent(self): - self.assertIn( - 'error', - self.conf([{"match": {"method": "GET"}, "action": {}}], 'routes'), - 'route pass absent configure', - ) + assert 'error' in self.conf( + [{"match": {"method": "GET"}, "action": {}}], 'routes' + ), 'route pass absent configure' def test_routes_action_unique(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": {"pass": "applications/app"}, - }, - "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - ) - - self.assertIn( - 'error', - self.conf( - {"proxy": "http://127.0.0.1:7081", "share": self.testdir}, - 'routes/0/action', - ), - 'proxy share', - ) - self.assertIn( - 'error', - self.conf( - { - "proxy": "http://127.0.0.1:7081", - "pass": "applications/app", + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "applications/app"}, }, - 'routes/0/action', - ), - 'proxy pass', - ) - self.assertIn( - 'error', - self.conf( - {"share": self.testdir, "pass": "applications/app"}, - 'routes/0/action', - ), - 'share pass', + "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } ) + assert 'error' in self.conf( + {"proxy": "http://127.0.0.1:7081", "share": self.temp_dir}, + 'routes/0/action', + ), 'proxy share' + assert 'error' in self.conf( + {"proxy": "http://127.0.0.1:7081", "pass": "applications/app",}, + 'routes/0/action', + ), 'proxy pass' + assert 'error' in self.conf( + {"share": self.temp_dir, "pass": "applications/app"}, + 'routes/0/action', + ), 'share pass' + def test_routes_rules_two(self): - self.assertIn( - 'success', - self.conf( - [ - {"match": {"method": "GET"}, "action": {"return": 200}}, - {"match": {"method": "POST"}, "action": {"return": 201}}, - ], - 'routes', - ), - 'rules two configure', - ) + assert 'success' in self.conf( + [ + {"match": {"method": "GET"}, "action": {"return": 200}}, + {"match": {"method": "POST"}, "action": {"return": 201}}, + ], + 'routes', + ), 'rules two configure' - self.assertEqual(self.get()['status'], 200, 'rules two match first') - self.assertEqual(self.post()['status'], 201, 'rules two match second') + assert self.get()['status'] == 200, 'rules two match first' + assert self.post()['status'] == 201, 'rules two match second' def test_routes_two(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/first"}}, - "routes": { - "first": [ - { - "match": {"method": "GET"}, - "action": {"pass": "routes/second"}, - } - ], - "second": [ - { - "match": {"host": "localhost"}, - "action": {"return": 200}, - } - ], - }, - "applications": {}, - } - ), - 'routes two configure', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/first"}}, + "routes": { + "first": [ + { + "match": {"method": "GET"}, + "action": {"pass": "routes/second"}, + } + ], + "second": [ + { + "match": {"host": "localhost"}, + "action": {"return": 200}, + } + ], + }, + "applications": {}, + } + ), 'routes two configure' - self.assertEqual(self.get()['status'], 200, 'routes two') + assert self.get()['status'] == 200, 'routes two' def test_routes_match_host_positive(self): self.route_match({"host": "localhost"}) - self.assertEqual(self.get()['status'], 200, 'localhost') + assert self.get()['status'] == 200, 'localhost' self.host('localhost.', 200) self.host('localhost.', 200) self.host('.localhost', 404) self.host('www.localhost', 404) self.host('localhost1', 404) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_routes_match_host_absent(self): self.route_match({"host": "localhost"}) - self.assertEqual( - self.get(headers={'Connection': 'close'})['status'], - 400, - 'match host absent', - ) + assert ( + self.get(headers={'Connection': 'close'})['status'] == 400 + ), 'match host absent' def test_routes_match_host_ipv4(self): self.route_match({"host": "127.0.0.1"}) @@ -503,13 +435,13 @@ class TestRouting(TestApplicationProto): def test_routes_match_host_positive_many(self): self.route_match({"host": ["localhost", "example.com"]}) - self.assertEqual(self.get()['status'], 200, 'localhost') + assert self.get()['status'] == 200, 'localhost' self.host('example.com', 200) def test_routes_match_host_positive_and_negative(self): self.route_match({"host": ["*example.com", "!www.example.com"]}) - self.assertEqual(self.get()['status'], 404, 'localhost') + assert self.get()['status'] == 404, 'localhost' self.host('example.com', 200) self.host('www.example.com', 404) self.host('!www.example.com', 200) @@ -535,380 +467,278 @@ class TestRouting(TestApplicationProto): self.route_match({"host": ""}) self.host('', 200) - self.assertEqual( - self.get(http_10=True, headers={})['status'], - 200, - 'match host empty 2', - ) - self.assertEqual(self.get()['status'], 404, 'match host empty 3') + assert ( + self.get(http_10=True, headers={})['status'] == 200 + ), 'match host empty 2' + assert self.get()['status'] == 404, 'match host empty 3' def test_routes_match_uri_positive(self): self.route_match({"uri": ["/blah", "/slash/"]}) - self.assertEqual(self.get()['status'], 404, '/') - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/blah#foo')['status'], 200, '/blah#foo') - self.assertEqual(self.get(url='/blah?var')['status'], 200, '/blah?var') - self.assertEqual(self.get(url='//blah')['status'], 200, '//blah') - self.assertEqual( - self.get(url='/slash/foo/../')['status'], 200, 'relative' - ) - self.assertEqual(self.get(url='/slash/./')['status'], 200, '/slash/./') - self.assertEqual( - self.get(url='/slash//.//')['status'], 200, 'adjacent slashes' - ) - self.assertEqual(self.get(url='/%')['status'], 400, 'percent') - self.assertEqual(self.get(url='/%1')['status'], 400, 'percent digit') - self.assertEqual(self.get(url='/%A')['status'], 400, 'percent letter') - self.assertEqual( - self.get(url='/slash/.?args')['status'], 200, 'dot args' - ) - self.assertEqual( - self.get(url='/slash/.#frag')['status'], 200, 'dot frag' - ) - self.assertEqual( - self.get(url='/slash/foo/..?args')['status'], - 200, - 'dot dot args', - ) - self.assertEqual( - self.get(url='/slash/foo/..#frag')['status'], - 200, - 'dot dot frag', - ) - self.assertEqual( - self.get(url='/slash/.')['status'], 200, 'trailing dot' - ) - self.assertEqual( - self.get(url='/slash/foo/..')['status'], - 200, - 'trailing dot dot', - ) + assert self.get()['status'] == 404, '/' + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/blah#foo')['status'] == 200, '/blah#foo' + assert self.get(url='/blah?var')['status'] == 200, '/blah?var' + assert self.get(url='//blah')['status'] == 200, '//blah' + assert self.get(url='/slash/foo/../')['status'] == 200, 'relative' + assert self.get(url='/slash/./')['status'] == 200, '/slash/./' + assert self.get(url='/slash//.//')['status'] == 200, 'adjacent slashes' + assert self.get(url='/%')['status'] == 400, 'percent' + assert self.get(url='/%1')['status'] == 400, 'percent digit' + assert self.get(url='/%A')['status'] == 400, 'percent letter' + assert self.get(url='/slash/.?args')['status'] == 200, 'dot args' + assert self.get(url='/slash/.#frag')['status'] == 200, 'dot frag' + assert ( + self.get(url='/slash/foo/..?args')['status'] == 200 + ), 'dot dot args' + assert ( + self.get(url='/slash/foo/..#frag')['status'] == 200 + ), 'dot dot frag' + assert self.get(url='/slash/.')['status'] == 200, 'trailing dot' + assert ( + self.get(url='/slash/foo/..')['status'] == 200 + ), 'trailing dot dot' def test_routes_match_uri_case_sensitive(self): self.route_match({"uri": "/BLAH"}) - self.assertEqual(self.get(url='/blah')['status'], 404, '/blah') - self.assertEqual(self.get(url='/BlaH')['status'], 404, '/BlaH') - self.assertEqual(self.get(url='/BLAH')['status'], 200, '/BLAH') + assert self.get(url='/blah')['status'] == 404, '/blah' + assert self.get(url='/BlaH')['status'] == 404, '/BlaH' + assert self.get(url='/BLAH')['status'] == 200, '/BLAH' def test_routes_match_uri_normalize(self): self.route_match({"uri": "/blah"}) - self.assertEqual( - self.get(url='/%62%6c%61%68')['status'], 200, 'normalize' - ) + assert self.get(url='/%62%6c%61%68')['status'] == 200, 'normalize' def test_routes_match_empty_array(self): self.route_match({"uri": []}) - self.assertEqual(self.get(url='/blah')['status'], 200, 'empty array') + assert self.get(url='/blah')['status'] == 200, 'empty array' def test_routes_reconfigure(self): - self.assertIn('success', self.conf([], 'routes'), 'redefine') - self.assertEqual(self.get()['status'], 404, 'redefine request') + assert 'success' in self.conf([], 'routes'), 'redefine' + assert self.get()['status'] == 404, 'redefine request' - self.assertIn( - 'success', - self.conf([{"action": {"return": 200}}], 'routes'), - 'redefine 2', - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 2') + assert 'success' in self.conf( + [{"action": {"return": 200}}], 'routes' + ), 'redefine 2' + assert self.get()['status'] == 200, 'redefine request 2' - self.assertIn('success', self.conf([], 'routes'), 'redefine 3') - self.assertEqual(self.get()['status'], 404, 'redefine request 3') + assert 'success' in self.conf([], 'routes'), 'redefine 3' + assert self.get()['status'] == 404, 'redefine request 3' - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/main"}}, - "routes": {"main": [{"action": {"return": 200}}]}, - "applications": {}, - } - ), - 'redefine 4', - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 4') + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/main"}}, + "routes": {"main": [{"action": {"return": 200}}]}, + "applications": {}, + } + ), 'redefine 4' + assert self.get()['status'] == 200, 'redefine request 4' - self.assertIn( - 'success', self.conf_delete('routes/main/0'), 'redefine 5' - ) - self.assertEqual(self.get()['status'], 404, 'redefine request 5') + assert 'success' in self.conf_delete('routes/main/0'), 'redefine 5' + assert self.get()['status'] == 404, 'redefine request 5' - self.assertIn( - 'success', - self.conf_post({"action": {"return": 200}}, 'routes/main'), - 'redefine 6', - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 6') + assert 'success' in self.conf_post( + {"action": {"return": 200}}, 'routes/main' + ), 'redefine 6' + assert self.get()['status'] == 200, 'redefine request 6' - self.assertIn( - 'error', - self.conf({"action": {"return": 200}}, 'routes/main/2'), - 'redefine 7', - ) - self.assertIn( - 'success', - self.conf({"action": {"return": 201}}, 'routes/main/1'), - 'redefine 8', - ) + assert 'error' in self.conf( + {"action": {"return": 200}}, 'routes/main/2' + ), 'redefine 7' + assert 'success' in self.conf( + {"action": {"return": 201}}, 'routes/main/1' + ), 'redefine 8' - self.assertEqual( - len(self.conf_get('routes/main')), 2, 'redefine conf 8' - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 8') + assert len(self.conf_get('routes/main')) == 2, 'redefine conf 8' + assert self.get()['status'] == 200, 'redefine request 8' def test_routes_edit(self): self.route_match({"method": "GET"}) - self.assertEqual(self.get()['status'], 200, 'routes edit GET') - self.assertEqual(self.post()['status'], 404, 'routes edit POST') - - self.assertIn( - 'success', - self.conf_post( - {"match": {"method": "POST"}, "action": {"return": 200}}, - 'routes', - ), - 'routes edit configure 2', - ) - self.assertEqual( - 'GET', - self.conf_get('routes/0/match/method'), - 'routes edit configure 2 check', - ) - self.assertEqual( - 'POST', - self.conf_get('routes/1/match/method'), - 'routes edit configure 2 check 2', - ) - - self.assertEqual(self.get()['status'], 200, 'routes edit GET 2') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 2') - - self.assertIn( - 'success', self.conf_delete('routes/0'), 'routes edit configure 3', - ) - - self.assertEqual(self.get()['status'], 404, 'routes edit GET 3') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 3') - - self.assertIn( - 'error', - self.conf_delete('routes/1'), - 'routes edit configure invalid', - ) - self.assertIn( - 'error', - self.conf_delete('routes/-1'), - 'routes edit configure invalid 2', - ) - self.assertIn( - 'error', - self.conf_delete('routes/blah'), - 'routes edit configure invalid 3', - ) - - self.assertEqual(self.get()['status'], 404, 'routes edit GET 4') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 4') - - self.assertIn( - 'success', self.conf_delete('routes/0'), 'routes edit configure 5', - ) - - self.assertEqual(self.get()['status'], 404, 'routes edit GET 5') - self.assertEqual(self.post()['status'], 404, 'routes edit POST 5') - - self.assertIn( - 'success', - self.conf_post( - {"match": {"method": "POST"}, "action": {"return": 200}}, - 'routes', - ), - 'routes edit configure 6', - ) - - self.assertEqual(self.get()['status'], 404, 'routes edit GET 6') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 6') - - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/main"}}, - "routes": {"main": [{"action": {"return": 200}}]}, - "applications": {}, - } - ), - 'route edit configure 7', - ) + assert self.get()['status'] == 200, 'routes edit GET' + assert self.post()['status'] == 404, 'routes edit POST' + + assert 'success' in self.conf_post( + {"match": {"method": "POST"}, "action": {"return": 200}}, 'routes', + ), 'routes edit configure 2' + assert 'GET' == self.conf_get( + 'routes/0/match/method' + ), 'routes edit configure 2 check' + assert 'POST' == self.conf_get( + 'routes/1/match/method' + ), 'routes edit configure 2 check 2' + + assert self.get()['status'] == 200, 'routes edit GET 2' + assert self.post()['status'] == 200, 'routes edit POST 2' + + assert 'success' in self.conf_delete( + 'routes/0' + ), 'routes edit configure 3' + + assert self.get()['status'] == 404, 'routes edit GET 3' + assert self.post()['status'] == 200, 'routes edit POST 3' + + assert 'error' in self.conf_delete( + 'routes/1' + ), 'routes edit configure invalid' + assert 'error' in self.conf_delete( + 'routes/-1' + ), 'routes edit configure invalid 2' + assert 'error' in self.conf_delete( + 'routes/blah' + ), 'routes edit configure invalid 3' + + assert self.get()['status'] == 404, 'routes edit GET 4' + assert self.post()['status'] == 200, 'routes edit POST 4' + + assert 'success' in self.conf_delete( + 'routes/0' + ), 'routes edit configure 5' + + assert self.get()['status'] == 404, 'routes edit GET 5' + assert self.post()['status'] == 404, 'routes edit POST 5' + + assert 'success' in self.conf_post( + {"match": {"method": "POST"}, "action": {"return": 200}}, 'routes', + ), 'routes edit configure 6' + + assert self.get()['status'] == 404, 'routes edit GET 6' + assert self.post()['status'] == 200, 'routes edit POST 6' + + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/main"}}, + "routes": {"main": [{"action": {"return": 200}}]}, + "applications": {}, + } + ), 'route edit configure 7' - self.assertIn( - 'error', - self.conf_delete('routes/0'), - 'routes edit configure invalid 4', - ) - self.assertIn( - 'error', - self.conf_delete('routes/main'), - 'routes edit configure invalid 5', - ) + assert 'error' in self.conf_delete( + 'routes/0' + ), 'routes edit configure invalid 4' + assert 'error' in self.conf_delete( + 'routes/main' + ), 'routes edit configure invalid 5' - self.assertEqual(self.get()['status'], 200, 'routes edit GET 7') + assert self.get()['status'] == 200, 'routes edit GET 7' - self.assertIn( - 'success', - self.conf_delete('listeners/*:7080'), - 'route edit configure 8', - ) - self.assertIn( - 'success', - self.conf_delete('routes/main'), - 'route edit configure 9', - ) + assert 'success' in self.conf_delete( + 'listeners/*:7080' + ), 'route edit configure 8' + assert 'success' in self.conf_delete( + 'routes/main' + ), 'route edit configure 9' def test_match_edit(self): - self.skip_alerts.append(r'failed to apply new conf') + skip_alert(r'failed to apply new conf') self.route_match({"method": ["GET", "POST"]}) - self.assertEqual(self.get()['status'], 200, 'match edit GET') - self.assertEqual(self.post()['status'], 200, 'match edit POST') - self.assertEqual(self.put()['status'], 404, 'match edit PUT') - - self.assertIn( - 'success', - self.conf_post('\"PUT\"', 'routes/0/match/method'), - 'match edit configure 2', - ) - self.assertListEqual( - ['GET', 'POST', 'PUT'], - self.conf_get('routes/0/match/method'), - 'match edit configure 2 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 2') - self.assertEqual(self.post()['status'], 200, 'match edit POST 2') - self.assertEqual(self.put()['status'], 200, 'match edit PUT 2') - - self.assertIn( - 'success', - self.conf_delete('routes/0/match/method/1'), - 'match edit configure 3', - ) - self.assertListEqual( - ['GET', 'PUT'], - self.conf_get('routes/0/match/method'), - 'match edit configure 3 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 3') - self.assertEqual(self.post()['status'], 404, 'match edit POST 3') - self.assertEqual(self.put()['status'], 200, 'match edit PUT 3') - - self.assertIn( - 'success', - self.conf_delete('routes/0/match/method/1'), - 'match edit configure 4', - ) - self.assertListEqual( - ['GET'], - self.conf_get('routes/0/match/method'), - 'match edit configure 4 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 4') - self.assertEqual(self.post()['status'], 404, 'match edit POST 4') - self.assertEqual(self.put()['status'], 404, 'match edit PUT 4') - - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/1'), - 'match edit configure invalid', - ) - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/-1'), - 'match edit configure invalid 2', - ) - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/blah'), - 'match edit configure invalid 3', - ) - self.assertListEqual( - ['GET'], - self.conf_get('routes/0/match/method'), - 'match edit configure 5 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 5') - self.assertEqual(self.post()['status'], 404, 'match edit POST 5') - self.assertEqual(self.put()['status'], 404, 'match edit PUT 5') - - self.assertIn( - 'success', - self.conf_delete('routes/0/match/method/0'), - 'match edit configure 6', - ) - self.assertListEqual( - [], - self.conf_get('routes/0/match/method'), - 'match edit configure 6 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 6') - self.assertEqual(self.post()['status'], 200, 'match edit POST 6') - self.assertEqual(self.put()['status'], 200, 'match edit PUT 6') - - self.assertIn( - 'success', - self.conf('"GET"', 'routes/0/match/method'), - 'match edit configure 7', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 7') - self.assertEqual(self.post()['status'], 404, 'match edit POST 7') - self.assertEqual(self.put()['status'], 404, 'match edit PUT 7') - - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/0'), - 'match edit configure invalid 5', - ) - self.assertIn( - 'error', - self.conf({}, 'routes/0/action'), - 'match edit configure invalid 6', - ) - - self.assertIn( - 'success', - self.conf({}, 'routes/0/match'), - 'match edit configure 8', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 8') + assert self.get()['status'] == 200, 'match edit GET' + assert self.post()['status'] == 200, 'match edit POST' + assert self.put()['status'] == 404, 'match edit PUT' + + assert 'success' in self.conf_post( + '\"PUT\"', 'routes/0/match/method' + ), 'match edit configure 2' + assert ['GET', 'POST', 'PUT'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 2 check' + + assert self.get()['status'] == 200, 'match edit GET 2' + assert self.post()['status'] == 200, 'match edit POST 2' + assert self.put()['status'] == 200, 'match edit PUT 2' + + assert 'success' in self.conf_delete( + 'routes/0/match/method/1' + ), 'match edit configure 3' + assert ['GET', 'PUT'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 3 check' + + assert self.get()['status'] == 200, 'match edit GET 3' + assert self.post()['status'] == 404, 'match edit POST 3' + assert self.put()['status'] == 200, 'match edit PUT 3' + + assert 'success' in self.conf_delete( + 'routes/0/match/method/1' + ), 'match edit configure 4' + assert ['GET'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 4 check' + + assert self.get()['status'] == 200, 'match edit GET 4' + assert self.post()['status'] == 404, 'match edit POST 4' + assert self.put()['status'] == 404, 'match edit PUT 4' + + assert 'error' in self.conf_delete( + 'routes/0/match/method/1' + ), 'match edit configure invalid' + assert 'error' in self.conf_delete( + 'routes/0/match/method/-1' + ), 'match edit configure invalid 2' + assert 'error' in self.conf_delete( + 'routes/0/match/method/blah' + ), 'match edit configure invalid 3' + assert ['GET'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 5 check' + + assert self.get()['status'] == 200, 'match edit GET 5' + assert self.post()['status'] == 404, 'match edit POST 5' + assert self.put()['status'] == 404, 'match edit PUT 5' + + assert 'success' in self.conf_delete( + 'routes/0/match/method/0' + ), 'match edit configure 6' + assert [] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 6 check' + + assert self.get()['status'] == 200, 'match edit GET 6' + assert self.post()['status'] == 200, 'match edit POST 6' + assert self.put()['status'] == 200, 'match edit PUT 6' + + assert 'success' in self.conf( + '"GET"', 'routes/0/match/method' + ), 'match edit configure 7' + + assert self.get()['status'] == 200, 'match edit GET 7' + assert self.post()['status'] == 404, 'match edit POST 7' + assert self.put()['status'] == 404, 'match edit PUT 7' + + assert 'error' in self.conf_delete( + 'routes/0/match/method/0' + ), 'match edit configure invalid 5' + assert 'error' in self.conf( + {}, 'routes/0/action' + ), 'match edit configure invalid 6' + + assert 'success' in self.conf( + {}, 'routes/0/match' + ), 'match edit configure 8' + + assert self.get()['status'] == 200, 'match edit GET 8' def test_routes_match_rules(self): self.route_match({"method": "GET", "host": "localhost", "uri": "/"}) - self.assertEqual(self.get()['status'], 200, 'routes match rules') + assert self.get()['status'] == 200, 'routes match rules' def test_routes_loop(self): - self.assertIn( - 'success', - self.route({"match": {"uri": "/"}, "action": {"pass": "routes"}}), - 'routes loop configure', - ) + assert 'success' in self.route( + {"match": {"uri": "/"}, "action": {"pass": "routes"}} + ), 'routes loop configure' - self.assertEqual(self.get()['status'], 500, 'routes loop') + assert self.get()['status'] == 500, 'routes loop' def test_routes_match_headers(self): self.route_match({"headers": {"host": "localhost"}}) - self.assertEqual(self.get()['status'], 200, 'match headers') + assert self.get()['status'] == 200, 'match headers' self.host('Localhost', 200) self.host('localhost.com', 404) self.host('llocalhost', 404) @@ -917,134 +747,122 @@ class TestRouting(TestApplicationProto): def test_routes_match_headers_multiple(self): self.route_match({"headers": {"host": "localhost", "x-blah": "test"}}) - self.assertEqual(self.get()['status'], 404, 'match headers multiple') - self.assertEqual( + assert self.get()['status'] == 404, 'match headers multiple' + assert ( self.get( headers={ "Host": "localhost", "X-blah": "test", "Connection": "close", } - )['status'], - 200, - 'match headers multiple 2', - ) + )['status'] + == 200 + ), 'match headers multiple 2' - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "X-blah": "", "Connection": "close", } - )['status'], - 404, - 'match headers multiple 3', - ) + )['status'] + == 404 + ), 'match headers multiple 3' def test_routes_match_headers_multiple_values(self): self.route_match({"headers": {"x-blah": "test"}}) - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "test", "test"], "Connection": "close", } - )['status'], - 200, - 'match headers multiple values', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers multiple values' + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "blah", "test"], "Connection": "close", } - )['status'], - 404, - 'match headers multiple values 2', - ) - self.assertEqual( + )['status'] + == 404 + ), 'match headers multiple values 2' + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "", "test"], "Connection": "close", } - )['status'], - 404, - 'match headers multiple values 3', - ) + )['status'] + == 404 + ), 'match headers multiple values 3' def test_routes_match_headers_multiple_rules(self): self.route_match({"headers": {"x-blah": ["test", "blah"]}}) - self.assertEqual( - self.get()['status'], 404, 'match headers multiple rules' - ) - self.assertEqual( + assert self.get()['status'] == 404, 'match headers multiple rules' + assert ( self.get( headers={ "Host": "localhost", "X-blah": "test", "Connection": "close", } - )['status'], - 200, - 'match headers multiple rules 2', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers multiple rules 2' + assert ( self.get( headers={ "Host": "localhost", "X-blah": "blah", "Connection": "close", } - )['status'], - 200, - 'match headers multiple rules 3', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers multiple rules 3' + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "blah", "test"], "Connection": "close", } - )['status'], - 200, - 'match headers multiple rules 4', - ) + )['status'] + == 200 + ), 'match headers multiple rules 4' - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["blah", ""], "Connection": "close", } - )['status'], - 404, - 'match headers multiple rules 5', - ) + )['status'] + == 404 + ), 'match headers multiple rules 5' def test_routes_match_headers_case_insensitive(self): self.route_match({"headers": {"X-BLAH": "TEST"}}) - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "x-blah": "test", "Connection": "close", } - )['status'], - 200, - 'match headers case insensitive', - ) + )['status'] + == 200 + ), 'match headers case insensitive' def test_routes_match_headers_invalid(self): self.route_match_invalid({"headers": ["blah"]}) @@ -1054,29 +872,30 @@ class TestRouting(TestApplicationProto): def test_routes_match_headers_empty_rule(self): self.route_match({"headers": {"host": ""}}) - self.assertEqual(self.get()['status'], 404, 'localhost') + assert self.get()['status'] == 404, 'localhost' self.host('', 200) def test_routes_match_headers_empty(self): self.route_match({"headers": {}}) - self.assertEqual(self.get()['status'], 200, 'empty') + assert self.get()['status'] == 200, 'empty' self.route_match({"headers": []}) - self.assertEqual(self.get()['status'], 200, 'empty 2') + assert self.get()['status'] == 200, 'empty 2' def test_routes_match_headers_rule_array_empty(self): self.route_match({"headers": {"blah": []}}) - self.assertEqual(self.get()['status'], 404, 'array empty') - self.assertEqual( + assert self.get()['status'] == 404, 'array empty' + assert ( self.get( headers={ "Host": "localhost", "blah": "foo", "Connection": "close", } - )['status'], 200, 'match headers rule array empty 2' - ) + )['status'] + == 200 + ), 'match headers rule array empty 2' def test_routes_match_headers_array(self): self.route_match( @@ -1090,52 +909,48 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get()['status'], 404, 'match headers array') - self.assertEqual( + 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', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers array 2' + assert ( self.get( headers={ "Host": "localhost", "x-header2": "bar", "Connection": "close", } - )['status'], - 200, - 'match headers array 3', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers array 3' + assert ( self.get( headers={ "Host": "localhost", "x-header3": "bar", "Connection": "close", } - )['status'], - 200, - 'match headers array 4', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers array 4' + assert ( self.get( headers={ "Host": "localhost", "x-header1": "bar", "Connection": "close", } - )['status'], - 404, - 'match headers array 5', - ) - self.assertEqual( + )['status'] + == 404 + ), 'match headers array 5' + assert ( self.get( headers={ "Host": "localhost", @@ -1143,49 +958,44 @@ class TestRouting(TestApplicationProto): "x-header4": "foo", "Connection": "close", } - )['status'], - 200, - 'match headers array 6', - ) + )['status'] + == 200 + ), 'match headers array 6' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/headers/1'), - 'match headers array configure 2', - ) + assert 'success' in self.conf_delete( + 'routes/0/match/headers/1' + ), 'match headers array configure 2' - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "x-header2": "bar", "Connection": "close", } - )['status'], - 404, - 'match headers array 7', - ) - self.assertEqual( + )['status'] + == 404 + ), 'match headers array 7' + assert ( self.get( headers={ "Host": "localhost", "x-header3": "foo", "Connection": "close", } - )['status'], - 200, - 'match headers array 8', - ) + )['status'] + == 200 + ), 'match headers array 8' def test_routes_match_arguments(self): self.route_match({"arguments": {"foo": "bar"}}) - self.assertEqual(self.get()['status'], 404, 'args') - self.assertEqual(self.get(url='/?foo=bar')['status'], 200, 'args 2') - self.assertEqual(self.get(url='/?foo=bar1')['status'], 404, 'args 3') - self.assertEqual(self.get(url='/?1foo=bar')['status'], 404, 'args 4') - self.assertEqual(self.get(url='/?Foo=bar')['status'], 404, 'case') - self.assertEqual(self.get(url='/?foo=Bar')['status'], 404, 'case 2') + assert self.get()['status'] == 404, 'args' + assert self.get(url='/?foo=bar')['status'] == 200, 'args 2' + assert self.get(url='/?foo=bar1')['status'] == 404, 'args 3' + assert self.get(url='/?1foo=bar')['status'] == 404, 'args 4' + assert self.get(url='/?Foo=bar')['status'] == 404, 'case' + assert self.get(url='/?foo=Bar')['status'] == 404, 'case 2' def test_routes_match_arguments_chars(self): chars = ( @@ -1195,15 +1005,30 @@ class TestRouting(TestApplicationProto): chars_enc = "" for h1 in ["2", "3", "4", "5", "6", "7"]: - for h2 in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", - "B", "C", "D", "E", "F", + for h2 in [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", ]: chars_enc += "%" + h1 + h2 chars_enc = chars_enc[:-3] def check_args(args, query): self.route_match({"arguments": args}) - self.assertEqual(self.get(url='/?' + query)['status'], 200) + assert self.get(url='/?' + query)['status'] == 200 check_args({chars: chars}, chars + '=' + chars) check_args({chars: chars}, chars + '=' + chars_enc) @@ -1216,175 +1041,155 @@ class TestRouting(TestApplicationProto): def test_routes_match_arguments_empty(self): self.route_match({"arguments": {}}) - self.assertEqual(self.get()['status'], 200, 'arguments empty') + assert self.get()['status'] == 200, 'arguments empty' self.route_match({"arguments": []}) - self.assertEqual(self.get()['status'], 200, 'arguments empty 2') + assert self.get()['status'] == 200, 'arguments empty 2' def test_routes_match_arguments_space(self): self.route_match({"arguments": {"+fo o%20": "%20b+a r"}}) - self.assertEqual(self.get(url='/? fo o = b a r&')['status'], 200) - self.assertEqual(self.get(url='/?+fo+o+=+b+a+r&')['status'], 200) - self.assertEqual( - self.get(url='/?%20fo%20o%20=%20b%20a%20r&')['status'], 200 - ) + assert self.get(url='/? fo o = b a r&')['status'] == 200 + assert self.get(url='/?+fo+o+=+b+a+r&')['status'] == 200 + assert self.get(url='/?%20fo%20o%20=%20b%20a%20r&')['status'] == 200 self.route_match({"arguments": {"%20foo": " bar"}}) - self.assertEqual(self.get(url='/? foo= bar')['status'], 200) - self.assertEqual(self.get(url='/?+foo=+bar')['status'], 200) - self.assertEqual(self.get(url='/?%20foo=%20bar')['status'], 200) - self.assertEqual(self.get(url='/?+foo= bar')['status'], 200) - self.assertEqual(self.get(url='/?%20foo=+bar')['status'], 200) + assert self.get(url='/? foo= bar')['status'] == 200 + assert self.get(url='/?+foo=+bar')['status'] == 200 + assert self.get(url='/?%20foo=%20bar')['status'] == 200 + assert self.get(url='/?+foo= bar')['status'] == 200 + assert self.get(url='/?%20foo=+bar')['status'] == 200 def test_routes_match_arguments_equal(self): self.route_match({"arguments": {"=": "="}}) - self.assertEqual(self.get(url='/?%3D=%3D')['status'], 200) - self.assertEqual(self.get(url='/?%3D==')['status'], 200) - self.assertEqual(self.get(url='/?===')['status'], 404) - self.assertEqual(self.get(url='/?%3D%3D%3D')['status'], 404) - self.assertEqual(self.get(url='/?==%3D')['status'], 404) + assert self.get(url='/?%3D=%3D')['status'] == 200 + assert self.get(url='/?%3D==')['status'] == 200 + assert self.get(url='/?===')['status'] == 404 + assert self.get(url='/?%3D%3D%3D')['status'] == 404 + assert self.get(url='/?==%3D')['status'] == 404 def test_routes_match_arguments_enc(self): self.route_match({"arguments": {"Ю": "н"}}) - self.assertEqual(self.get(url='/?%D0%AE=%D0%BD')['status'], 200) - self.assertEqual(self.get(url='/?%d0%ae=%d0%Bd')['status'], 200) + assert self.get(url='/?%D0%AE=%D0%BD')['status'] == 200 + assert self.get(url='/?%d0%ae=%d0%Bd')['status'] == 200 def test_routes_match_arguments_hash(self): self.route_match({"arguments": {"#": "#"}}) - self.assertEqual(self.get(url='/?%23=%23')['status'], 200) - self.assertEqual(self.get(url='/?%23=%23#')['status'], 200) - self.assertEqual(self.get(url='/?#=#')['status'], 404) - self.assertEqual(self.get(url='/?%23=#')['status'], 404) + assert self.get(url='/?%23=%23')['status'] == 200 + assert self.get(url='/?%23=%23#')['status'] == 200 + assert self.get(url='/?#=#')['status'] == 404 + assert self.get(url='/?%23=#')['status'] == 404 def test_routes_match_arguments_wildcard(self): self.route_match({"arguments": {"foo": "*"}}) - self.assertEqual(self.get(url='/?foo')['status'], 200) - self.assertEqual(self.get(url='/?foo=')['status'], 200) - self.assertEqual(self.get(url='/?foo=blah')['status'], 200) - self.assertEqual(self.get(url='/?blah=foo')['status'], 404) + assert self.get(url='/?foo')['status'] == 200 + assert self.get(url='/?foo=')['status'] == 200 + assert self.get(url='/?foo=blah')['status'] == 200 + assert self.get(url='/?blah=foo')['status'] == 404 self.route_match({"arguments": {"foo": "%25*"}}) - self.assertEqual(self.get(url='/?foo=%xx')['status'], 200) + assert self.get(url='/?foo=%xx')['status'] == 200 self.route_match({"arguments": {"foo": "%2A*"}}) - self.assertEqual(self.get(url='/?foo=*xx')['status'], 200) - self.assertEqual(self.get(url='/?foo=xx')['status'], 404) + assert self.get(url='/?foo=*xx')['status'] == 200 + assert self.get(url='/?foo=xx')['status'] == 404 self.route_match({"arguments": {"foo": "*%2A"}}) - self.assertEqual(self.get(url='/?foo=xx*')['status'], 200) - self.assertEqual(self.get(url='/?foo=xx*x')['status'], 404) + assert self.get(url='/?foo=xx*')['status'] == 200 + assert self.get(url='/?foo=xx*x')['status'] == 404 self.route_match({"arguments": {"foo": "1*2"}}) - self.assertEqual(self.get(url='/?foo=12')['status'], 200) - self.assertEqual(self.get(url='/?foo=1blah2')['status'], 200) - self.assertEqual(self.get(url='/?foo=1%2A2')['status'], 200) - self.assertEqual(self.get(url='/?foo=x12')['status'], 404) + assert self.get(url='/?foo=12')['status'] == 200 + assert self.get(url='/?foo=1blah2')['status'] == 200 + assert self.get(url='/?foo=1%2A2')['status'] == 200 + assert self.get(url='/?foo=x12')['status'] == 404 self.route_match({"arguments": {"foo": "bar*", "%25": "%25"}}) - self.assertEqual(self.get(url='/?foo=barxx&%=%')['status'], 200) - self.assertEqual(self.get(url='/?foo=barxx&x%=%')['status'], 404) + assert self.get(url='/?foo=barxx&%=%')['status'] == 200 + assert self.get(url='/?foo=barxx&x%=%')['status'] == 404 def test_routes_match_arguments_negative(self): self.route_match({"arguments": {"foo": "!%25"}}) - self.assertEqual(self.get(url='/?foo=blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=%')['status'], 404) + assert self.get(url='/?foo=blah')['status'] == 200 + assert self.get(url='/?foo=%')['status'] == 404 self.route_match({"arguments": {"foo": "%21blah"}}) - self.assertEqual(self.get(url='/?foo=%21blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=!blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=bar')['status'], 404) + assert self.get(url='/?foo=%21blah')['status'] == 200 + assert self.get(url='/?foo=!blah')['status'] == 200 + assert self.get(url='/?foo=bar')['status'] == 404 self.route_match({"arguments": {"foo": "!!%21*a"}}) - self.assertEqual(self.get(url='/?foo=blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=!blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=!!a')['status'], 404) - self.assertEqual(self.get(url='/?foo=!!bla')['status'], 404) + assert self.get(url='/?foo=blah')['status'] == 200 + assert self.get(url='/?foo=!blah')['status'] == 200 + assert self.get(url='/?foo=!!a')['status'] == 404 + assert self.get(url='/?foo=!!bla')['status'] == 404 def test_routes_match_arguments_percent(self): self.route_match({"arguments": {"%25": "%25"}}) - self.assertEqual(self.get(url='/?%=%')['status'], 200) - self.assertEqual(self.get(url='/?%25=%25')['status'], 200) - self.assertEqual(self.get(url='/?%25=%')['status'], 200) + assert self.get(url='/?%=%')['status'] == 200 + assert self.get(url='/?%25=%25')['status'] == 200 + assert self.get(url='/?%25=%')['status'] == 200 self.route_match({"arguments": {"%251": "%252"}}) - self.assertEqual(self.get(url='/?%1=%2')['status'], 200) - self.assertEqual(self.get(url='/?%251=%252')['status'], 200) - self.assertEqual(self.get(url='/?%251=%2')['status'], 200) + assert self.get(url='/?%1=%2')['status'] == 200 + assert self.get(url='/?%251=%252')['status'] == 200 + assert self.get(url='/?%251=%2')['status'] == 200 self.route_match({"arguments": {"%25%21%251": "%25%24%252"}}) - self.assertEqual(self.get(url='/?%!%1=%$%2')['status'], 200) - self.assertEqual(self.get(url='/?%25!%251=%25$%252')['status'], 200) - self.assertEqual(self.get(url='/?%25!%1=%$%2')['status'], 200) + assert self.get(url='/?%!%1=%$%2')['status'] == 200 + assert self.get(url='/?%25!%251=%25$%252')['status'] == 200 + assert self.get(url='/?%25!%1=%$%2')['status'] == 200 def test_routes_match_arguments_ampersand(self): self.route_match({"arguments": {"foo": "&"}}) - self.assertEqual(self.get(url='/?foo=%26')['status'], 200) - self.assertEqual(self.get(url='/?foo=%26&')['status'], 200) - self.assertEqual(self.get(url='/?foo=%26%26')['status'], 404) - self.assertEqual(self.get(url='/?foo=&')['status'], 404) + assert self.get(url='/?foo=%26')['status'] == 200 + assert self.get(url='/?foo=%26&')['status'] == 200 + assert self.get(url='/?foo=%26%26')['status'] == 404 + assert self.get(url='/?foo=&')['status'] == 404 self.route_match({"arguments": {"&": ""}}) - self.assertEqual(self.get(url='/?%26=')['status'], 200) - self.assertEqual(self.get(url='/?%26=&')['status'], 200) - self.assertEqual(self.get(url='/?%26=%26')['status'], 404) - self.assertEqual(self.get(url='/?&=')['status'], 404) + assert self.get(url='/?%26=')['status'] == 200 + assert self.get(url='/?%26=&')['status'] == 200 + assert self.get(url='/?%26=%26')['status'] == 404 + assert self.get(url='/?&=')['status'] == 404 def test_routes_match_arguments_complex(self): self.route_match({"arguments": {"foo": ""}}) - self.assertEqual(self.get(url='/?foo')['status'], 200, 'complex') - self.assertEqual( - self.get(url='/?blah=blah&foo=')['status'], 200, 'complex 2' - ) - self.assertEqual( - self.get(url='/?&&&foo&&&')['status'], 200, 'complex 3' - ) - self.assertEqual( - self.get(url='/?foo&foo=bar&foo')['status'], 404, 'complex 4' - ) - self.assertEqual( - self.get(url='/?foo=&foo')['status'], 200, 'complex 5' - ) - self.assertEqual( - self.get(url='/?&=&foo&==&')['status'], 200, 'complex 6' - ) - self.assertEqual( - self.get(url='/?&=&bar&==&')['status'], 404, 'complex 7' - ) + assert self.get(url='/?foo')['status'] == 200, 'complex' + assert self.get(url='/?blah=blah&foo=')['status'] == 200, 'complex 2' + assert self.get(url='/?&&&foo&&&')['status'] == 200, 'complex 3' + assert self.get(url='/?foo&foo=bar&foo')['status'] == 404, 'complex 4' + assert self.get(url='/?foo=&foo')['status'] == 200, 'complex 5' + assert self.get(url='/?&=&foo&==&')['status'] == 200, 'complex 6' + assert self.get(url='/?&=&bar&==&')['status'] == 404, 'complex 7' def test_routes_match_arguments_multiple(self): self.route_match({"arguments": {"foo": "bar", "blah": "test"}}) - self.assertEqual(self.get()['status'], 404, 'multiple') - self.assertEqual( - self.get(url='/?foo=bar&blah=test')['status'], 200, 'multiple 2' - ) - self.assertEqual( - self.get(url='/?foo=bar&blah')['status'], 404, 'multiple 3' - ) - self.assertEqual( - self.get(url='/?foo=bar&blah=tes')['status'], 404, 'multiple 4' - ) - self.assertEqual( - self.get(url='/?foo=b%61r&bl%61h=t%65st')['status'], - 200, - 'multiple 5', - ) + assert self.get()['status'] == 404, 'multiple' + assert ( + self.get(url='/?foo=bar&blah=test')['status'] == 200 + ), 'multiple 2' + assert self.get(url='/?foo=bar&blah')['status'] == 404, 'multiple 3' + assert ( + self.get(url='/?foo=bar&blah=tes')['status'] == 404 + ), 'multiple 4' + assert ( + self.get(url='/?foo=b%61r&bl%61h=t%65st')['status'] == 200 + ), 'multiple 5' def test_routes_match_arguments_multiple_rules(self): self.route_match({"arguments": {"foo": ["bar", "blah"]}}) - self.assertEqual(self.get()['status'], 404, 'rules') - self.assertEqual(self.get(url='/?foo=bar')['status'], 200, 'rules 2') - self.assertEqual(self.get(url='/?foo=blah')['status'], 200, 'rules 3') - self.assertEqual( - self.get(url='/?foo=blah&foo=bar&foo=blah')['status'], - 200, - 'rules 4', - ) - self.assertEqual( - self.get(url='/?foo=blah&foo=bar&foo=')['status'], 404, 'rules 5' - ) + assert self.get()['status'] == 404, 'rules' + assert self.get(url='/?foo=bar')['status'] == 200, 'rules 2' + assert self.get(url='/?foo=blah')['status'] == 200, 'rules 3' + assert ( + self.get(url='/?foo=blah&foo=bar&foo=blah')['status'] == 200 + ), 'rules 4' + assert ( + self.get(url='/?foo=blah&foo=bar&foo=')['status'] == 404 + ), 'rules 5' def test_routes_match_arguments_array(self): self.route_match( @@ -1398,27 +1203,23 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get()['status'], 404, 'arr') - self.assertEqual(self.get(url='/?var1=val123')['status'], 200, 'arr 2') - self.assertEqual(self.get(url='/?var2=val2')['status'], 200, 'arr 3') - self.assertEqual(self.get(url='/?var3=bar')['status'], 200, 'arr 4') - self.assertEqual(self.get(url='/?var1=bar')['status'], 404, 'arr 5') - self.assertEqual( - self.get(url='/?var1=bar&var4=foo')['status'], 200, 'arr 6' - ) + assert self.get()['status'] == 404, 'arr' + assert self.get(url='/?var1=val123')['status'] == 200, 'arr 2' + assert self.get(url='/?var2=val2')['status'] == 200, 'arr 3' + assert self.get(url='/?var3=bar')['status'] == 200, 'arr 4' + assert self.get(url='/?var1=bar')['status'] == 404, 'arr 5' + assert self.get(url='/?var1=bar&var4=foo')['status'] == 200, 'arr 6' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/arguments/1'), - 'match arguments array configure 2', - ) + assert 'success' in self.conf_delete( + 'routes/0/match/arguments/1' + ), 'match arguments array configure 2' - self.assertEqual(self.get(url='/?var2=val2')['status'], 404, 'arr 7') - self.assertEqual(self.get(url='/?var3=foo')['status'], 200, 'arr 8') + 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): # TODO remove it after controller fixed - self.skip_alerts.append(r'failed to apply new conf') + skip_alert(r'failed to apply new conf') self.route_match_invalid({"arguments": ["var"]}) self.route_match_invalid({"arguments": [{"var1": {}}]}) @@ -1434,7 +1235,7 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies(self): self.route_match({"cookies": {"foO": "bar"}}) - self.assertEqual(self.get()['status'], 404, 'cookie') + assert self.get()['status'] == 404, 'cookie' self.cookie('foO=bar', 200) self.cookie('foO=bar;1', 200) self.cookie(['foO=bar', 'blah=blah'], 200) @@ -1446,10 +1247,10 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies_empty(self): self.route_match({"cookies": {}}) - self.assertEqual(self.get()['status'], 200, 'cookies empty') + assert self.get()['status'] == 200, 'cookies empty' self.route_match({"cookies": []}) - self.assertEqual(self.get()['status'], 200, 'cookies empty 2') + assert self.get()['status'] == 200, 'cookies empty 2' def test_routes_match_cookies_invalid(self): self.route_match_invalid({"cookies": ["var"]}) @@ -1458,7 +1259,7 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies_multiple(self): self.route_match({"cookies": {"foo": "bar", "blah": "blah"}}) - self.assertEqual(self.get()['status'], 404, 'multiple') + assert self.get()['status'] == 404, 'multiple' self.cookie('foo=bar; blah=blah', 200) self.cookie(['foo=bar', 'blah=blah'], 200) self.cookie(['foo=bar; blah', 'blah'], 404) @@ -1474,12 +1275,12 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies_multiple_rules(self): self.route_match({"cookies": {"blah": ["test", "blah"]}}) - self.assertEqual(self.get()['status'], 404, 'multiple rules') + assert self.get()['status'] == 404, 'multiple rules' self.cookie('blah=test', 200) self.cookie('blah=blah', 200) self.cookie(['blah=blah', 'blah=test', 'blah=blah'], 200) self.cookie(['blah=blah; blah=test', 'blah=blah'], 200) - self.cookie(['blah=blah', 'blah'], 200) # invalid cookie + self.cookie(['blah=blah', 'blah'], 200) # invalid cookie def test_routes_match_cookies_array(self): self.route_match( @@ -1493,7 +1294,7 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get()['status'], 404, 'cookies array') + assert self.get()['status'] == 404, 'cookies array' self.cookie('var1=val123', 200) self.cookie('var2=val2', 200) self.cookie(' var2=val2 ', 200) @@ -1503,11 +1304,9 @@ class TestRouting(TestApplicationProto): self.cookie('var1=bar; var4=foo;', 200) self.cookie(['var1=bar', 'var4=foo'], 200) - self.assertIn( - 'success', - self.conf_delete('routes/0/match/cookies/1'), - 'match cookies array configure 2', - ) + assert 'success' in self.conf_delete( + 'routes/0/match/cookies/1' + ), 'match cookies array configure 2' self.cookie('var2=val2', 404) self.cookie('var3=foo', 200) @@ -1535,22 +1334,22 @@ class TestRouting(TestApplicationProto): sock2, port2 = sock_port() self.route_match({"source": "127.0.0.1:" + str(port)}) - self.assertEqual(self.get(sock=sock)['status'], 200, 'exact') - self.assertEqual(self.get(sock=sock2)['status'], 404, 'exact 2') + assert self.get(sock=sock)['status'] == 200, 'exact' + assert self.get(sock=sock2)['status'] == 404, 'exact 2' sock, port = sock_port() sock2, port2 = sock_port() self.route_match({"source": "!127.0.0.1:" + str(port)}) - self.assertEqual(self.get(sock=sock)['status'], 404, 'negative') - self.assertEqual(self.get(sock=sock2)['status'], 200, 'negative 2') + assert self.get(sock=sock)['status'] == 404, 'negative' + assert self.get(sock=sock2)['status'] == 200, 'negative 2' sock, port = sock_port() sock2, port2 = sock_port() self.route_match({"source": ["*:" + str(port), "!127.0.0.1"]}) - self.assertEqual(self.get(sock=sock)['status'], 404, 'negative 3') - self.assertEqual(self.get(sock=sock2)['status'], 404, 'negative 4') + assert self.get(sock=sock)['status'] == 404, 'negative 3' + assert self.get(sock=sock2)['status'] == 404, 'negative 4' sock, port = sock_port() sock2, port2 = sock_port() @@ -1558,8 +1357,8 @@ class TestRouting(TestApplicationProto): self.route_match( {"source": "127.0.0.1:" + str(port) + "-" + str(port)} ) - self.assertEqual(self.get(sock=sock)['status'], 200, 'range single') - self.assertEqual(self.get(sock=sock2)['status'], 404, 'range single 2') + assert self.get(sock=sock)['status'] == 200, 'range single' + assert self.get(sock=sock2)['status'] == 404, 'range single 2' socks = [ sock_port(), @@ -1578,11 +1377,11 @@ class TestRouting(TestApplicationProto): + str(socks[3][1]) # fourth port number } ) - self.assertEqual(self.get(sock=socks[0][0])['status'], 404, 'range') - self.assertEqual(self.get(sock=socks[1][0])['status'], 200, 'range 2') - self.assertEqual(self.get(sock=socks[2][0])['status'], 200, 'range 3') - self.assertEqual(self.get(sock=socks[3][0])['status'], 200, 'range 4') - self.assertEqual(self.get(sock=socks[4][0])['status'], 404, 'range 5') + assert self.get(sock=socks[0][0])['status'] == 404, 'range' + assert self.get(sock=socks[1][0])['status'] == 200, 'range 2' + assert self.get(sock=socks[2][0])['status'] == 200, 'range 3' + assert self.get(sock=socks[3][0])['status'] == 200, 'range 4' + assert self.get(sock=socks[4][0])['status'] == 404, 'range 5' socks = [ sock_port(), @@ -1599,218 +1398,194 @@ class TestRouting(TestApplicationProto): ] } ) - self.assertEqual(self.get(sock=socks[0][0])['status'], 200, 'array') - self.assertEqual(self.get(sock=socks[1][0])['status'], 404, 'array 2') - self.assertEqual(self.get(sock=socks[2][0])['status'], 200, 'array 3') + assert self.get(sock=socks[0][0])['status'] == 200, 'array' + assert self.get(sock=socks[1][0])['status'] == 404, 'array 2' + assert self.get(sock=socks[2][0])['status'] == 200, 'array 3' def test_routes_source_addr(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "[::1]:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + {"*:7080": {"pass": "routes"}, "[::1]:7081": {"pass": "routes"},}, + 'listeners', + ), 'source listeners configure' def get_ipv6(): return self.get(sock_type='ipv6', port=7081) self.route_match({"source": "127.0.0.1"}) - self.assertEqual(self.get()['status'], 200, 'exact') - self.assertEqual(get_ipv6()['status'], 404, 'exact ipv6') + assert self.get()['status'] == 200, 'exact' + assert get_ipv6()['status'] == 404, 'exact ipv6' self.route_match({"source": ["127.0.0.1"]}) - self.assertEqual(self.get()['status'], 200, 'exact 2') - self.assertEqual(get_ipv6()['status'], 404, 'exact 2 ipv6') + assert self.get()['status'] == 200, 'exact 2' + assert get_ipv6()['status'] == 404, 'exact 2 ipv6' self.route_match({"source": "!127.0.0.1"}) - self.assertEqual(self.get()['status'], 404, 'exact neg') - self.assertEqual(get_ipv6()['status'], 200, 'exact neg ipv6') + assert self.get()['status'] == 404, 'exact neg' + assert get_ipv6()['status'] == 200, 'exact neg ipv6' self.route_match({"source": "127.0.0.2"}) - self.assertEqual(self.get()['status'], 404, 'exact 3') - self.assertEqual(get_ipv6()['status'], 404, 'exact 3 ipv6') + assert self.get()['status'] == 404, 'exact 3' + assert get_ipv6()['status'] == 404, 'exact 3 ipv6' self.route_match({"source": "127.0.0.1-127.0.0.1"}) - self.assertEqual(self.get()['status'], 200, 'range single') - self.assertEqual(get_ipv6()['status'], 404, 'range single ipv6') + assert self.get()['status'] == 200, 'range single' + assert get_ipv6()['status'] == 404, 'range single ipv6' self.route_match({"source": "127.0.0.2-127.0.0.2"}) - self.assertEqual(self.get()['status'], 404, 'range single 2') - self.assertEqual(get_ipv6()['status'], 404, 'range single 2 ipv6') + assert self.get()['status'] == 404, 'range single 2' + assert get_ipv6()['status'] == 404, 'range single 2 ipv6' self.route_match({"source": "127.0.0.2-127.0.0.3"}) - self.assertEqual(self.get()['status'], 404, 'range') - self.assertEqual(get_ipv6()['status'], 404, 'range ipv6') + assert self.get()['status'] == 404, 'range' + assert get_ipv6()['status'] == 404, 'range ipv6' self.route_match({"source": "127.0.0.1-127.0.0.2"}) - self.assertEqual(self.get()['status'], 200, 'range 2') - self.assertEqual(get_ipv6()['status'], 404, 'range 2 ipv6') + assert self.get()['status'] == 200, 'range 2' + assert get_ipv6()['status'] == 404, 'range 2 ipv6' self.route_match({"source": "127.0.0.0-127.0.0.2"}) - self.assertEqual(self.get()['status'], 200, 'range 3') - self.assertEqual(get_ipv6()['status'], 404, 'range 3 ipv6') + assert self.get()['status'] == 200, 'range 3' + assert get_ipv6()['status'] == 404, 'range 3 ipv6' self.route_match({"source": "127.0.0.0-127.0.0.1"}) - self.assertEqual(self.get()['status'], 200, 'range 4') - self.assertEqual(get_ipv6()['status'], 404, 'range 4 ipv6') + assert self.get()['status'] == 200, 'range 4' + assert get_ipv6()['status'] == 404, 'range 4 ipv6' self.route_match({"source": "126.0.0.0-127.0.0.0"}) - self.assertEqual(self.get()['status'], 404, 'range 5') - self.assertEqual(get_ipv6()['status'], 404, 'range 5 ipv6') + assert self.get()['status'] == 404, 'range 5' + assert get_ipv6()['status'] == 404, 'range 5 ipv6' self.route_match({"source": "126.126.126.126-127.0.0.2"}) - self.assertEqual(self.get()['status'], 200, 'range 6') - self.assertEqual(get_ipv6()['status'], 404, 'range 6 ipv6') + assert self.get()['status'] == 200, 'range 6' + assert get_ipv6()['status'] == 404, 'range 6 ipv6' def test_routes_source_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "[::1]:7080": {"pass": "routes"}, - "127.0.0.1:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + { + "[::1]:7080": {"pass": "routes"}, + "127.0.0.1:7081": {"pass": "routes"}, + }, + 'listeners', + ), 'source listeners configure' self.route_match({"source": "::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'exact') - self.assertEqual(self.get(port=7081)['status'], 404, 'exact ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'exact' + assert self.get(port=7081)['status'] == 404, 'exact ipv4' self.route_match({"source": ["::1"]}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'exact 2') - self.assertEqual(self.get(port=7081)['status'], 404, 'exact 2 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'exact 2' + assert self.get(port=7081)['status'] == 404, 'exact 2 ipv4' self.route_match({"source": "!::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'exact neg') - self.assertEqual(self.get(port=7081)['status'], 200, 'exact neg ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'exact neg' + assert self.get(port=7081)['status'] == 200, 'exact neg ipv4' self.route_match({"source": "::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'exact 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'exact 3 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'exact 3' + assert self.get(port=7081)['status'] == 404, 'exact 3 ipv4' self.route_match({"source": "::1-::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range') - self.assertEqual(self.get(port=7081)['status'], 404, 'range ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range' + assert self.get(port=7081)['status'] == 404, 'range ipv4' self.route_match({"source": "::2-::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'range 2') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 2 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'range 2' + assert self.get(port=7081)['status'] == 404, 'range 2 ipv4' self.route_match({"source": "::2-::3"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'range 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 3 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'range 3' + assert self.get(port=7081)['status'] == 404, 'range 3 ipv4' self.route_match({"source": "::1-::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range 4') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 4 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range 4' + assert self.get(port=7081)['status'] == 404, 'range 4 ipv4' self.route_match({"source": "::0-::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range 5') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 5 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range 5' + assert self.get(port=7081)['status'] == 404, 'range 5 ipv4' self.route_match({"source": "::0-::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range 6') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 6 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range 6' + assert self.get(port=7081)['status'] == 404, 'range 6 ipv4' def test_routes_source_cidr(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "[::1]:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + {"*:7080": {"pass": "routes"}, "[::1]:7081": {"pass": "routes"},}, + 'listeners', + ), 'source listeners configure' def get_ipv6(): return self.get(sock_type='ipv6', port=7081) self.route_match({"source": "127.0.0.1/32"}) - self.assertEqual(self.get()['status'], 200, '32') - self.assertEqual(get_ipv6()['status'], 404, '32 ipv6') + assert self.get()['status'] == 200, '32' + assert get_ipv6()['status'] == 404, '32 ipv6' self.route_match({"source": "127.0.0.0/32"}) - self.assertEqual(self.get()['status'], 404, '32 2') - self.assertEqual(get_ipv6()['status'], 404, '32 2 ipv6') + assert self.get()['status'] == 404, '32 2' + assert get_ipv6()['status'] == 404, '32 2 ipv6' self.route_match({"source": "127.0.0.0/31"}) - self.assertEqual(self.get()['status'], 200, '31') - self.assertEqual(get_ipv6()['status'], 404, '31 ipv6') + assert self.get()['status'] == 200, '31' + assert get_ipv6()['status'] == 404, '31 ipv6' self.route_match({"source": "0.0.0.0/1"}) - self.assertEqual(self.get()['status'], 200, '1') - self.assertEqual(get_ipv6()['status'], 404, '1 ipv6') + assert self.get()['status'] == 200, '1' + assert get_ipv6()['status'] == 404, '1 ipv6' self.route_match({"source": "0.0.0.0/0"}) - self.assertEqual(self.get()['status'], 200, '0') - self.assertEqual(get_ipv6()['status'], 404, '0 ipv6') + assert self.get()['status'] == 200, '0' + assert get_ipv6()['status'] == 404, '0 ipv6' def test_routes_source_cidr_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "[::1]:7080": {"pass": "routes"}, - "127.0.0.1:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + { + "[::1]:7080": {"pass": "routes"}, + "127.0.0.1:7081": {"pass": "routes"}, + }, + 'listeners', + ), 'source listeners configure' self.route_match({"source": "::1/128"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '128') - self.assertEqual(self.get(port=7081)['status'], 404, '128 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '128' + assert self.get(port=7081)['status'] == 404, '128 ipv4' self.route_match({"source": "::0/128"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, '128 2') - self.assertEqual(self.get(port=7081)['status'], 404, '128 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, '128 2' + assert self.get(port=7081)['status'] == 404, '128 ipv4' self.route_match({"source": "::0/127"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '127') - self.assertEqual(self.get(port=7081)['status'], 404, '127 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '127' + assert self.get(port=7081)['status'] == 404, '127 ipv4' self.route_match({"source": "::0/32"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '32') - self.assertEqual(self.get(port=7081)['status'], 404, '32 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '32' + assert self.get(port=7081)['status'] == 404, '32 ipv4' self.route_match({"source": "::0/1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '1') - self.assertEqual(self.get(port=7081)['status'], 404, '1 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '1' + assert self.get(port=7081)['status'] == 404, '1 ipv4' self.route_match({"source": "::/0"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '0') - self.assertEqual(self.get(port=7081)['status'], 404, '0 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '0' + assert self.get(port=7081)['status'] == 404, '0 ipv4' def test_routes_source_unix(self): - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' - self.assertIn( - 'success', - self.conf({"unix:" + addr: {"pass": "routes"}}, 'listeners'), - 'source listeners configure', - ) + assert 'success' in self.conf( + {"unix:" + addr: {"pass": "routes"}}, 'listeners' + ), 'source listeners configure' self.route_match({"source": "!0.0.0.0/0"}) - self.assertEqual( - self.get(sock_type='unix', addr=addr)['status'], 200, 'unix ipv4' - ) + assert ( + self.get(sock_type='unix', addr=addr)['status'] == 200 + ), 'unix ipv4' self.route_match({"source": "!::/0"}) - self.assertEqual( - self.get(sock_type='unix', addr=addr)['status'], 200, 'unix ipv6' - ) + assert ( + self.get(sock_type='unix', addr=addr)['status'] == 200 + ), 'unix ipv6' def test_routes_match_source(self): self.route_match({"source": "::"}) @@ -1863,7 +1638,7 @@ class TestRouting(TestApplicationProto): } ) self.route_match({"source": "*:0-65535"}) - self.assertEqual(self.get()['status'], 200, 'source any') + assert self.get()['status'] == 200, 'source any' def test_routes_match_source_invalid(self): self.route_match_invalid({"source": "127"}) @@ -1888,104 +1663,84 @@ class TestRouting(TestApplicationProto): self.route_match_invalid({"source": "*:65536"}) def test_routes_match_destination(self): - self.assertIn( - 'success', - self.conf( - {"*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}}, - 'listeners', - ), - 'listeners configure', - ) + assert 'success' in self.conf( + {"*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}}, + 'listeners', + ), 'listeners configure' self.route_match({"destination": "*:7080"}) - self.assertEqual(self.get()['status'], 200, 'dest') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest 2') + assert self.get()['status'] == 200, 'dest' + assert self.get(port=7081)['status'] == 404, 'dest 2' self.route_match({"destination": ["127.0.0.1:7080"]}) - self.assertEqual(self.get()['status'], 200, 'dest 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest 4') + assert self.get()['status'] == 200, 'dest 3' + assert self.get(port=7081)['status'] == 404, 'dest 4' self.route_match({"destination": "!*:7080"}) - self.assertEqual(self.get()['status'], 404, 'dest neg') - self.assertEqual(self.get(port=7081)['status'], 200, 'dest neg 2') + assert self.get()['status'] == 404, 'dest neg' + assert self.get(port=7081)['status'] == 200, 'dest neg 2' self.route_match({"destination": ['!*:7080', '!*:7081']}) - self.assertEqual(self.get()['status'], 404, 'dest neg 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 4') + assert self.get()['status'] == 404, 'dest neg 3' + assert self.get(port=7081)['status'] == 404, 'dest neg 4' self.route_match({"destination": ['!*:7081', '!*:7082']}) - self.assertEqual(self.get()['status'], 200, 'dest neg 5') + assert self.get()['status'] == 200, 'dest neg 5' self.route_match({"destination": ['*:7080', '!*:7080']}) - self.assertEqual(self.get()['status'], 404, 'dest neg 6') + assert self.get()['status'] == 404, 'dest neg 6' self.route_match( {"destination": ['127.0.0.1:7080', '*:7081', '!*:7080']} ) - self.assertEqual(self.get()['status'], 404, 'dest neg 7') - self.assertEqual(self.get(port=7081)['status'], 200, 'dest neg 8') + assert self.get()['status'] == 404, 'dest neg 7' + assert self.get(port=7081)['status'] == 200, 'dest neg 8' self.route_match({"destination": ['!*:7081', '!*:7082', '*:7083']}) - self.assertEqual(self.get()['status'], 404, 'dest neg 9') + assert self.get()['status'] == 404, 'dest neg 9' self.route_match( {"destination": ['*:7081', '!127.0.0.1:7080', '*:7080']} ) - self.assertEqual(self.get()['status'], 404, 'dest neg 10') - self.assertEqual(self.get(port=7081)['status'], 200, 'dest neg 11') + assert self.get()['status'] == 404, 'dest neg 10' + assert self.get(port=7081)['status'] == 200, 'dest neg 11' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/destination/0'), - 'remove destination rule', - ) - self.assertEqual(self.get()['status'], 404, 'dest neg 12') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 13') + assert 'success' in self.conf_delete( + 'routes/0/match/destination/0' + ), 'remove destination rule' + assert self.get()['status'] == 404, 'dest neg 12' + assert self.get(port=7081)['status'] == 404, 'dest neg 13' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/destination/0'), - 'remove destination rule 2', - ) - self.assertEqual(self.get()['status'], 200, 'dest neg 14') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 15') + assert 'success' in self.conf_delete( + 'routes/0/match/destination/0' + ), 'remove destination rule 2' + assert self.get()['status'] == 200, 'dest neg 14' + assert self.get(port=7081)['status'] == 404, 'dest neg 15' - self.assertIn( - 'success', - self.conf_post("\"!127.0.0.1\"", 'routes/0/match/destination'), - 'add destination rule', - ) - self.assertEqual(self.get()['status'], 404, 'dest neg 16') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 17') + assert 'success' in self.conf_post( + "\"!127.0.0.1\"", 'routes/0/match/destination' + ), 'add destination rule' + assert self.get()['status'] == 404, 'dest neg 16' + assert self.get(port=7081)['status'] == 404, 'dest neg 17' def test_routes_match_destination_proxy(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes/first"}, - "*:7081": {"pass": "routes/second"}, - }, - "routes": { - "first": [ - {"action": {"proxy": "http://127.0.0.1:7081"}} - ], - "second": [ - { - "match": {"destination": ["127.0.0.1:7081"]}, - "action": {"return": 200}, - } - ], - }, - "applications": {}, - } - ), - 'proxy configure', - ) - - self.assertEqual(self.get()['status'], 200, 'proxy') - + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes/first"}, + "*:7081": {"pass": "routes/second"}, + }, + "routes": { + "first": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "second": [ + { + "match": {"destination": ["127.0.0.1:7081"]}, + "action": {"return": 200}, + } + ], + }, + "applications": {}, + } + ), 'proxy configure' -if __name__ == '__main__': - TestRouting.main() + assert self.get()['status'] == 200, 'proxy' diff --git a/test/test_routing_tls.py b/test/test_routing_tls.py index a9b8f88d..76cfb485 100644 --- a/test/test_routing_tls.py +++ b/test/test_routing_tls.py @@ -7,36 +7,22 @@ class TestRoutingTLS(TestApplicationTLS): def test_routes_match_scheme_tls(self): self.certificate() - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": { - "pass": "routes", - "tls": {"certificate": 'default'}, - }, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": { + "pass": "routes", + "tls": {"certificate": 'default'}, }, - "routes": [ - { - "match": {"scheme": "http"}, - "action": {"return": 200}, - }, - { - "match": {"scheme": "https"}, - "action": {"return": 201}, - }, - ], - "applications": {}, - } - ), - 'scheme configure', - ) + }, + "routes": [ + {"match": {"scheme": "http"}, "action": {"return": 200}}, + {"match": {"scheme": "https"}, "action": {"return": 201}}, + ], + "applications": {}, + } + ), 'scheme configure' - self.assertEqual(self.get()['status'], 200, 'http') - self.assertEqual(self.get_ssl(port=7081)['status'], 201, 'https') - - -if __name__ == '__main__': - TestRoutingTLS.main() + assert self.get()['status'] == 200, 'http' + assert self.get_ssl(port=7081)['status'] == 201, 'https' diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index 4709df6c..bdd1afb9 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -1,6 +1,8 @@ -import unittest +import pytest +import re from unit.applications.lang.ruby import TestApplicationRuby +from conftest import skip_alert class TestRubyApplication(TestApplicationRuby): @@ -21,173 +23,151 @@ class TestRubyApplication(TestApplicationRuby): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) - - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - 'Rack-Version': '13', - 'Rack-Url-Scheme': 'http', - 'Rack-Multithread': 'false', - 'Rack-Multiprocess': 'true', - 'Rack-Run-Once': 'false', - 'Rack-Hijack-Q': 'false', - 'Rack-Hijack': '', - 'Rack-Hijack-IO': '', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + 'Rack-Version': '13', + 'Rack-Url-Scheme': 'http', + 'Rack-Multithread': 'false', + 'Rack-Multiprocess': 'true', + 'Rack-Run-Once': 'false', + 'Rack-Hijack-Q': 'false', + 'Rack-Hijack': '', + 'Rack-Hijack-IO': '', + }, 'headers' + assert resp['body'] == body, 'body' def test_ruby_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String header', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String header' def test_ruby_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_ruby_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_server_port(self): self.load('server_port') - self.assertEqual( - self.get()['headers']['Server-Port'], '7080', 'Server-Port header' - ) + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' def test_ruby_application_status_int(self): self.load('status_int') - self.assertEqual(self.get()['status'], 200, 'status int') + assert self.get()['status'] == 200, 'status int' def test_ruby_application_input_read_empty(self): self.load('input_read_empty') - self.assertEqual(self.get()['body'], '', 'read empty') + assert self.get()['body'] == '', 'read empty' def test_ruby_application_input_read_parts(self): self.load('input_read_parts') - self.assertEqual( - self.post(body='0123456789')['body'], - '012345678', - 'input read parts', - ) + assert ( + self.post(body='0123456789')['body'] == '012345678' + ), 'input read parts' def test_ruby_application_input_read_buffer(self): self.load('input_read_buffer') - self.assertEqual( - self.post(body='0123456789')['body'], - '0123456789', - 'input read buffer', - ) + assert ( + self.post(body='0123456789')['body'] == '0123456789' + ), 'input read buffer' def test_ruby_application_input_read_buffer_not_empty(self): self.load('input_read_buffer_not_empty') - self.assertEqual( - self.post(body='0123456789')['body'], - '0123456789', - 'input read buffer not empty', - ) + assert ( + self.post(body='0123456789')['body'] == '0123456789' + ), 'input read buffer not empty' def test_ruby_application_input_gets(self): self.load('input_gets') body = '0123456789' - self.assertEqual(self.post(body=body)['body'], body, 'input gets') + assert self.post(body=body)['body'] == body, 'input gets' def test_ruby_application_input_gets_2(self): self.load('input_gets') - self.assertEqual( - self.post(body='01234\n56789\n')['body'], '01234\n', 'input gets 2' - ) + assert ( + self.post(body='01234\n56789\n')['body'] == '01234\n' + ), 'input gets 2' def test_ruby_application_input_gets_all(self): self.load('input_gets_all') body = '\n01234\n56789\n\n' - self.assertEqual(self.post(body=body)['body'], body, 'input gets all') + assert self.post(body=body)['body'] == body, 'input gets all' def test_ruby_application_input_each(self): self.load('input_each') body = '\n01234\n56789\n\n' - self.assertEqual(self.post(body=body)['body'], body, 'input each') + assert self.post(body=body)['body'] == body, 'input each' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_input_rewind(self): self.load('input_rewind') body = '0123456789' - self.assertEqual(self.post(body=body)['body'], body, 'input rewind') + assert self.post(body=body)['body'] == body, 'input rewind' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_syntax_error(self): - self.skip_alerts.extend( - [ - r'Failed to parse rack script', - r'syntax error', - r'new_from_string', - r'parse_file', - ] + skip_alert( + r'Failed to parse rack script', + r'syntax error', + r'new_from_string', + r'parse_file', ) self.load('syntax_error') - self.assertEqual(self.get()['status'], 500, 'syntax error') + assert self.get()['status'] == 500, 'syntax error' def test_ruby_application_errors_puts(self): self.load('errors_puts') @@ -196,10 +176,10 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application'), - 'errors puts', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application') + is not None + ), 'errors puts' def test_ruby_application_errors_puts_int(self): self.load('errors_puts_int') @@ -208,9 +188,9 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+1234567890'), 'errors puts int' - ) + assert ( + self.wait_for_record(r'\[error\].+1234567890') is not None + ), 'errors puts int' def test_ruby_application_errors_write(self): self.load('errors_write') @@ -219,15 +199,15 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application'), - 'errors write', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application') + is not None + ), 'errors write' def test_ruby_application_errors_write_to_s_custom(self): self.load('errors_write_to_s_custom') - self.assertEqual(self.get()['status'], 200, 'errors write to_s custom') + assert self.get()['status'] == 200, 'errors write to_s custom' def test_ruby_application_errors_write_int(self): self.load('errors_write_int') @@ -236,9 +216,9 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+1234567890'), 'errors write int' - ) + assert ( + self.wait_for_record(r'\[error\].+1234567890') is not None + ), 'errors write int' def test_ruby_application_at_exit(self): self.load('at_exit') @@ -249,79 +229,81 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+At exit called\.'), 'at exit' - ) + assert ( + self.wait_for_record(r'\[error\].+At exit called\.') is not None + ), 'at exit' def test_ruby_application_header_custom(self): self.load('header_custom') resp = self.post(body="\ntc=one,two\ntc=three,four,\n\n") - self.assertEqual( - resp['headers']['Custom-Header'], - ['', 'tc=one,two', 'tc=three,four,', '', ''], - 'header custom', - ) + assert resp['headers']['Custom-Header'] == [ + '', + 'tc=one,two', + 'tc=three,four,', + '', + '', + ], 'header custom' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_header_custom_non_printable(self): self.load('header_custom') - self.assertEqual( - self.post(body='\b')['status'], 500, 'header custom non printable' - ) + assert ( + self.post(body='\b')['status'] == 500 + ), 'header custom non printable' def test_ruby_application_header_status(self): self.load('header_status') - self.assertEqual(self.get()['status'], 200, 'header status') + assert self.get()['status'] == 200, 'header status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_header_rack(self): self.load('header_rack') - self.assertEqual(self.get()['status'], 500, 'header rack') + assert self.get()['status'] == 500, 'header rack' def test_ruby_application_body_empty(self): self.load('body_empty') - self.assertEqual(self.get()['body'], '', 'body empty') + assert self.get()['body'] == '', 'body empty' def test_ruby_application_body_array(self): self.load('body_array') - self.assertEqual(self.get()['body'], '0123456789', 'body array') + assert self.get()['body'] == '0123456789', 'body array' def test_ruby_application_body_large(self): self.load('mirror') body = '0123456789' * 1000 - self.assertEqual(self.post(body=body)['body'], body, 'body large') + assert self.post(body=body)['body'] == body, 'body large' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_body_each_error(self): self.load('body_each_error') - self.assertEqual(self.get()['status'], 500, 'body each error status') + assert self.get()['status'] == 500, 'body each error status' self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Failed to run ruby script'), - 'body each error', - ) + assert ( + self.wait_for_record(r'\[error\].+Failed to run ruby script') + is not None + ), 'body each error' def test_ruby_application_body_file(self): self.load('body_file') - self.assertEqual(self.get()['body'], 'body\n', 'body file') + assert self.get()['body'] == 'body\n', 'body file' def test_ruby_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -335,7 +317,7 @@ class TestRubyApplication(TestApplicationRuby): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -348,31 +330,22 @@ class TestRubyApplication(TestApplicationRuby): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_ruby_application_constants(self): self.load('constants') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] - self.assertGreater(len(headers['X-Copyright']), 0, 'RUBY_COPYRIGHT') - self.assertGreater( - len(headers['X-Description']), 0, 'RUBY_DESCRIPTION' - ) - self.assertGreater(len(headers['X-Engine']), 0, 'RUBY_ENGINE') - self.assertGreater( - len(headers['X-Engine-Version']), 0, 'RUBY_ENGINE_VERSION' - ) - self.assertGreater(len(headers['X-Patchlevel']), 0, 'RUBY_PATCHLEVEL') - self.assertGreater(len(headers['X-Platform']), 0, 'RUBY_PLATFORM') - self.assertGreater( - len(headers['X-Release-Date']), 0, 'RUBY_RELEASE_DATE' - ) - self.assertGreater(len(headers['X-Revision']), 0, 'RUBY_REVISION') - self.assertGreater(len(headers['X-Version']), 0, 'RUBY_VERSION') - -if __name__ == '__main__': - TestRubyApplication.main() + assert len(headers['X-Copyright']) > 0, 'RUBY_COPYRIGHT' + assert len(headers['X-Description']) > 0, 'RUBY_DESCRIPTION' + assert len(headers['X-Engine']) > 0, 'RUBY_ENGINE' + assert len(headers['X-Engine-Version']) > 0, 'RUBY_ENGINE_VERSION' + assert len(headers['X-Patchlevel']) > 0, 'RUBY_PATCHLEVEL' + assert len(headers['X-Platform']) > 0, 'RUBY_PLATFORM' + assert len(headers['X-Release-Date']) > 0, 'RUBY_RELEASE_DATE' + assert len(headers['X-Revision']) > 0, 'RUBY_REVISION' + assert len(headers['X-Version']) > 0, 'RUBY_VERSION' diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index 9bac162e..be20300e 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -1,9 +1,10 @@ import os +import pytest import shutil -import unittest from unit.applications.lang.ruby import TestApplicationRuby from unit.feature.isolation import TestFeatureIsolation +from conftest import option class TestRubyIsolation(TestApplicationRuby): @@ -12,60 +13,45 @@ class TestRubyIsolation(TestApplicationRuby): isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() - def test_ruby_isolation_rootfs(self): + def test_ruby_isolation_rootfs(self, is_su): isolation_features = self.available['features']['isolation'].keys() if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') - os.mkdir(self.testdir + '/ruby') + os.mkdir(self.temp_dir + '/ruby') shutil.copytree( - self.current_dir + '/ruby/status_int', - self.testdir + '/ruby/status_int', + option.test_dir + '/ruby/status_int', + self.temp_dir + '/ruby/status_int', ) isolation = { - 'namespaces': {'credential': not self.is_su, 'mount': True}, - 'rootfs': self.testdir, + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': self.temp_dir, } self.load('status_int', isolation=isolation) - self.assertIn( - 'success', - self.conf( - '"/ruby/status_int/config.ru"', - 'applications/status_int/script', - ), + assert 'success' in self.conf( + '"/ruby/status_int/config.ru"', 'applications/status_int/script', ) - self.assertIn( - 'success', - self.conf( - '"/ruby/status_int"', - 'applications/status_int/working_directory', - ), + assert 'success' in self.conf( + '"/ruby/status_int"', 'applications/status_int/working_directory', ) - self.assertEqual(self.get()['status'], 200, 'status int') - - -if __name__ == '__main__': - TestRubyIsolation.main() + assert self.get()['status'] == 200, 'status int' diff --git a/test/test_settings.py b/test/test_settings.py index 6600358d..59b4a048 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -1,8 +1,9 @@ +import pytest import socket import time -import unittest from unit.applications.lang.python import TestApplicationPython +import re class TestSettings(TestApplicationPython): @@ -32,7 +33,7 @@ Connection: close raw=True, ) - self.assertEqual(resp['status'], 408, 'status header read timeout') + assert resp['status'] == 408, 'status header read timeout' def test_settings_header_read_timeout_update(self): self.load('empty') @@ -83,9 +84,7 @@ Connection: close raw=True, ) - self.assertEqual( - resp['status'], 408, 'status header read timeout update' - ) + assert resp['status'] == 408, 'status header read timeout update' def test_settings_body_read_timeout(self): self.load('empty') @@ -109,7 +108,7 @@ Connection: close resp = self.http(b"""0123456789""", sock=sock, raw=True) - self.assertEqual(resp['status'], 408, 'status body read timeout') + assert resp['status'] == 408, 'status body read timeout' def test_settings_body_read_timeout_update(self): self.load('empty') @@ -144,9 +143,7 @@ Connection: close resp = self.http(b"""6789""", sock=sock, raw=True) - self.assertEqual( - resp['status'], 200, 'status body read timeout update' - ) + assert resp['status'] == 200, 'status body read timeout update' def test_settings_send_timeout(self): self.load('mirror') @@ -155,7 +152,7 @@ Connection: close self.conf({'http': {'send_timeout': 1}}, 'settings') - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' self.conf({"unix:" + addr: {'application': 'mirror'}}, 'listeners') @@ -182,13 +179,13 @@ Connection: close sock.close() - self.assertRegex(data, r'200 OK', 'status send timeout') - self.assertLess(len(data), data_len, 'data send timeout') + assert re.search(r'200 OK', data), 'status send timeout' + assert len(data) < data_len, 'data send timeout' def test_settings_idle_timeout(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.conf({'http': {'idle_timeout': 2}}, 'settings') @@ -204,17 +201,15 @@ Connection: close headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock ) - self.assertEqual(resp['status'], 408, 'status idle timeout') + assert resp['status'] == 408, 'status idle timeout' def test_settings_max_body_size(self): self.load('empty') self.conf({'http': {'max_body_size': 5}}, 'settings') - self.assertEqual(self.post(body='01234')['status'], 200, 'status size') - self.assertEqual( - self.post(body='012345')['status'], 413, 'status size max' - ) + assert self.post(body='01234')['status'] == 200, 'status size' + assert self.post(body='012345')['status'] == 413, 'status size max' def test_settings_max_body_size_large(self): self.load('mirror') @@ -223,32 +218,26 @@ Connection: close body = '0123456789abcdef' * 4 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 4') - self.assertEqual(resp['body'], body, 'status body 4') + assert resp['status'] == 200, 'status size 4' + assert resp['body'] == body, 'status body 4' body = '0123456789abcdef' * 8 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 8') - self.assertEqual(resp['body'], body, 'status body 8') + assert resp['status'] == 200, 'status size 8' + assert resp['body'] == body, 'status body 8' body = '0123456789abcdef' * 16 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 16') - self.assertEqual(resp['body'], body, 'status body 16') + assert resp['status'] == 200, 'status size 16' + assert resp['body'] == body, 'status body 16' body = '0123456789abcdef' * 32 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 32') - self.assertEqual(resp['body'], body, 'status body 32') + assert resp['status'] == 200, 'status size 32' + assert resp['body'] == body, 'status body 32' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_settings_negative_value(self): - self.assertIn( - 'error', - self.conf({'http': {'max_body_size': -1}}, 'settings'), - 'settings negative value', - ) - - -if __name__ == '__main__': - TestSettings.main() + assert 'error' in self.conf( + {'http': {'max_body_size': -1}}, 'settings' + ), 'settings negative value' diff --git a/test/test_share_fallback.py b/test/test_share_fallback.py index ca5e2678..391066ec 100644 --- a/test/test_share_fallback.py +++ b/test/test_share_fallback.py @@ -1,20 +1,21 @@ import os from unit.applications.proto import TestApplicationProto +from conftest import skip_alert class TestStatic(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - os.makedirs(self.testdir + '/assets/dir') - with open(self.testdir + '/assets/index.html', 'w') as index: + os.makedirs(self.temp_dir + '/assets/dir') + with open(self.temp_dir + '/assets/index.html', 'w') as index: index.write('0123456789') - os.makedirs(self.testdir + '/assets/403') - os.chmod(self.testdir + '/assets/403', 0o000) + os.makedirs(self.temp_dir + '/assets/403') + os.chmod(self.temp_dir + '/assets/403', 0o000) self._load_conf( { @@ -22,48 +23,46 @@ class TestStatic(TestApplicationProto): "*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}, }, - "routes": [{"action": {"share": self.testdir + "/assets"}}], + "routes": [{"action": {"share": self.temp_dir + "/assets"}}], "applications": {}, } ) - def tearDown(self): - os.chmod(self.testdir + '/assets/403', 0o777) + def teardown_method(self): + os.chmod(self.temp_dir + '/assets/403', 0o777) - super().tearDown() + super().teardown_method() def action_update(self, conf): - self.assertIn('success', self.conf(conf, 'routes/0/action')) + assert 'success' in self.conf(conf, 'routes/0/action') def test_fallback(self): self.action_update({"share": "/blah"}) - self.assertEqual(self.get()['status'], 404, 'bad path no fallback') + assert self.get()['status'] == 404, 'bad path no fallback' self.action_update({"share": "/blah", "fallback": {"return": 200}}) resp = self.get() - self.assertEqual(resp['status'], 200, 'bad path fallback status') - self.assertEqual(resp['body'], '', 'bad path fallback') + assert resp['status'] == 200, 'bad path fallback status' + assert resp['body'] == '', 'bad path fallback' def test_fallback_valid_path(self): self.action_update( - {"share": self.testdir + "/assets", "fallback": {"return": 200}} + {"share": self.temp_dir + "/assets", "fallback": {"return": 200}} ) resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback status') - self.assertEqual(resp['body'], '0123456789', 'fallback') + assert resp['status'] == 200, 'fallback status' + assert resp['body'] == '0123456789', 'fallback' resp = self.get(url='/403/') - self.assertEqual(resp['status'], 200, 'fallback status 403') - self.assertEqual(resp['body'], '', 'fallback 403') + assert resp['status'] == 200, 'fallback status 403' + assert resp['body'] == '', 'fallback 403' resp = self.post() - self.assertEqual(resp['status'], 200, 'fallback status 405') - self.assertEqual(resp['body'], '', 'fallback 405') + assert resp['status'] == 200, 'fallback status 405' + assert resp['body'] == '', 'fallback 405' - self.assertEqual( - self.get(url='/dir')['status'], 301, 'fallback status 301' - ) + assert self.get(url='/dir')['status'] == 301, 'fallback status 301' def test_fallback_nested(self): self.action_update( @@ -77,62 +76,56 @@ class TestStatic(TestApplicationProto): ) resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback nested status') - self.assertEqual(resp['body'], '', 'fallback nested') + assert resp['status'] == 200, 'fallback nested status' + assert resp['body'] == '', 'fallback nested' def test_fallback_share(self): self.action_update( { "share": "/blah", - "fallback": {"share": self.testdir + "/assets"}, + "fallback": {"share": self.temp_dir + "/assets"}, } ) resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback share status') - self.assertEqual(resp['body'], '0123456789', 'fallback share') + assert resp['status'] == 200, 'fallback share status' + assert resp['body'] == '0123456789', 'fallback share' resp = self.head() - self.assertEqual(resp['status'], 200, 'fallback share status HEAD') - self.assertEqual(resp['body'], '', 'fallback share HEAD') + assert resp['status'] == 200, 'fallback share status HEAD' + assert resp['body'] == '', 'fallback share HEAD' - self.assertEqual( - self.get(url='/dir')['status'], 301, 'fallback share status 301' - ) + assert ( + self.get(url='/dir')['status'] == 301 + ), 'fallback share status 301' def test_fallback_proxy(self): - self.assertIn( - 'success', - self.conf( - [ - { - "match": {"destination": "*:7081"}, - "action": {"return": 200}, - }, - { - "action": { - "share": "/blah", - "fallback": {"proxy": "http://127.0.0.1:7081"}, - } - }, - ], - 'routes', - ), - 'configure fallback proxy route', - ) + assert 'success' in self.conf( + [ + { + "match": {"destination": "*:7081"}, + "action": {"return": 200}, + }, + { + "action": { + "share": "/blah", + "fallback": {"proxy": "http://127.0.0.1:7081"}, + } + }, + ], + 'routes', + ), 'configure fallback proxy route' resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback proxy status') - self.assertEqual(resp['body'], '', 'fallback proxy') + assert resp['status'] == 200, 'fallback proxy status' + assert resp['body'] == '', 'fallback proxy' def test_fallback_proxy_loop(self): - self.skip_alerts.extend( - [ - r'open.*/blah/index.html.*failed', - r'accept.*failed', - r'socket.*failed', - r'new connections are not accepted', - ] + skip_alert( + r'open.*/blah/index.html.*failed', + r'accept.*failed', + r'socket.*failed', + r'new connections are not accepted', ) self.action_update( @@ -140,12 +133,12 @@ class TestStatic(TestApplicationProto): ) self.get(no_recv=True) - self.assertIn('success', self.conf_delete('listeners/*:7081')) + assert 'success' in self.conf_delete('listeners/*:7081') self.get(read_timeout=1) def test_fallback_invalid(self): def check_error(conf): - self.assertIn('error', self.conf(conf, 'routes/0/action')) + assert 'error' in self.conf(conf, 'routes/0/action') check_error({"share": "/blah", "fallback": {}}) check_error({"share": "/blah", "fallback": ""}) @@ -154,7 +147,3 @@ class TestStatic(TestApplicationProto): {"proxy": "http://127.0.0.1:7081", "fallback": {"share": "/blah"}} ) check_error({"fallback": {"share": "/blah"}}) - - -if __name__ == '__main__': - TestStatic.main() diff --git a/test/test_static.py b/test/test_static.py index bee5db28..fc8bb0a9 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -1,21 +1,23 @@ import os +import pytest import socket -import unittest from unit.applications.proto import TestApplicationProto +from conftest import waitforfiles class TestStatic(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - os.makedirs(self.testdir + '/assets/dir') - with open(self.testdir + '/assets/index.html', 'w') as index, \ - open(self.testdir + '/assets/README', 'w') as readme, \ - open(self.testdir + '/assets/log.log', 'w') as log, \ - open(self.testdir + '/assets/dir/file', 'w') as file: + os.makedirs(self.temp_dir + '/assets/dir') + with open(self.temp_dir + '/assets/index.html', 'w') as index, open( + self.temp_dir + '/assets/README', 'w' + ) as readme, open(self.temp_dir + '/assets/log.log', 'w') as log, open( + self.temp_dir + '/assets/dir/file', 'w' + ) as file: index.write('0123456789') readme.write('readme') log.write('[debug]') @@ -24,7 +26,7 @@ class TestStatic(TestApplicationProto): self._load_conf( { "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"share": self.testdir + "/assets"}}], + "routes": [{"action": {"share": self.temp_dir + "/assets"}}], "settings": { "http": { "static": { @@ -36,123 +38,95 @@ class TestStatic(TestApplicationProto): ) def test_static_index(self): - self.assertEqual( - self.get(url='/index.html')['body'], '0123456789', 'index' - ) - self.assertEqual(self.get(url='/')['body'], '0123456789', 'index 2') - self.assertEqual(self.get(url='//')['body'], '0123456789', 'index 3') - self.assertEqual(self.get(url='/.')['body'], '0123456789', 'index 4') - self.assertEqual(self.get(url='/./')['body'], '0123456789', 'index 5') - self.assertEqual( - self.get(url='/?blah')['body'], '0123456789', 'index vars' - ) - self.assertEqual( - self.get(url='/#blah')['body'], '0123456789', 'index anchor' - ) - self.assertEqual( - self.get(url='/dir/')['status'], 404, 'index not found' - ) + assert self.get(url='/index.html')['body'] == '0123456789', 'index' + assert self.get(url='/')['body'] == '0123456789', 'index 2' + assert self.get(url='//')['body'] == '0123456789', 'index 3' + assert self.get(url='/.')['body'] == '0123456789', 'index 4' + assert self.get(url='/./')['body'] == '0123456789', 'index 5' + assert self.get(url='/?blah')['body'] == '0123456789', 'index vars' + assert self.get(url='/#blah')['body'] == '0123456789', 'index anchor' + assert self.get(url='/dir/')['status'] == 404, 'index not found' resp = self.get(url='/index.html/') - self.assertEqual(resp['status'], 404, 'index not found 2 status') - self.assertEqual( - resp['headers']['Content-Type'], - 'text/html', - 'index not found 2 Content-Type', - ) + assert resp['status'] == 404, 'index not found 2 status' + assert ( + resp['headers']['Content-Type'] == 'text/html' + ), 'index not found 2 Content-Type' def test_static_large_file(self): file_size = 32 * 1024 * 1024 - with open(self.testdir + '/assets/large', 'wb') as f: + with open(self.temp_dir + '/assets/large', 'wb') as f: f.seek(file_size - 1) f.write(b'\0') - self.assertEqual( - len( - self.get(url='/large', read_buffer_size=1024 * 1024)['body'] - ), - file_size, - 'large file', - ) + assert ( + len(self.get(url='/large', read_buffer_size=1024 * 1024)['body']) + == file_size + ), 'large file' def test_static_etag(self): etag = self.get(url='/')['headers']['ETag'] etag_2 = self.get(url='/README')['headers']['ETag'] - self.assertNotEqual(etag, etag_2, 'different ETag') - self.assertEqual( - etag, self.get(url='/')['headers']['ETag'], 'same ETag' - ) + assert etag != etag_2, 'different ETag' + assert etag == self.get(url='/')['headers']['ETag'], 'same ETag' - with open(self.testdir + '/assets/index.html', 'w') as f: + with open(self.temp_dir + '/assets/index.html', 'w') as f: f.write('blah') - self.assertNotEqual( - etag, self.get(url='/')['headers']['ETag'], 'new ETag' - ) + assert etag != self.get(url='/')['headers']['ETag'], 'new ETag' def test_static_redirect(self): resp = self.get(url='/dir') - self.assertEqual(resp['status'], 301, 'redirect status') - self.assertEqual( - resp['headers']['Location'], '/dir/', 'redirect Location' - ) - self.assertNotIn( - 'Content-Type', resp['headers'], 'redirect Content-Type' - ) + assert resp['status'] == 301, 'redirect status' + assert resp['headers']['Location'] == '/dir/', 'redirect Location' + assert 'Content-Type' not in resp['headers'], 'redirect Content-Type' def test_static_space_in_name(self): os.rename( - self.testdir + '/assets/dir/file', - self.testdir + '/assets/dir/fi le', - ) - self.waitforfiles(self.testdir + '/assets/dir/fi le') - self.assertEqual( - self.get(url='/dir/fi le')['body'], 'blah', 'file name' + self.temp_dir + '/assets/dir/file', + self.temp_dir + '/assets/dir/fi le', ) + assert waitforfiles(self.temp_dir + '/assets/dir/fi le') + assert self.get(url='/dir/fi le')['body'] == 'blah', 'file name' - os.rename(self.testdir + '/assets/dir', self.testdir + '/assets/di r') - self.waitforfiles(self.testdir + '/assets/di r/fi le') - self.assertEqual( - self.get(url='/di r/fi le')['body'], 'blah', 'dir name' - ) + os.rename(self.temp_dir + '/assets/dir', self.temp_dir + '/assets/di r') + assert waitforfiles(self.temp_dir + '/assets/di r/fi le') + assert self.get(url='/di r/fi le')['body'] == 'blah', 'dir name' os.rename( - self.testdir + '/assets/di r', self.testdir + '/assets/ di r ' - ) - self.waitforfiles(self.testdir + '/assets/ di r /fi le') - self.assertEqual( - self.get(url='/ di r /fi le')['body'], 'blah', 'dir name enclosing' - ) - - self.assertEqual( - self.get(url='/%20di%20r%20/fi le')['body'], 'blah', 'dir encoded' - ) - self.assertEqual( - self.get(url='/ di r %2Ffi le')['body'], 'blah', 'slash encoded' - ) - self.assertEqual( - self.get(url='/ di r /fi%20le')['body'], 'blah', 'file encoded' - ) - self.assertEqual( - self.get(url='/%20di%20r%20%2Ffi%20le')['body'], 'blah', 'encoded' - ) - self.assertEqual( - self.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body'], - 'blah', - 'encoded 2', - ) + self.temp_dir + '/assets/di r', self.temp_dir + '/assets/ di r ' + ) + assert waitforfiles(self.temp_dir + '/assets/ di r /fi le') + assert ( + self.get(url='/ di r /fi le')['body'] == 'blah' + ), 'dir name enclosing' + + assert ( + self.get(url='/%20di%20r%20/fi le')['body'] == 'blah' + ), 'dir encoded' + assert ( + self.get(url='/ di r %2Ffi le')['body'] == 'blah' + ), 'slash encoded' + assert ( + self.get(url='/ di r /fi%20le')['body'] == 'blah' + ), 'file encoded' + assert ( + self.get(url='/%20di%20r%20%2Ffi%20le')['body'] == 'blah' + ), 'encoded' + assert ( + self.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body'] + == 'blah' + ), 'encoded 2' os.rename( - self.testdir + '/assets/ di r /fi le', - self.testdir + '/assets/ di r / fi le ', - ) - self.waitforfiles(self.testdir + '/assets/ di r / fi le ') - self.assertEqual( - self.get(url='/%20di%20r%20/%20fi%20le%20')['body'], - 'blah', - 'file name enclosing', + self.temp_dir + '/assets/ di r /fi le', + self.temp_dir + '/assets/ di r / fi le ', ) + assert waitforfiles(self.temp_dir + '/assets/ di r / fi le ') + assert ( + self.get(url='/%20di%20r%20/%20fi%20le%20')['body'] == 'blah' + ), 'file name enclosing' try: print('файл') @@ -163,267 +137,190 @@ class TestStatic(TestApplicationProto): if utf8: os.rename( - self.testdir + '/assets/ di r / fi le ', - self.testdir + '/assets/ di r /фа йл', - ) - self.waitforfiles(self.testdir + '/assets/ di r /фа йл') - self.assertEqual( - self.get(url='/ di r /фа йл')['body'], 'blah', 'file name 2' + self.temp_dir + '/assets/ di r / fi le ', + self.temp_dir + '/assets/ di r /фа йл', ) + assert waitforfiles(self.temp_dir + '/assets/ di r /фа йл') + assert ( + self.get(url='/ di r /фа йл')['body'] == 'blah' + ), 'file name 2' os.rename( - self.testdir + '/assets/ di r ', - self.testdir + '/assets/ди ректория', - ) - self.waitforfiles(self.testdir + '/assets/ди ректория/фа йл') - self.assertEqual( - self.get(url='/ди ректория/фа йл')['body'], 'blah', 'dir name 2' + self.temp_dir + '/assets/ di r ', + self.temp_dir + '/assets/ди ректория', ) + assert waitforfiles(self.temp_dir + '/assets/ди ректория/фа йл') + assert ( + self.get(url='/ди ректория/фа йл')['body'] == 'blah' + ), 'dir name 2' def test_static_unix_socket(self): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.bind(self.testdir + '/assets/unix_socket') + sock.bind(self.temp_dir + '/assets/unix_socket') - self.assertEqual(self.get(url='/unix_socket')['status'], 404, 'socket') + assert self.get(url='/unix_socket')['status'] == 404, 'socket' sock.close() def test_static_unix_fifo(self): - os.mkfifo(self.testdir + '/assets/fifo') + os.mkfifo(self.temp_dir + '/assets/fifo') - self.assertEqual(self.get(url='/fifo')['status'], 404, 'fifo') + assert self.get(url='/fifo')['status'] == 404, 'fifo' def test_static_symlink(self): - os.symlink(self.testdir + '/assets/dir', self.testdir + '/assets/link') + os.symlink(self.temp_dir + '/assets/dir', self.temp_dir + '/assets/link') - self.assertEqual(self.get(url='/dir')['status'], 301, 'dir') - self.assertEqual(self.get(url='/dir/file')['status'], 200, 'file') - self.assertEqual(self.get(url='/link')['status'], 301, 'symlink dir') - self.assertEqual( - self.get(url='/link/file')['status'], 200, 'symlink file' - ) + 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() - self.assertEqual(resp['status'], 200, 'HEAD status') - self.assertEqual(resp['body'], '', 'HEAD empty body') + assert resp['status'] == 200, 'HEAD status' + assert resp['body'] == '', 'HEAD empty body' - self.assertEqual(self.delete()['status'], 405, 'DELETE') - self.assertEqual(self.post()['status'], 405, 'POST') - self.assertEqual(self.put()['status'], 405, 'PUT') + assert self.delete()['status'] == 405, 'DELETE' + assert self.post()['status'] == 405, 'POST' + assert self.put()['status'] == 405, 'PUT' def test_static_path(self): - self.assertEqual( - self.get(url='/dir/../dir/file')['status'], 200, 'relative' - ) + assert self.get(url='/dir/../dir/file')['status'] == 200, 'relative' - self.assertEqual(self.get(url='./')['status'], 400, 'path invalid') - self.assertEqual(self.get(url='../')['status'], 400, 'path invalid 2') - self.assertEqual(self.get(url='/..')['status'], 400, 'path invalid 3') - self.assertEqual( - self.get(url='../assets/')['status'], 400, 'path invalid 4' - ) - self.assertEqual( - self.get(url='/../assets/')['status'], 400, 'path invalid 5' - ) + assert self.get(url='./')['status'] == 400, 'path invalid' + assert self.get(url='../')['status'] == 400, 'path invalid 2' + assert self.get(url='/..')['status'] == 400, 'path invalid 3' + assert self.get(url='../assets/')['status'] == 400, 'path invalid 4' + assert self.get(url='/../assets/')['status'] == 400, 'path invalid 5' def test_static_two_clients(self): _, sock = self.get(url='/', start=True, no_recv=True) _, sock2 = self.get(url='/', start=True, no_recv=True) - self.assertEqual(sock.recv(1), b'H', 'client 1') - self.assertEqual(sock2.recv(1), b'H', 'client 2') - self.assertEqual(sock.recv(1), b'T', 'client 1 again') - self.assertEqual(sock2.recv(1), b'T', 'client 2 again') + assert sock.recv(1) == b'H', 'client 1' + assert sock2.recv(1) == b'H', 'client 2' + assert sock.recv(1) == b'T', 'client 1 again' + assert sock2.recv(1) == b'T', 'client 2 again' sock.close() sock2.close() def test_static_mime_types(self): - self.assertIn( - 'success', - self.conf( - { - "text/x-code/x-blah/x-blah": "readme", - "text/plain": [".html", ".log", "file"], - }, - 'settings/http/static/mime_types', - ), - 'configure mime_types', - ) - - self.assertEqual( - self.get(url='/README')['headers']['Content-Type'], - 'text/x-code/x-blah/x-blah', - 'mime_types string case insensitive', - ) - self.assertEqual( - self.get(url='/index.html')['headers']['Content-Type'], - 'text/plain', - 'mime_types html', - ) - self.assertEqual( - self.get(url='/')['headers']['Content-Type'], - 'text/plain', - 'mime_types index default', - ) - self.assertEqual( - self.get(url='/dir/file')['headers']['Content-Type'], - 'text/plain', - 'mime_types file in dir', - ) + assert 'success' in self.conf( + { + "text/x-code/x-blah/x-blah": "readme", + "text/plain": [".html", ".log", "file"], + }, + 'settings/http/static/mime_types', + ), 'configure mime_types' + + assert ( + self.get(url='/README')['headers']['Content-Type'] + == 'text/x-code/x-blah/x-blah' + ), 'mime_types string case insensitive' + assert ( + self.get(url='/index.html')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types html' + assert ( + self.get(url='/')['headers']['Content-Type'] == 'text/plain' + ), 'mime_types index default' + assert ( + self.get(url='/dir/file')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types file in dir' def test_static_mime_types_partial_match(self): - self.assertIn( - 'success', - self.conf( - { - "text/x-blah": ["ile", "fil", "f", "e", ".file"], - }, - 'settings/http/static/mime_types', - ), - 'configure mime_types', - ) - self.assertNotIn( - 'Content-Type', self.get(url='/dir/file'), 'partial match' - ) + assert 'success' in self.conf( + {"text/x-blah": ["ile", "fil", "f", "e", ".file"],}, + 'settings/http/static/mime_types', + ), 'configure mime_types' + assert 'Content-Type' not in self.get(url='/dir/file'), 'partial match' def test_static_mime_types_reconfigure(self): - self.assertIn( - 'success', - self.conf( - { - "text/x-code": "readme", - "text/plain": [".html", ".log", "file"], - }, - 'settings/http/static/mime_types', - ), - 'configure mime_types', - ) - - self.assertEqual( - self.conf_get('settings/http/static/mime_types'), - {'text/x-code': 'readme', 'text/plain': ['.html', '.log', 'file']}, - 'mime_types get', - ) - self.assertEqual( - self.conf_get('settings/http/static/mime_types/text%2Fx-code'), - 'readme', - 'mime_types get string', - ) - self.assertEqual( - self.conf_get('settings/http/static/mime_types/text%2Fplain'), - ['.html', '.log', 'file'], - 'mime_types get array', - ) - self.assertEqual( - self.conf_get('settings/http/static/mime_types/text%2Fplain/1'), - '.log', - 'mime_types get array element', - ) - - self.assertIn( - 'success', - self.conf_delete('settings/http/static/mime_types/text%2Fplain/2'), - 'mime_types remove array element', - ) - self.assertNotIn( - 'Content-Type', - self.get(url='/dir/file')['headers'], - 'mime_types removed', - ) - - self.assertIn( - 'success', - self.conf_post( - '"file"', 'settings/http/static/mime_types/text%2Fplain' - ), - 'mime_types add array element', - ) - self.assertEqual( - self.get(url='/dir/file')['headers']['Content-Type'], - 'text/plain', - 'mime_types reverted', - ) - - self.assertIn( - 'success', - self.conf( - '"file"', 'settings/http/static/mime_types/text%2Fplain' - ), - 'configure mime_types update', - ) - self.assertEqual( - self.get(url='/dir/file')['headers']['Content-Type'], - 'text/plain', - 'mime_types updated', - ) - self.assertNotIn( - 'Content-Type', - self.get(url='/log.log')['headers'], - 'mime_types updated 2', - ) - - self.assertIn( - 'success', - self.conf( - '".log"', 'settings/http/static/mime_types/text%2Fblahblahblah' - ), - 'configure mime_types create', - ) - self.assertEqual( - self.get(url='/log.log')['headers']['Content-Type'], - 'text/blahblahblah', - 'mime_types create', - ) + assert 'success' in self.conf( + { + "text/x-code": "readme", + "text/plain": [".html", ".log", "file"], + }, + 'settings/http/static/mime_types', + ), 'configure mime_types' + + assert self.conf_get('settings/http/static/mime_types') == { + 'text/x-code': 'readme', + 'text/plain': ['.html', '.log', 'file'], + }, 'mime_types get' + assert ( + self.conf_get('settings/http/static/mime_types/text%2Fx-code') + == 'readme' + ), 'mime_types get string' + assert self.conf_get( + 'settings/http/static/mime_types/text%2Fplain' + ) == ['.html', '.log', 'file'], 'mime_types get array' + assert ( + self.conf_get('settings/http/static/mime_types/text%2Fplain/1') + == '.log' + ), 'mime_types get array element' + + assert 'success' in self.conf_delete( + 'settings/http/static/mime_types/text%2Fplain/2' + ), 'mime_types remove array element' + assert ( + 'Content-Type' not in self.get(url='/dir/file')['headers'] + ), 'mime_types removed' + + assert 'success' in self.conf_post( + '"file"', 'settings/http/static/mime_types/text%2Fplain' + ), 'mime_types add array element' + assert ( + self.get(url='/dir/file')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types reverted' + + assert 'success' in self.conf( + '"file"', 'settings/http/static/mime_types/text%2Fplain' + ), 'configure mime_types update' + assert ( + self.get(url='/dir/file')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types updated' + assert ( + 'Content-Type' not in self.get(url='/log.log')['headers'] + ), 'mime_types updated 2' + + assert 'success' in self.conf( + '".log"', 'settings/http/static/mime_types/text%2Fblahblahblah' + ), 'configure mime_types create' + assert ( + self.get(url='/log.log')['headers']['Content-Type'] + == 'text/blahblahblah' + ), 'mime_types create' def test_static_mime_types_correct(self): - self.assertIn( - 'error', - self.conf( - {"text/x-code": "readme", "text/plain": "readme"}, - 'settings/http/static/mime_types', - ), - 'mime_types same extensions', - ) - self.assertIn( - 'error', - self.conf( - {"text/x-code": [".h", ".c"], "text/plain": ".c"}, - 'settings/http/static/mime_types', - ), - 'mime_types same extensions array', - ) - self.assertIn( - 'error', - self.conf( - { - "text/x-code": [".h", ".c", "readme"], - "text/plain": "README", - }, - 'settings/http/static/mime_types', - ), - 'mime_types same extensions case insensitive', - ) - - @unittest.skip('not yet') + assert 'error' in self.conf( + {"text/x-code": "readme", "text/plain": "readme"}, + 'settings/http/static/mime_types', + ), 'mime_types same extensions' + assert 'error' in self.conf( + {"text/x-code": [".h", ".c"], "text/plain": ".c"}, + 'settings/http/static/mime_types', + ), 'mime_types same extensions array' + assert 'error' in self.conf( + {"text/x-code": [".h", ".c", "readme"], "text/plain": "README",}, + 'settings/http/static/mime_types', + ), 'mime_types same extensions case insensitive' + + @pytest.mark.skip('not yet') def test_static_mime_types_invalid(self): - self.assertIn( - 'error', - self.http( - b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r + assert 'error' in self.http( + b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r Host: localhost\r Connection: close\r Content-Length: 6\r \r \"blah\"""", - raw_resp=True, - raw=True, - sock_type='unix', - addr=self.testdir + '/control.unit.sock', - ), - 'mime_types invalid', - ) - -if __name__ == '__main__': - TestStatic.main() + raw_resp=True, + raw=True, + sock_type='unix', + addr=self.temp_dir + '/control.unit.sock', + ), 'mime_types invalid' diff --git a/test/test_tls.py b/test/test_tls.py index a0434174..9881e973 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -1,17 +1,18 @@ import io +import pytest import re import ssl import subprocess -import unittest from unit.applications.tls import TestApplicationTLS +from conftest import skip_alert class TestTLS(TestApplicationTLS): prerequisites = {'modules': {'python': 'any', 'openssl': 'any'}} def findall(self, pattern): - with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) def openssl_date_to_sec_epoch(self, date): @@ -38,7 +39,7 @@ class TestTLS(TestApplicationTLS): self.add_tls() - self.assertEqual(self.get_ssl()['status'], 200, 'add listener option') + assert self.get_ssl()['status'] == 200, 'add listener option' def test_tls_listener_option_remove(self): self.load('empty') @@ -51,18 +52,16 @@ class TestTLS(TestApplicationTLS): self.remove_tls() - self.assertEqual(self.get()['status'], 200, 'remove listener option') + assert self.get()['status'] == 200, 'remove listener option' def test_tls_certificate_remove(self): self.load('empty') self.certificate() - self.assertIn( - 'success', - self.conf_delete('/certificates/default'), - 'remove certificate', - ) + assert 'success' in self.conf_delete( + '/certificates/default' + ), 'remove certificate' def test_tls_certificate_remove_used(self): self.load('empty') @@ -71,11 +70,9 @@ class TestTLS(TestApplicationTLS): self.add_tls() - self.assertIn( - 'error', - self.conf_delete('/certificates/default'), - 'remove certificate', - ) + assert 'error' in self.conf_delete( + '/certificates/default' + ), 'remove certificate' def test_tls_certificate_remove_nonexisting(self): self.load('empty') @@ -84,13 +81,11 @@ class TestTLS(TestApplicationTLS): self.add_tls() - self.assertIn( - 'error', - self.conf_delete('/certificates/blah'), - 'remove nonexistings certificate', - ) + assert 'error' in self.conf_delete( + '/certificates/blah' + ), 'remove nonexistings certificate' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_certificate_update(self): self.load('empty') @@ -102,20 +97,18 @@ class TestTLS(TestApplicationTLS): self.certificate() - self.assertNotEqual( - cert_old, self.get_server_certificate(), 'update certificate' - ) + assert cert_old != self.get_server_certificate(), 'update certificate' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_certificate_key_incorrect(self): self.load('empty') self.certificate('first', False) self.certificate('second', False) - self.assertIn( - 'error', self.certificate_load('first', 'second'), 'key incorrect' - ) + assert 'error' in self.certificate_load( + 'first', 'second' + ), 'key incorrect' def test_tls_certificate_change(self): self.load('empty') @@ -129,20 +122,16 @@ class TestTLS(TestApplicationTLS): self.add_tls(cert='new') - self.assertNotEqual( - cert_old, self.get_server_certificate(), 'change certificate' - ) + assert cert_old != self.get_server_certificate(), 'change certificate' def test_tls_certificate_key_rsa(self): self.load('empty') self.certificate() - self.assertEqual( - self.conf_get('/certificates/default/key'), - 'RSA (2048 bits)', - 'certificate key rsa', - ) + assert ( + self.conf_get('/certificates/default/key') == 'RSA (2048 bits)' + ), 'certificate key rsa' def test_tls_certificate_key_ec(self): self.load('empty') @@ -155,8 +144,10 @@ class TestTLS(TestApplicationTLS): 'ecparam', '-noout', '-genkey', - '-out', self.testdir + '/ec.key', - '-name', 'prime256v1', + '-out', + self.temp_dir + '/ec.key', + '-name', + 'prime256v1', ], stderr=subprocess.STDOUT, ) @@ -167,19 +158,23 @@ class TestTLS(TestApplicationTLS): 'req', '-x509', '-new', - '-subj', '/CN=ec/', - '-config', self.testdir + '/openssl.conf', - '-key', self.testdir + '/ec.key', - '-out', self.testdir + '/ec.crt', + '-subj', + '/CN=ec/', + '-config', + self.temp_dir + '/openssl.conf', + '-key', + self.temp_dir + '/ec.key', + '-out', + self.temp_dir + '/ec.crt', ], stderr=subprocess.STDOUT, ) self.certificate_load('ec') - self.assertEqual( - self.conf_get('/certificates/ec/key'), 'ECDH', 'certificate key ec' - ) + assert ( + self.conf_get('/certificates/ec/key') == 'ECDH' + ), 'certificate key ec' def test_tls_certificate_chain_options(self): self.load('empty') @@ -188,35 +183,29 @@ class TestTLS(TestApplicationTLS): chain = self.conf_get('/certificates/default/chain') - self.assertEqual(len(chain), 1, 'certificate chain length') + assert len(chain) == 1, 'certificate chain length' cert = chain[0] - self.assertEqual( - cert['subject']['common_name'], - 'default', - 'certificate subject common name', - ) - self.assertEqual( - cert['issuer']['common_name'], - 'default', - 'certificate issuer common name', - ) + assert ( + cert['subject']['common_name'] == 'default' + ), 'certificate subject common name' + assert ( + cert['issuer']['common_name'] == 'default' + ), 'certificate issuer common name' - self.assertLess( + assert ( abs( self.sec_epoch() - self.openssl_date_to_sec_epoch(cert['validity']['since']) - ), - 5, - 'certificate validity since', - ) - self.assertEqual( + ) + < 5 + ), 'certificate validity since' + assert ( self.openssl_date_to_sec_epoch(cert['validity']['until']) - - self.openssl_date_to_sec_epoch(cert['validity']['since']), - 2592000, - 'certificate validity until', - ) + - self.openssl_date_to_sec_epoch(cert['validity']['since']) + == 2592000 + ), 'certificate validity until' def test_tls_certificate_chain(self): self.load('empty') @@ -228,10 +217,14 @@ class TestTLS(TestApplicationTLS): 'openssl', 'req', '-new', - '-subj', '/CN=int/', - '-config', self.testdir + '/openssl.conf', - '-out', self.testdir + '/int.csr', - '-keyout', self.testdir + '/int.key', + '-subj', + '/CN=int/', + '-config', + self.temp_dir + '/openssl.conf', + '-out', + self.temp_dir + '/int.csr', + '-keyout', + self.temp_dir + '/int.key', ], stderr=subprocess.STDOUT, ) @@ -241,15 +234,19 @@ class TestTLS(TestApplicationTLS): 'openssl', 'req', '-new', - '-subj', '/CN=end/', - '-config', self.testdir + '/openssl.conf', - '-out', self.testdir + '/end.csr', - '-keyout', self.testdir + '/end.key', + '-subj', + '/CN=end/', + '-config', + self.temp_dir + '/openssl.conf', + '-out', + self.temp_dir + '/end.csr', + '-keyout', + self.temp_dir + '/end.key', ], stderr=subprocess.STDOUT, ) - with open(self.testdir + '/ca.conf', 'w') as f: + with open(self.temp_dir + '/ca.conf', 'w') as f: f.write( """[ ca ] default_ca = myca @@ -269,16 +266,16 @@ commonName = supplied [ myca_extensions ] basicConstraints = critical,CA:TRUE""" % { - 'dir': self.testdir, - 'database': self.testdir + '/certindex', - 'certserial': self.testdir + '/certserial', + 'dir': self.temp_dir, + 'database': self.temp_dir + '/certindex', + 'certserial': self.temp_dir + '/certserial', } ) - with open(self.testdir + '/certserial', 'w') as f: + with open(self.temp_dir + '/certserial', 'w') as f: f.write('1000') - with open(self.testdir + '/certindex', 'w') as f: + with open(self.temp_dir + '/certindex', 'w') as f: f.write('') subprocess.call( @@ -286,12 +283,18 @@ basicConstraints = critical,CA:TRUE""" 'openssl', 'ca', '-batch', - '-subj', '/CN=int/', - '-config', self.testdir + '/ca.conf', - '-keyfile', self.testdir + '/root.key', - '-cert', self.testdir + '/root.crt', - '-in', self.testdir + '/int.csr', - '-out', self.testdir + '/int.crt', + '-subj', + '/CN=int/', + '-config', + self.temp_dir + '/ca.conf', + '-keyfile', + self.temp_dir + '/root.key', + '-cert', + self.temp_dir + '/root.crt', + '-in', + self.temp_dir + '/int.csr', + '-out', + self.temp_dir + '/int.crt', ], stderr=subprocess.STDOUT, ) @@ -301,50 +304,50 @@ basicConstraints = critical,CA:TRUE""" 'openssl', 'ca', '-batch', - '-subj', '/CN=end/', - '-config', self.testdir + '/ca.conf', - '-keyfile', self.testdir + '/int.key', - '-cert', self.testdir + '/int.crt', - '-in', self.testdir + '/end.csr', - '-out', self.testdir + '/end.crt', + '-subj', + '/CN=end/', + '-config', + self.temp_dir + '/ca.conf', + '-keyfile', + self.temp_dir + '/int.key', + '-cert', + self.temp_dir + '/int.crt', + '-in', + self.temp_dir + '/end.csr', + '-out', + self.temp_dir + '/end.crt', ], stderr=subprocess.STDOUT, ) - crt_path = self.testdir + '/end-int.crt' - end_path = self.testdir + '/end.crt' - int_path = self.testdir + '/int.crt' + crt_path = self.temp_dir + '/end-int.crt' + end_path = self.temp_dir + '/end.crt' + int_path = self.temp_dir + '/int.crt' - with open(crt_path, 'wb') as crt, \ - open(end_path, 'rb') as end, \ - open(int_path, 'rb') as int: + with open(crt_path, 'wb') as crt, open(end_path, 'rb') as end, open( + int_path, 'rb' + ) 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(self.testdir + '/root.crt') + self.context.load_verify_locations(self.temp_dir + '/root.crt') # incomplete chain - self.assertIn( - 'success', - self.certificate_load('end', 'end'), - 'certificate chain end upload', - ) + assert 'success' in self.certificate_load( + 'end', 'end' + ), 'certificate chain end upload' chain = self.conf_get('/certificates/end/chain') - self.assertEqual(len(chain), 1, 'certificate chain end length') - self.assertEqual( - chain[0]['subject']['common_name'], - 'end', - 'certificate chain end subject common name', - ) - self.assertEqual( - chain[0]['issuer']['common_name'], - 'int', - 'certificate chain end issuer common name', - ) + assert len(chain) == 1, 'certificate chain end length' + assert ( + chain[0]['subject']['common_name'] == 'end' + ), 'certificate chain end subject common name' + assert ( + chain[0]['issuer']['common_name'] == 'int' + ), 'certificate chain end issuer common name' self.add_tls(cert='end') @@ -353,79 +356,61 @@ basicConstraints = critical,CA:TRUE""" except ssl.SSLError: resp = None - self.assertEqual(resp, None, 'certificate chain incomplete chain') + assert resp == None, 'certificate chain incomplete chain' # intermediate - self.assertIn( - 'success', - self.certificate_load('int', 'int'), - 'certificate chain int upload', - ) + assert 'success' in self.certificate_load( + 'int', 'int' + ), 'certificate chain int upload' chain = self.conf_get('/certificates/int/chain') - self.assertEqual(len(chain), 1, 'certificate chain int length') - self.assertEqual( - chain[0]['subject']['common_name'], - 'int', - 'certificate chain int subject common name', - ) - self.assertEqual( - chain[0]['issuer']['common_name'], - 'root', - 'certificate chain int issuer common name', - ) + assert len(chain) == 1, 'certificate chain int length' + assert ( + chain[0]['subject']['common_name'] == 'int' + ), 'certificate chain int subject common name' + assert ( + chain[0]['issuer']['common_name'] == 'root' + ), 'certificate chain int issuer common name' self.add_tls(cert='int') - self.assertEqual( - self.get_ssl()['status'], 200, 'certificate chain intermediate' - ) + assert ( + self.get_ssl()['status'] == 200 + ), 'certificate chain intermediate' # intermediate server - self.assertIn( - 'success', - self.certificate_load('end-int', 'end'), - 'certificate chain end-int upload', - ) + assert 'success' in self.certificate_load( + 'end-int', 'end' + ), 'certificate chain end-int upload' chain = self.conf_get('/certificates/end-int/chain') - self.assertEqual(len(chain), 2, 'certificate chain end-int length') - self.assertEqual( - chain[0]['subject']['common_name'], - 'end', - 'certificate chain end-int int subject common name', - ) - self.assertEqual( - chain[0]['issuer']['common_name'], - 'int', - 'certificate chain end-int int issuer common name', - ) - self.assertEqual( - chain[1]['subject']['common_name'], - 'int', - 'certificate chain end-int end subject common name', - ) - self.assertEqual( - chain[1]['issuer']['common_name'], - 'root', - 'certificate chain end-int end issuer common name', - ) + assert len(chain) == 2, 'certificate chain end-int length' + assert ( + chain[0]['subject']['common_name'] == 'end' + ), 'certificate chain end-int int subject common name' + assert ( + chain[0]['issuer']['common_name'] == 'int' + ), 'certificate chain end-int int issuer common name' + assert ( + chain[1]['subject']['common_name'] == 'int' + ), 'certificate chain end-int end subject common name' + assert ( + chain[1]['issuer']['common_name'] == 'root' + ), 'certificate chain end-int end issuer common name' self.add_tls(cert='end-int') - self.assertEqual( - self.get_ssl()['status'], - 200, - 'certificate chain intermediate server', - ) + assert ( + self.get_ssl()['status'] == 200 + ), 'certificate chain intermediate server' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_reconfigure(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.certificate() @@ -435,21 +420,17 @@ basicConstraints = critical,CA:TRUE""" read_timeout=1, ) - self.assertEqual(resp['status'], 200, 'initial status') + assert resp['status'] == 200, 'initial status' self.add_tls() - self.assertEqual( - self.get(sock=sock)['status'], 200, 'reconfigure status' - ) - self.assertEqual( - self.get_ssl()['status'], 200, 'reconfigure tls status' - ) + assert self.get(sock=sock)['status'] == 200, 'reconfigure status' + assert self.get_ssl()['status'] == 200, 'reconfigure tls status' def test_tls_keepalive(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.certificate() @@ -466,7 +447,7 @@ basicConstraints = critical,CA:TRUE""" read_timeout=1, ) - self.assertEqual(resp['body'], '0123456789', 'keepalive 1') + assert resp['body'] == '0123456789', 'keepalive 1' resp = self.post_ssl( headers={ @@ -478,13 +459,13 @@ basicConstraints = critical,CA:TRUE""" body='0123456789', ) - self.assertEqual(resp['body'], '0123456789', 'keepalive 2') + assert resp['body'] == '0123456789', 'keepalive 2' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_keepalive_certificate_remove(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.certificate() @@ -506,19 +487,17 @@ basicConstraints = critical,CA:TRUE""" except: resp = None - self.assertEqual(resp, None, 'keepalive remove certificate') + assert resp == None, 'keepalive remove certificate' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_certificates_remove_all(self): self.load('empty') self.certificate() - self.assertIn( - 'success', - self.conf_delete('/certificates'), - 'remove all certificates', - ) + assert 'success' in self.conf_delete( + '/certificates' + ), 'remove all certificates' def test_tls_application_respawn(self): self.load('mirror') @@ -544,7 +523,7 @@ basicConstraints = critical,CA:TRUE""" subprocess.call(['kill', '-9', app_id]) - self.skip_alerts.append(r'process %s exited on signal 9' % app_id) + skip_alert(r'process %s exited on signal 9' % app_id) self.wait_for_record( re.compile( @@ -562,15 +541,13 @@ basicConstraints = critical,CA:TRUE""" body='0123456789', ) - self.assertEqual(resp['status'], 200, 'application respawn status') - self.assertEqual( - resp['body'], '0123456789', 'application respawn body' - ) + assert resp['status'] == 200, 'application respawn status' + assert resp['body'] == '0123456789', 'application respawn body' def test_tls_url_scheme(self): self.load('variables') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -578,16 +555,15 @@ basicConstraints = critical,CA:TRUE""" 'Custom-Header': '', 'Connection': 'close', } - )['headers']['Wsgi-Url-Scheme'], - 'http', - 'url scheme http', - ) + )['headers']['Wsgi-Url-Scheme'] + == 'http' + ), 'url scheme http' self.certificate() self.add_tls(application='variables') - self.assertEqual( + assert ( self.post_ssl( headers={ 'Host': 'localhost', @@ -595,10 +571,9 @@ basicConstraints = critical,CA:TRUE""" 'Custom-Header': '', 'Connection': 'close', } - )['headers']['Wsgi-Url-Scheme'], - 'https', - 'url scheme https', - ) + )['headers']['Wsgi-Url-Scheme'] + == 'https' + ), 'url scheme https' def test_tls_big_upload(self): self.load('upload') @@ -610,15 +585,14 @@ basicConstraints = critical,CA:TRUE""" filename = 'test.txt' data = '0123456789' * 9000 - res = self.post_ssl(body={ - 'file': { - 'filename': filename, - 'type': 'text/plain', - 'data': io.StringIO(data), + res = self.post_ssl( + body={ + 'file': { + 'filename': filename, + 'type': 'text/plain', + 'data': io.StringIO(data), + } } - }) - self.assertEqual(res['status'], 200, 'status ok') - self.assertEqual(res['body'], filename + data) - -if __name__ == '__main__': - TestTLS.main() + ) + assert res['status'] == 200, 'status ok' + assert res['body'] == filename + data diff --git a/test/test_upstreams_rr.py b/test/test_upstreams_rr.py index 2f74fbde..ceab11c3 100644 --- a/test/test_upstreams_rr.py +++ b/test/test_upstreams_rr.py @@ -2,49 +2,46 @@ import os import re from unit.applications.lang.python import TestApplicationPython +from conftest import option class TestUpstreamsRR(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "upstreams/one"}, - "*:7090": {"pass": "upstreams/two"}, - "*:7081": {"pass": "routes/one"}, - "*:7082": {"pass": "routes/two"}, - "*:7083": {"pass": "routes/three"}, - }, - "upstreams": { - "one": { - "servers": { - "127.0.0.1:7081": {}, - "127.0.0.1:7082": {}, - }, - }, - "two": { - "servers": { - "127.0.0.1:7081": {}, - "127.0.0.1:7082": {}, - }, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "upstreams/one"}, + "*:7090": {"pass": "upstreams/two"}, + "*:7081": {"pass": "routes/one"}, + "*:7082": {"pass": "routes/two"}, + "*:7083": {"pass": "routes/three"}, + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, }, }, - "routes": { - "one": [{"action": {"return": 200}}], - "two": [{"action": {"return": 201}}], - "three": [{"action": {"return": 202}}], + "two": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, }, - "applications": {}, }, - ), - 'upstreams initial configuration', - ) + "routes": { + "one": [{"action": {"return": 200}}], + "two": [{"action": {"return": 201}}], + "three": [{"action": {"return": 202}}], + }, + "applications": {}, + }, + ), 'upstreams initial configuration' self.cpu_count = os.cpu_count() @@ -91,113 +88,87 @@ Connection: close def test_upstreams_rr_no_weight(self): resps = self.get_resps() - self.assertEqual(sum(resps), 100, 'no weight sum') - self.assertLessEqual( - abs(resps[0] - resps[1]), self.cpu_count, 'no weight' - ) + assert sum(resps) == 100, 'no weight sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'no weight' - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7081'), - 'no weight server remove', - ) + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7081' + ), 'no weight server remove' resps = self.get_resps(req=50) - self.assertEqual(resps[1], 50, 'no weight 2') + assert resps[1] == 50, 'no weight 2' - self.assertIn( - 'success', - self.conf({}, 'upstreams/one/servers/127.0.0.1:7081'), - 'no weight server revert', - ) + assert 'success' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1:7081' + ), 'no weight server revert' resps = self.get_resps() - self.assertEqual(sum(resps), 100, 'no weight 3 sum') - self.assertLessEqual( - abs(resps[0] - resps[1]), self.cpu_count, 'no weight 3' - ) + assert sum(resps) == 100, 'no weight 3 sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'no weight 3' - self.assertIn( - 'success', - self.conf({}, 'upstreams/one/servers/127.0.0.1:7083'), - 'no weight server new', - ) + assert 'success' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1:7083' + ), 'no weight server new' resps = self.get_resps() - self.assertEqual(sum(resps), 100, 'no weight 4 sum') - self.assertLessEqual( - max(resps) - min(resps), self.cpu_count, 'no weight 4' - ) + assert sum(resps) == 100, 'no weight 4 sum' + assert max(resps) - min(resps) <= self.cpu_count, 'no weight 4' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 10, 'no weight 4 0') - self.assertEqual(resps[1], 10, 'no weight 4 1') - self.assertEqual(resps[2], 10, 'no weight 4 2') + assert resps[0] == 10, 'no weight 4 0' + assert resps[1] == 10, 'no weight 4 1' + assert resps[2] == 10, 'no weight 4 2' def test_upstreams_rr_weight(self): - self.assertIn( - 'success', - self.conf({"weight": 3}, 'upstreams/one/servers/127.0.0.1:7081'), - 'configure weight', - ) + assert 'success' in self.conf( + {"weight": 3}, 'upstreams/one/servers/127.0.0.1:7081' + ), 'configure weight' resps = self.get_resps_sc() - self.assertEqual(resps[0], 75, 'weight 3 0') - self.assertEqual(resps[1], 25, 'weight 3 1') + assert resps[0] == 75, 'weight 3 0' + assert resps[1] == 25, 'weight 3 1' - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7081/weight'), - 'configure weight remove', - ) + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'configure weight remove' resps = self.get_resps_sc(req=10) - self.assertEqual(resps[0], 5, 'weight 0 0') - self.assertEqual(resps[1], 5, 'weight 0 1') + assert resps[0] == 5, 'weight 0 0' + assert resps[1] == 5, 'weight 0 1' - self.assertIn( - 'success', - self.conf('1', 'upstreams/one/servers/127.0.0.1:7081/weight'), - 'configure weight 1', - ) + assert 'success' in self.conf( + '1', 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'configure weight 1' resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'weight 1 0') - self.assertEqual(resps[1], 50, 'weight 1 1') + assert resps[0] == 50, 'weight 1 0' + assert resps[1] == 50, 'weight 1 1' - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": 3}, - "127.0.0.1:7083": {"weight": 2}, - }, - 'upstreams/one/servers', - ), - 'configure weight 2', - ) + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": 3}, + "127.0.0.1:7083": {"weight": 2}, + }, + 'upstreams/one/servers', + ), 'configure weight 2' resps = self.get_resps_sc() - self.assertEqual(resps[0], 60, 'weight 2 0') - self.assertEqual(resps[2], 40, 'weight 2 1') + assert resps[0] == 60, 'weight 2 0' + assert resps[2] == 40, 'weight 2 1' def test_upstreams_rr_weight_rational(self): def set_weights(w1, w2): - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": w1}, - "127.0.0.1:7082": {"weight": w2}, - }, - 'upstreams/one/servers', - ), - 'configure weights', - ) + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": w1}, + "127.0.0.1:7082": {"weight": w2}, + }, + 'upstreams/one/servers', + ), 'configure weights' def check_reqs(w1, w2, reqs=10): resps = self.get_resps_sc(req=reqs) - self.assertEqual(resps[0], reqs * w1 / (w1 + w2), 'weight 1') - self.assertEqual(resps[1], reqs * w2 / (w1 + w2), 'weight 2') + assert resps[0] == reqs * w1 / (w1 + w2), 'weight 1' + assert resps[1] == reqs * w2 / (w1 + w2), 'weight 2' def check_weights(w1, w2): set_weights(w1, w2) @@ -207,39 +178,33 @@ Connection: close check_weights(0, 999999.0123456) check_weights(1, 9) check_weights(100000, 900000) - check_weights(1, .25) check_weights(1, 0.25) - check_weights(0.2, .8) + check_weights(1, 0.25) + check_weights(0.2, 0.8) check_weights(1, 1.5) - check_weights(1e-3, 1E-3) + check_weights(1e-3, 1e-3) check_weights(1e-20, 1e-20) check_weights(1e4, 1e4) check_weights(1000000, 1000000) set_weights(0.25, 0.25) - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7081/weight'), - 'delete weight', - ) + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'delete weight' check_reqs(1, 0.25) - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": 0.1}, - "127.0.0.1:7082": {"weight": 1}, - "127.0.0.1:7083": {"weight": 0.9}, - }, - 'upstreams/one/servers', - ), - 'configure weights', - ) + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": 0.1}, + "127.0.0.1:7082": {"weight": 1}, + "127.0.0.1:7083": {"weight": 0.9}, + }, + 'upstreams/one/servers', + ), 'configure weights' resps = self.get_resps_sc(req=20) - self.assertEqual(resps[0], 1, 'weight 3 1') - self.assertEqual(resps[1], 10, 'weight 3 2') - self.assertEqual(resps[2], 9, 'weight 3 3') + assert resps[0] == 1, 'weight 3 1' + assert resps[1] == 10, 'weight 3 2' + assert resps[2] == 9, 'weight 3 3' def test_upstreams_rr_independent(self): def sum_resps(*args): @@ -250,90 +215,77 @@ Connection: close return sum resps = self.get_resps_sc(req=30, port=7090) - self.assertEqual(resps[0], 15, 'dep two before 0') - self.assertEqual(resps[1], 15, 'dep two before 1') + assert resps[0] == 15, 'dep two before 0' + assert resps[1] == 15, 'dep two before 1' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 15, 'dep one before 0') - self.assertEqual(resps[1], 15, 'dep one before 1') + assert resps[0] == 15, 'dep one before 0' + assert resps[1] == 15, 'dep one before 1' - self.assertIn( - 'success', - self.conf('2', 'upstreams/two/servers/127.0.0.1:7081/weight'), - 'configure dep weight', - ) + assert 'success' in self.conf( + '2', 'upstreams/two/servers/127.0.0.1:7081/weight' + ), 'configure dep weight' resps = self.get_resps_sc(req=30, port=7090) - self.assertEqual(resps[0], 20, 'dep two 0') - self.assertEqual(resps[1], 10, 'dep two 1') + assert resps[0] == 20, 'dep two 0' + assert resps[1] == 10, 'dep two 1' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 15, 'dep one 0') - self.assertEqual(resps[1], 15, 'dep one 1') + assert resps[0] == 15, 'dep one 0' + assert resps[1] == 15, 'dep one 1' - self.assertIn( - 'success', - self.conf('1', 'upstreams/two/servers/127.0.0.1:7081/weight'), - 'configure dep weight 1', - ) + assert 'success' in self.conf( + '1', 'upstreams/two/servers/127.0.0.1:7081/weight' + ), 'configure dep weight 1' r_one, r_two = [0, 0], [0, 0] for _ in range(10): r_one = sum_resps(r_one, self.get_resps(req=10)) r_two = sum_resps(r_two, self.get_resps(req=10, port=7090)) - - self.assertEqual(sum(r_one), 100, 'dep one mix sum') - self.assertLessEqual( - abs(r_one[0] - r_one[1]), self.cpu_count, 'dep one mix' - ) - self.assertEqual(sum(r_two), 100, 'dep two mix sum') - self.assertLessEqual( - abs(r_two[0] - r_two[1]), self.cpu_count, 'dep two mix' - ) + assert sum(r_one) == 100, 'dep one mix sum' + assert abs(r_one[0] - r_one[1]) <= self.cpu_count, 'dep one mix' + assert sum(r_two) == 100, 'dep two mix sum' + assert abs(r_two[0] - r_two[1]) <= self.cpu_count, 'dep two mix' def test_upstreams_rr_delay(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "upstreams/one"}, - "*:7081": {"pass": "routes"}, - "*:7082": {"pass": "routes"}, - }, - "upstreams": { - "one": { - "servers": { - "127.0.0.1:7081": {}, - "127.0.0.1:7082": {}, - }, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "upstreams/one"}, + "*:7081": {"pass": "routes"}, + "*:7082": {"pass": "routes"}, + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, }, }, - "routes": [ - { - "match": {"destination": "*:7081"}, - "action": {"pass": "applications/delayed"}, - }, - { - "match": {"destination": "*:7082"}, - "action": {"return": 201}, - }, - ], - "applications": { - "delayed": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/delayed", - "working_directory": self.current_dir - + "/python/delayed", - "module": "wsgi", - } + }, + "routes": [ + { + "match": {"destination": "*:7081"}, + "action": {"pass": "applications/delayed"}, }, + { + "match": {"destination": "*:7082"}, + "action": {"return": 201}, + }, + ], + "applications": { + "delayed": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/delayed", + "working_directory": option.test_dir + + "/python/delayed", + "module": "wsgi", + } }, - ), - 'upstreams initial configuration', - ) + }, + ), 'upstreams initial configuration' req = 50 @@ -357,12 +309,12 @@ Connection: close resp = self.recvall(socks[i]).decode() socks[i].close() - m = re.search('HTTP/1.1 20(\d)', resp) - self.assertIsNotNone(m, 'status') + m = re.search(r'HTTP/1.1 20(\d)', resp) + assert m is not None, 'status' resps[int(m.group(1))] += 1 - self.assertEqual(sum(resps), req, 'delay sum') - self.assertLessEqual(abs(resps[0] - resps[1]), self.cpu_count, 'delay') + assert sum(resps) == req, 'delay sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'delay' def test_upstreams_rr_active_req(self): conns = 5 @@ -389,59 +341,46 @@ Connection: close # Send one more request and read response to make sure that previous # requests had enough time to reach server. - self.assertEqual(self.get()['body'], '') - - self.assertIn( - 'success', - self.conf( - {"127.0.0.1:7083": {"weight": 2}}, 'upstreams/one/servers', - ), - 'active req new server', - ) - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7083'), - 'active req server remove', - ) - self.assertIn( - 'success', self.conf_delete('listeners/*:7080'), 'delete listener' - ) - self.assertIn( - 'success', - self.conf_delete('upstreams/one'), - 'active req upstream remove', - ) + assert self.get()['body'] == '' + + assert 'success' in self.conf( + {"127.0.0.1:7083": {"weight": 2}}, 'upstreams/one/servers', + ), 'active req new server' + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7083' + ), 'active req server remove' + assert 'success' in self.conf_delete( + 'listeners/*:7080' + ), 'delete listener' + assert 'success' in self.conf_delete( + 'upstreams/one' + ), 'active req upstream remove' for i in range(conns): - self.assertEqual( - self.http(b'', sock=socks[i], raw=True)['body'], - '', - 'active req GET', - ) + assert ( + self.http(b'', sock=socks[i], raw=True)['body'] == '' + ), 'active req GET' - self.assertEqual( - self.http(b"""0123456789""", sock=socks2[i], raw=True)['body'], - '', - 'active req POST', - ) + assert ( + self.http(b"""0123456789""", sock=socks2[i], raw=True)['body'] + == '' + ), 'active req POST' def test_upstreams_rr_bad_server(self): - self.assertIn( - 'success', - self.conf({"weight": 1}, 'upstreams/one/servers/127.0.0.1:7084'), - 'configure bad server', - ) + assert 'success' in self.conf( + {"weight": 1}, 'upstreams/one/servers/127.0.0.1:7084' + ), 'configure bad server' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 10, 'bad server 0') - self.assertEqual(resps[1], 10, 'bad server 1') - self.assertEqual(sum(resps), 20, 'bad server sum') + assert resps[0] == 10, 'bad server 0' + assert resps[1] == 10, 'bad server 1' + assert sum(resps) == 20, 'bad server sum' def test_upstreams_rr_pipeline(self): resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'pipeline 0') - self.assertEqual(resps[1], 50, 'pipeline 1') + assert resps[0] == 50, 'pipeline 0' + assert resps[1] == 50, 'pipeline 1' def test_upstreams_rr_post(self): resps = [0, 0] @@ -449,120 +388,87 @@ Connection: close resps[self.get()['status'] % 10] += 1 resps[self.post(body='0123456789')['status'] % 10] += 1 - self.assertEqual(sum(resps), 100, 'post sum') - self.assertLessEqual(abs(resps[0] - resps[1]), self.cpu_count, 'post') + assert sum(resps) == 100, 'post sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'post' def test_upstreams_rr_unix(self): - addr_0 = self.testdir + '/sock_0' - addr_1 = self.testdir + '/sock_1' - - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "upstreams/one"}, - "unix:" + addr_0: {"pass": "routes/one"}, - "unix:" + addr_1: {"pass": "routes/two"}, - }, - 'listeners', - ), - 'configure listeners unix', - ) - - self.assertIn( - 'success', - self.conf( - {"unix:" + addr_0: {}, "unix:" + addr_1: {}}, - 'upstreams/one/servers', - ), - 'configure servers unix', - ) + addr_0 = self.temp_dir + '/sock_0' + addr_1 = self.temp_dir + '/sock_1' + + assert 'success' in self.conf( + { + "*:7080": {"pass": "upstreams/one"}, + "unix:" + addr_0: {"pass": "routes/one"}, + "unix:" + addr_1: {"pass": "routes/two"}, + }, + 'listeners', + ), 'configure listeners unix' + + assert 'success' in self.conf( + {"unix:" + addr_0: {}, "unix:" + addr_1: {}}, + 'upstreams/one/servers', + ), 'configure servers unix' resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'unix 0') - self.assertEqual(resps[1], 50, 'unix 1') + assert resps[0] == 50, 'unix 0' + assert resps[1] == 50, 'unix 1' def test_upstreams_rr_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "upstreams/one"}, - "[::1]:7081": {"pass": "routes/one"}, - "[::1]:7082": {"pass": "routes/two"}, - }, - 'listeners', - ), - 'configure listeners ipv6', - ) - - self.assertIn( - 'success', - self.conf( - {"[::1]:7081": {}, "[::1]:7082": {}}, 'upstreams/one/servers' - ), - 'configure servers ipv6', - ) + assert 'success' in self.conf( + { + "*:7080": {"pass": "upstreams/one"}, + "[::1]:7081": {"pass": "routes/one"}, + "[::1]:7082": {"pass": "routes/two"}, + }, + 'listeners', + ), 'configure listeners ipv6' + + assert 'success' in self.conf( + {"[::1]:7081": {}, "[::1]:7082": {}}, 'upstreams/one/servers' + ), 'configure servers ipv6' resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'ipv6 0') - self.assertEqual(resps[1], 50, 'ipv6 1') + assert resps[0] == 50, 'ipv6 0' + assert resps[1] == 50, 'ipv6 1' def test_upstreams_rr_servers_empty(self): - self.assertIn( - 'success', - self.conf({}, 'upstreams/one/servers'), - 'configure servers empty', - ) - self.assertEqual(self.get()['status'], 502, 'servers empty') - - self.assertIn( - 'success', - self.conf( - {"127.0.0.1:7081": {"weight": 0}}, 'upstreams/one/servers' - ), - 'configure servers empty one', - ) - self.assertEqual(self.get()['status'], 502, 'servers empty one') - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": 0}, - "127.0.0.1:7082": {"weight": 0}, - }, - 'upstreams/one/servers', - ), - 'configure servers empty two', - ) - self.assertEqual(self.get()['status'], 502, 'servers empty two') + assert 'success' in self.conf( + {}, 'upstreams/one/servers' + ), 'configure servers empty' + assert self.get()['status'] == 502, 'servers empty' + + assert 'success' in self.conf( + {"127.0.0.1:7081": {"weight": 0}}, 'upstreams/one/servers' + ), 'configure servers empty one' + assert self.get()['status'] == 502, 'servers empty one' + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": 0}, + "127.0.0.1:7082": {"weight": 0}, + }, + 'upstreams/one/servers', + ), 'configure servers empty two' + assert self.get()['status'] == 502, 'servers empty two' def test_upstreams_rr_invalid(self): - self.assertIn( - 'error', self.conf({}, 'upstreams'), 'upstreams empty', - ) - self.assertIn( - 'error', self.conf({}, 'upstreams/one'), 'named upstreams empty', - ) - self.assertIn( - 'error', - self.conf({}, 'upstreams/one/servers/127.0.0.1'), - 'invalid address', - ) - self.assertIn( - 'error', - self.conf({}, 'upstreams/one/servers/127.0.0.1:7081/blah'), - 'invalid server option', - ) + assert 'error' in self.conf({}, 'upstreams'), 'upstreams empty' + assert 'error' in self.conf( + {}, 'upstreams/one' + ), 'named upstreams empty' + assert 'error' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1' + ), 'invalid address' + assert 'error' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1:7081/blah' + ), 'invalid server option' def check_weight(w): - self.assertIn( - 'error', - self.conf(w, 'upstreams/one/servers/127.0.0.1:7081/weight'), - 'invalid weight option', - ) + assert 'error' in self.conf( + w, 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'invalid weight option' + check_weight({}) check_weight('-1') check_weight('1.') @@ -571,7 +477,3 @@ Connection: close check_weight('.01234567890123') check_weight('1000001') check_weight('2e6') - - -if __name__ == '__main__': - TestUpstreamsRR.main() diff --git a/test/test_usr1.py b/test/test_usr1.py index d1db652f..19081223 100644 --- a/test/test_usr1.py +++ b/test/test_usr1.py @@ -2,6 +2,7 @@ import os from subprocess import call from unit.applications.lang.python import TestApplicationPython +from conftest import waitforfiles class TestUSR1(TestApplicationPython): @@ -12,83 +13,74 @@ class TestUSR1(TestApplicationPython): log = 'access.log' log_new = 'new.log' - log_path = self.testdir + '/' + log + log_path = self.temp_dir + '/' + log - self.assertIn( - 'success', - self.conf('"' + log_path + '"', 'access_log'), - 'access log configure', - ) + assert 'success' in self.conf( + '"' + log_path + '"', 'access_log' + ), 'access log configure' - self.assertTrue(self.waitforfiles(log_path), 'open') + assert waitforfiles(log_path), 'open' - os.rename(log_path, self.testdir + '/' + log_new) + os.rename(log_path, self.temp_dir + '/' + log_new) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', log_new), - 'rename new', - ) - self.assertFalse(os.path.isfile(log_path), 'rename old') + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', log_new) + is not None + ), 'rename new' + assert not os.path.isfile(log_path), 'rename old' - with open(self.testdir + '/unit.pid', 'r') as f: + with open(self.temp_dir + '/unit.pid', 'r') as f: pid = f.read().rstrip() call(['kill', '-s', 'USR1', pid]) - self.assertTrue(self.waitforfiles(log_path), 'reopen') + assert waitforfiles(log_path), 'reopen' - self.assertEqual(self.get(url='/usr1')['status'], 200) + assert self.get(url='/usr1')['status'] == 200 self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log), - 'reopen 2', - ) - self.assertIsNone( - self.search_in_log(r'/usr1', log_new), 'rename new 2' - ) + assert ( + self.wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log) + is not None + ), 'reopen 2' + assert self.search_in_log(r'/usr1', log_new) is None, 'rename new 2' def test_usr1_unit_log(self): self.load('log_body') log_new = 'new.log' - log_path = self.testdir + '/unit.log' - log_path_new = self.testdir + '/' + log_new + log_path = self.temp_dir + '/unit.log' + log_path_new = self.temp_dir + '/' + log_new os.rename(log_path, log_path_new) body = 'body_for_a_log_new' - self.assertEqual(self.post(body=body)['status'], 200) + assert self.post(body=body)['status'] == 200 - self.assertIsNotNone( - self.wait_for_record(body, log_new), 'rename new' - ) - self.assertFalse(os.path.isfile(log_path), 'rename old') + assert self.wait_for_record(body, log_new) is not None, 'rename new' + assert not os.path.isfile(log_path), 'rename old' - with open(self.testdir + '/unit.pid', 'r') as f: + with open(self.temp_dir + '/unit.pid', 'r') as f: pid = f.read().rstrip() call(['kill', '-s', 'USR1', pid]) - self.assertTrue(self.waitforfiles(log_path), 'reopen') + assert waitforfiles(log_path), 'reopen' body = 'body_for_a_log_unit' - self.assertEqual(self.post(body=body)['status'], 200) + assert self.post(body=body)['status'] == 200 self.stop() - self.assertIsNotNone(self.wait_for_record(body), 'rename new') - self.assertIsNone(self.search_in_log(body, log_new), 'rename new 2') + assert self.wait_for_record(body) is not None, 'rename new' + assert self.search_in_log(body, log_new) is None, 'rename new 2' # merge two log files into unit.log to check alerts - with open(log_path, 'w') as unit_log, \ - open(log_path_new, 'r') as unit_log_new: + with open(log_path, 'w') as unit_log, open( + log_path_new, 'r' + ) as unit_log_new: unit_log.write(unit_log_new.read()) - - -if __name__ == '__main__': - TestUSR1.main() diff --git a/test/test_variables.py b/test/test_variables.py index fb481be5..0fa4296c 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -4,52 +4,48 @@ from unit.applications.proto import TestApplicationProto class TestVariables(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() - - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/$method"}}, - "routes": { - "GET": [{"action": {"return": 201}}], - "POST": [{"action": {"return": 202}}], - "3": [{"action": {"return": 203}}], - "4*": [{"action": {"return": 204}}], - "blahGET}": [{"action": {"return": 205}}], - "5GET": [{"action": {"return": 206}}], - "GETGET": [{"action": {"return": 207}}], - "localhost": [{"action": {"return": 208}}], - }, + def setup_method(self): + super().setup_method() + + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/$method"}}, + "routes": { + "GET": [{"action": {"return": 201}}], + "POST": [{"action": {"return": 202}}], + "3": [{"action": {"return": 203}}], + "4*": [{"action": {"return": 204}}], + "blahGET}": [{"action": {"return": 205}}], + "5GET": [{"action": {"return": 206}}], + "GETGET": [{"action": {"return": 207}}], + "localhost": [{"action": {"return": 208}}], }, - ), - 'configure routes', - ) + }, + ), 'configure routes' def conf_routes(self, routes): - self.assertIn('success', self.conf(routes, 'listeners/*:7080/pass')) + assert 'success' in self.conf(routes, 'listeners/*:7080/pass') def test_variables_method(self): - self.assertEqual(self.get()['status'], 201, 'method GET') - self.assertEqual(self.post()['status'], 202, 'method POST') + assert self.get()['status'] == 201, 'method GET' + assert self.post()['status'] == 202, 'method POST' def test_variables_uri(self): self.conf_routes("\"routes$uri\"") - self.assertEqual(self.get(url='/3')['status'], 203, 'uri') - self.assertEqual(self.get(url='/4*')['status'], 204, 'uri 2') - self.assertEqual(self.get(url='/4%2A')['status'], 204, 'uri 3') + assert self.get(url='/3')['status'] == 203, 'uri' + assert self.get(url='/4*')['status'] == 204, 'uri 2' + assert self.get(url='/4%2A')['status'] == 204, 'uri 3' def test_variables_host(self): self.conf_routes("\"routes/$host\"") def check_host(host, status=208): - self.assertEqual( + assert ( self.get(headers={'Host': host, 'Connection': 'close'})[ 'status' - ], - status, + ] + == status ) check_host('localhost') @@ -61,43 +57,41 @@ class TestVariables(TestApplicationProto): def test_variables_many(self): self.conf_routes("\"routes$uri$method\"") - self.assertEqual(self.get(url='/5')['status'], 206, 'many') + assert self.get(url='/5')['status'] == 206, 'many' self.conf_routes("\"routes${uri}${method}\"") - self.assertEqual(self.get(url='/5')['status'], 206, 'many 2') + assert self.get(url='/5')['status'] == 206, 'many 2' self.conf_routes("\"routes${uri}$method\"") - self.assertEqual(self.get(url='/5')['status'], 206, 'many 3') + assert self.get(url='/5')['status'] == 206, 'many 3' self.conf_routes("\"routes/$method$method\"") - self.assertEqual(self.get()['status'], 207, 'many 4') + assert self.get()['status'] == 207, 'many 4' self.conf_routes("\"routes/$method$uri\"") - self.assertEqual(self.get()['status'], 404, 'no route') - self.assertEqual(self.get(url='/blah')['status'], 404, 'no route 2') + assert self.get()['status'] == 404, 'no route' + assert self.get(url='/blah')['status'] == 404, 'no route 2' def test_variables_replace(self): - self.assertEqual(self.get()['status'], 201) + assert self.get()['status'] == 201 self.conf_routes("\"routes$uri\"") - self.assertEqual(self.get(url='/3')['status'], 203) + assert self.get(url='/3')['status'] == 203 self.conf_routes("\"routes/${method}\"") - self.assertEqual(self.post()['status'], 202) + assert self.post()['status'] == 202 self.conf_routes("\"routes${uri}\"") - self.assertEqual(self.get(url='/4*')['status'], 204) + assert self.get(url='/4*')['status'] == 204 self.conf_routes("\"routes/blah$method}\"") - self.assertEqual(self.get()['status'], 205) + assert self.get()['status'] == 205 def test_variables_invalid(self): def check_variables(routes): - self.assertIn( - 'error', - self.conf(routes, 'listeners/*:7080/pass'), - 'invalid variables', - ) + assert 'error' in self.conf( + routes, 'listeners/*:7080/pass' + ), 'invalid variables' check_variables("\"routes$\"") check_variables("\"routes${\"") @@ -106,6 +100,3 @@ class TestVariables(TestApplicationProto): check_variables("\"routes$uriblah\"") check_variables("\"routes${uri\"") check_variables("\"routes${{uri}\"") - -if __name__ == '__main__': - TestVariables.main() diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 83bde4d8..069bdecb 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -2,17 +2,18 @@ import os import subprocess from unit.applications.proto import TestApplicationProto +from conftest import option class TestApplicationGo(TestApplicationProto): @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) # check go module go_app = TestApplicationGo() - go_app.testdir = unit.testdir + go_app.temp_dir = unit.temp_dir proc = go_app.prepare_env('empty', 'app') if proc and proc.returncode == 0: cls.available['modules']['go'] = [] @@ -20,8 +21,8 @@ class TestApplicationGo(TestApplicationProto): return unit if not complete_check else unit.complete() def prepare_env(self, script, name, static=False): - if not os.path.exists(self.testdir + '/go'): - os.mkdir(self.testdir + '/go') + if not os.path.exists(self.temp_dir + '/go'): + os.mkdir(self.temp_dir + '/go') env = os.environ.copy() env['GOPATH'] = self.pardir + '/build/go' @@ -35,16 +36,16 @@ class TestApplicationGo(TestApplicationProto): '-ldflags', '-extldflags "-static"', '-o', - self.testdir + '/go/' + name, - self.current_dir + '/go/' + script + '/' + name + '.go', + self.temp_dir + '/go/' + name, + option.test_dir + '/go/' + script + '/' + name + '.go', ] else: args = [ 'go', 'build', '-o', - self.testdir + '/go/' + name, - self.current_dir + '/go/' + script + '/' + name + '.go', + self.temp_dir + '/go/' + name, + option.test_dir + '/go/' + script + '/' + name + '.go', ] try: @@ -59,8 +60,8 @@ class TestApplicationGo(TestApplicationProto): def load(self, script, name='app', **kwargs): static_build = False - wdir = self.current_dir + "/go/" + script - executable = self.testdir + "/go/" + name + wdir = option.test_dir + "/go/" + script + executable = self.temp_dir + "/go/" + name if 'isolation' in kwargs and 'rootfs' in kwargs['isolation']: wdir = "/go/" diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index c2c6dc51..b5511883 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -1,17 +1,19 @@ import glob import os +import pytest import shutil import subprocess from unit.applications.proto import TestApplicationProto +from conftest import option class TestApplicationJava(TestApplicationProto): def load(self, script, name='app', **kwargs): - app_path = self.testdir + '/java' + app_path = self.temp_dir + '/java' web_inf_path = app_path + '/WEB-INF/' classes_path = web_inf_path + 'classes/' - script_path = self.current_dir + '/java/' + script + '/' + script_path = option.test_dir + '/java/' + script + '/' if not os.path.isdir(app_path): os.makedirs(app_path) @@ -54,7 +56,7 @@ class TestApplicationJava(TestApplicationProto): ) if not ws_jars: - self.fail('websocket api jar not found.') + pytest.fail('websocket api jar not found.') javac = [ 'javac', @@ -69,7 +71,7 @@ class TestApplicationJava(TestApplicationProto): process.communicate() except: - self.fail('Cann\'t run javac process.') + pytest.fail('Cann\'t run javac process.') self._load_conf( { diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index cf2a99f6..f510acce 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -3,12 +3,13 @@ import shutil from urllib.parse import quote from unit.applications.proto import TestApplicationProto +from conftest import option, public_dir class TestApplicationNode(TestApplicationProto): @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) # check node module @@ -21,17 +22,17 @@ class TestApplicationNode(TestApplicationProto): # copy application shutil.copytree( - self.current_dir + '/node/' + script, self.testdir + '/node' + option.test_dir + '/node/' + script, self.temp_dir + '/node' ) # copy modules shutil.copytree( self.pardir + '/node/node_modules', - self.testdir + '/node/node_modules', + self.temp_dir + '/node/node_modules', ) - self.public_dir(self.testdir + '/node') + public_dir(self.temp_dir + '/node') self._load_conf( { @@ -42,7 +43,7 @@ class TestApplicationNode(TestApplicationProto): script: { "type": "external", "processes": {"spare": 0}, - "working_directory": self.testdir + '/node', + "working_directory": self.temp_dir + '/node', "executable": name, } }, diff --git a/test/unit/applications/lang/perl.py b/test/unit/applications/lang/perl.py index d32aca33..92939d88 100644 --- a/test/unit/applications/lang/perl.py +++ b/test/unit/applications/lang/perl.py @@ -1,11 +1,12 @@ from unit.applications.proto import TestApplicationProto +from conftest import option class TestApplicationPerl(TestApplicationProto): application_type = "perl" def load(self, script, name='psgi.pl', **kwargs): - script_path = self.current_dir + '/perl/' + script + script_path = option.test_dir + '/perl/' + script self._load_conf( { diff --git a/test/unit/applications/lang/php.py b/test/unit/applications/lang/php.py index e8c70c62..350eb29b 100644 --- a/test/unit/applications/lang/php.py +++ b/test/unit/applications/lang/php.py @@ -1,11 +1,12 @@ from unit.applications.proto import TestApplicationProto +from conftest import option class TestApplicationPHP(TestApplicationProto): application_type = "php" def load(self, script, index='index.php', **kwargs): - script_path = self.current_dir + '/php/' + script + script_path = option.test_dir + '/php/' + script self._load_conf( { diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index 91559f4b..dcdd2ffe 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -1,20 +1,23 @@ import os import shutil +import pytest from unit.applications.proto import TestApplicationProto +from conftest import option class TestApplicationPython(TestApplicationProto): application_type = "python" def load(self, script, name=None, **kwargs): + print() if name is None: name = script if script[0] == '/': script_path = script else: - script_path = self.current_dir + '/python/' + script + script_path = option.test_dir + '/python/' + script if kwargs.get('isolation') and kwargs['isolation'].get('rootfs'): rootfs = kwargs['isolation']['rootfs'] @@ -27,12 +30,17 @@ class TestApplicationPython(TestApplicationProto): script_path = '/app/python/' + name + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type + self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + name}}, "applications": { name: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "path": script_path, "working_directory": script_path, diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index 8c8acecc..534227bd 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -1,11 +1,12 @@ from unit.applications.proto import TestApplicationProto +from conftest import option class TestApplicationRuby(TestApplicationProto): application_type = "ruby" def load(self, script, name='config.ru', **kwargs): - script_path = self.current_dir + '/ruby/' + script + script_path = option.test_dir + '/ruby/' + script self._load_conf( { diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py index 244cb5be..7bf7e244 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -1,7 +1,9 @@ +import os import re import time from unit.control import TestControl +from conftest import option class TestApplicationProto(TestControl): @@ -12,7 +14,7 @@ class TestApplicationProto(TestControl): return time.mktime(time.strptime(date, template)) def search_in_log(self, pattern, name='unit.log'): - with open(self.testdir + '/' + name, 'r', errors='ignore') as f: + with open(self.temp_dir + '/' + name, 'r', errors='ignore') as f: return re.search(pattern, f.read()) def wait_for_record(self, pattern, name='unit.log'): @@ -26,6 +28,16 @@ class TestApplicationProto(TestControl): return found + def get_appication_type(self): + current_test = ( + os.environ.get('PYTEST_CURRENT_TEST').split(':')[-1].split(' ')[0] + ) + + if current_test in option.generated_tests: + return option.generated_tests[current_test] + + return None + def _load_conf(self, conf, **kwargs): if 'applications' in conf: for app in conf['applications'].keys(): @@ -39,6 +51,4 @@ class TestApplicationProto(TestControl): if 'isolation' in kwargs: app_conf['isolation'] = kwargs['isolation'] - self.assertIn( - 'success', self.conf(conf), 'load application configuration' - ) + assert 'success' in self.conf(conf), 'load application configuration' diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index e6a846b2..5453eef0 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -4,19 +4,20 @@ import ssl import subprocess from unit.applications.proto import TestApplicationProto +from conftest import option class TestApplicationTLS(TestApplicationProto): - def __init__(self, test): - super().__init__(test) + def setup_method(self): + super().setup_method() self.context = ssl.create_default_context() self.context.check_hostname = False self.context.verify_mode = ssl.CERT_NONE @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) # check tls module @@ -45,9 +46,9 @@ class TestApplicationTLS(TestApplicationProto): '-x509', '-new', '-subj', '/CN=' + name + '/', - '-config', self.testdir + '/openssl.conf', - '-out', self.testdir + '/' + name + '.crt', - '-keyout', self.testdir + '/' + name + '.key', + '-config', self.temp_dir + '/openssl.conf', + '-out', self.temp_dir + '/' + name + '.crt', + '-keyout', self.temp_dir + '/' + name + '.key', ], stderr=subprocess.STDOUT, ) @@ -59,8 +60,8 @@ class TestApplicationTLS(TestApplicationProto): if key is None: key = crt - key_path = self.testdir + '/' + key + '.key' - crt_path = self.testdir + '/' + crt + '.crt' + key_path = self.temp_dir + '/' + key + '.key' + crt_path = self.temp_dir + '/' + crt + '.crt' with open(key_path, 'rb') as k, open(crt_path, 'rb') as c: return self.conf(k.read() + c.read(), '/certificates/' + crt) @@ -87,7 +88,7 @@ class TestApplicationTLS(TestApplicationProto): return ssl.get_server_certificate(addr, ssl_version=ssl_version) def openssl_conf(self): - conf_path = self.testdir + '/openssl.conf' + conf_path = self.temp_dir + '/openssl.conf' if os.path.exists(conf_path): return @@ -105,7 +106,7 @@ distinguished_name = req_distinguished_name if name is None: name = script - script_path = self.current_dir + '/python/' + script + script_path = option.test_dir + '/python/' + script self._load_conf( { diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index e0dd2c0d..f1ddf630 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -1,6 +1,7 @@ import base64 import hashlib import itertools +import pytest import random import re import select @@ -21,9 +22,6 @@ class TestApplicationWebsocket(TestApplicationProto): OP_PONG = 0x0A CLOSE_CODES = [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011] - def __init__(self, preinit=False): - self.preinit = preinit - def key(self): raw_key = bytes(random.getrandbits(8) for _ in range(16)) return base64.b64encode(raw_key).decode() @@ -56,7 +54,7 @@ class TestApplicationWebsocket(TestApplicationProto): while True: rlist = select.select([sock], [], [], 60)[0] if not rlist: - self.fail('Can\'t read response from server.') + pytest.fail('Can\'t read response from server.') resp += sock.recv(4096).decode() @@ -84,7 +82,7 @@ class TestApplicationWebsocket(TestApplicationProto): # For all current cases if the "read_timeout" was changed # than test do not expect to get a response from server. if read_timeout == 60: - self.fail('Can\'t read response from server.') + pytest.fail('Can\'t read response from server.') break data += sock.recv(bytes - len(data)) @@ -130,19 +128,19 @@ class TestApplicationWebsocket(TestApplicationProto): code, = struct.unpack('!H', data[:2]) reason = data[2:].decode('utf-8') if not (code in self.CLOSE_CODES or 3000 <= code < 5000): - self.fail('Invalid status code') + pytest.fail('Invalid status code') frame['code'] = code frame['reason'] = reason elif length == 0: frame['code'] = 1005 frame['reason'] = '' else: - self.fail('Close frame too short') + pytest.fail('Close frame too short') frame['data'] = data if frame['mask']: - self.fail('Received frame with mask') + pytest.fail('Received frame with mask') return frame diff --git a/test/unit/control.py b/test/unit/control.py index 029072b5..6fd350f4 100644 --- a/test/unit/control.py +++ b/test/unit/control.py @@ -53,7 +53,7 @@ class TestControl(TestHTTP): args = { 'url': url, 'sock_type': 'unix', - 'addr': self.testdir + '/control.unit.sock', + 'addr': self.temp_dir + '/control.unit.sock', } if conf is not None: diff --git a/test/unit/feature/isolation.py b/test/unit/feature/isolation.py index 4f33d04a..c6f6f3c0 100644 --- a/test/unit/feature/isolation.py +++ b/test/unit/feature/isolation.py @@ -13,7 +13,7 @@ from unit.applications.proto import TestApplicationProto class TestFeatureIsolation(TestApplicationProto): allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net'] - def check(self, available, testdir): + def check(self, available, temp_dir): test_conf = {"namespaces": {"credential": True}} module = '' @@ -45,7 +45,7 @@ class TestFeatureIsolation(TestApplicationProto): if not module: return - module.testdir = testdir + module.temp_dir = temp_dir module.load(app) resp = module.conf(test_conf, 'applications/' + app + '/isolation') diff --git a/test/unit/http.py b/test/unit/http.py index de3bb2a4..54d32c06 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -2,12 +2,14 @@ import binascii import io import json import os +import pytest import re import select import socket import time from unit.main import TestUnit +from conftest import option class TestHTTP(TestUnit): @@ -56,7 +58,7 @@ class TestHTTP(TestUnit): sock.connect(connect_args) except ConnectionRefusedError: sock.close() - self.fail('Client can\'t connect to the server.') + pytest.fail('Client can\'t connect to the server.') else: sock = kwargs['sock'] @@ -128,7 +130,7 @@ class TestHTTP(TestUnit): return (resp, sock) def log_out(self, log, encoding): - if TestUnit.detailed: + if option.detailed: print('>>>') log = self.log_truncate(log) try: @@ -137,7 +139,7 @@ class TestHTTP(TestUnit): print(log) def log_in(self, log): - if TestUnit.detailed: + if option.detailed: print('<<<') log = self.log_truncate(log) try: @@ -190,7 +192,7 @@ class TestHTTP(TestUnit): # For all current cases if the "read_timeout" was changed # than test do not expect to get a response from server. if timeout == timeout_default: - self.fail('Can\'t read response from server.') + pytest.fail('Can\'t read response from server.') break try: @@ -243,28 +245,28 @@ class TestHTTP(TestUnit): chunks = raw_body.split(crlf) if len(chunks) < 3: - self.fail('Invalid chunked body') + pytest.fail('Invalid chunked body') if chunks.pop() != b'': - self.fail('No CRLF at the end of the body') + pytest.fail('No CRLF at the end of the body') try: last_size = int(chunks[-2], 16) except: - self.fail('Invalid zero size chunk') + pytest.fail('Invalid zero size chunk') if last_size != 0 or chunks[-1] != b'': - self.fail('Incomplete body') + pytest.fail('Incomplete body') body = b'' while len(chunks) >= 2: try: size = int(chunks.pop(0), 16) except: - self.fail('Invalid chunk size %s' % str(size)) + pytest.fail('Invalid chunk size %s' % str(size)) if size == 0: - self.assertEqual(len(chunks), 1, 'last zero size') + assert len(chunks) == 1, 'last zero size' break temp_body = crlf.join(chunks) @@ -280,8 +282,8 @@ class TestHTTP(TestUnit): def _parse_json(self, resp): headers = resp['headers'] - self.assertIn('Content-Type', headers) - self.assertEqual(headers['Content-Type'], 'application/json') + assert 'Content-Type' in headers + assert headers['Content-Type'] == 'application/json' resp['body'] = json.loads(resp['body']) @@ -305,7 +307,7 @@ class TestHTTP(TestUnit): sock.close() - self.assertTrue(ret, 'socket connected') + assert ret, 'socket connected' def form_encode(self, fields): is_multipart = False @@ -345,7 +347,7 @@ class TestHTTP(TestUnit): datatype = value['type'] if not isinstance(value['data'], io.IOBase): - self.fail('multipart encoding of file requires a stream.') + pytest.fail('multipart encoding of file requires a stream.') data = value['data'].read() @@ -353,7 +355,7 @@ class TestHTTP(TestUnit): data = value else: - self.fail('multipart requires a string or stream data') + pytest.fail('multipart requires a string or stream data') body += ( "--%s\r\nContent-Disposition: form-data; name=\"%s\"" diff --git a/test/unit/main.py b/test/unit/main.py index 83aa9139..18ea326e 100644 --- a/test/unit/main.py +++ b/test/unit/main.py @@ -1,8 +1,8 @@ import argparse import atexit -import fcntl import os import platform +import pytest import re import shutil import signal @@ -11,80 +11,19 @@ import subprocess import sys import tempfile import time -import unittest +from conftest import option, public_dir, waitforfiles, _check_alerts, _print_log from multiprocessing import Process -class TestUnit(unittest.TestCase): +class TestUnit(): - current_dir = os.path.abspath( - os.path.join(os.path.dirname(__file__), os.pardir) - ) pardir = os.path.abspath( os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) ) - is_su = os.geteuid() == 0 - uid = os.geteuid() - gid = os.getegid() - architecture = platform.architecture()[0] - system = platform.system() - maxDiff = None - - detailed = False - save_log = False - print_log = False - unsafe = False - - def __init__(self, methodName='runTest'): - super().__init__(methodName) - - if re.match(r'.*\/run\.py$', sys.argv[0]): - args, rest = TestUnit._parse_args() - - TestUnit._set_args(args) - - def run(self, result=None): - if not hasattr(self, 'application_type'): - return super().run(result) - - # rerun test for each available module version - - type = self.application_type - for module in self.prerequisites['modules']: - if module in self.available['modules']: - prereq_version = self.prerequisites['modules'][module] - available_versions = self.available['modules'][module] - - if prereq_version == 'all': - for version in available_versions: - self.application_type = type + ' ' + version - super().run(result) - elif prereq_version == 'any': - self.application_type = type + ' ' + available_versions[0] - super().run(result) - else: - for version in available_versions: - if version.startswith(prereq_version): - self.application_type = type + ' ' + version - super().run(result) - - @classmethod - def main(cls): - args, rest = TestUnit._parse_args() - - for i, arg in enumerate(rest): - if arg[:5] == 'test_': - rest[i] = cls.__name__ + '.' + arg - - sys.argv = sys.argv[:1] + rest - - TestUnit._set_args(args) - - unittest.main() @classmethod - def setUpClass(cls, complete_check=True): - cls.available = {'modules': {}, 'features': {}} + def setup_class(cls, complete_check=True): + cls.available = option.available unit = TestUnit() unit._run() @@ -92,7 +31,7 @@ class TestUnit(unittest.TestCase): # read unit.log for i in range(50): - with open(unit.testdir + '/unit.log', 'r') as f: + with open(unit.temp_dir + '/unit.log', 'r') as f: log = f.read() m = re.search('controller started', log) @@ -102,7 +41,7 @@ class TestUnit(unittest.TestCase): break if m is None: - unit._print_log() + _print_log() exit("Unit is writing log too long") # discover available modules from unit.log @@ -128,8 +67,7 @@ class TestUnit(unittest.TestCase): missed.append(module) if missed: - print('Unit has no ' + ', '.join(missed) + ' module(s)') - raise unittest.SkipTest() + pytest.skip('Unit has no ' + ', '.join(missed) + ' module(s)') # check features @@ -143,13 +81,12 @@ class TestUnit(unittest.TestCase): missed.append(feature) if missed: - print(', '.join(missed) + ' feature(s) not supported') - raise unittest.SkipTest() + pytest.skip(', '.join(missed) + ' feature(s) not supported') def destroy(): unit.stop() - unit._check_alerts(log) - shutil.rmtree(unit.testdir) + _check_alerts(log) + shutil.rmtree(unit.temp_dir) def complete(): destroy() @@ -161,7 +98,7 @@ class TestUnit(unittest.TestCase): unit.complete = complete return unit - def setUp(self): + def setup_method(self): self._run() def _run(self): @@ -171,82 +108,56 @@ class TestUnit(unittest.TestCase): if not os.path.isfile(self.unitd): exit("Could not find unit") - self.testdir = tempfile.mkdtemp(prefix='unit-test-') + self.temp_dir = tempfile.mkdtemp(prefix='unit-test-') - self.public_dir(self.testdir) + public_dir(self.temp_dir) if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777': - self.public_dir(build_dir) + public_dir(build_dir) - os.mkdir(self.testdir + '/state') + os.mkdir(self.temp_dir + '/state') - with open(self.testdir + '/unit.log', 'w') as log: + with open(self.temp_dir + '/unit.log', 'w') as log: self._p = subprocess.Popen( [ self.unitd, '--no-daemon', '--modules', self.pardir + '/build', - '--state', self.testdir + '/state', - '--pid', self.testdir + '/unit.pid', - '--log', self.testdir + '/unit.log', - '--control', 'unix:' + self.testdir + '/control.unit.sock', - '--tmp', self.testdir, + '--state', self.temp_dir + '/state', + '--pid', self.temp_dir + '/unit.pid', + '--log', self.temp_dir + '/unit.log', + '--control', 'unix:' + self.temp_dir + '/control.unit.sock', + '--tmp', self.temp_dir, ], stderr=log, ) atexit.register(self.stop) - if not self.waitforfiles(self.testdir + '/control.unit.sock'): - self._print_log() + if not waitforfiles(self.temp_dir + '/control.unit.sock'): + _print_log() exit("Could not start unit") self._started = True - self.skip_alerts = [ - r'read signalfd\(4\) failed', - r'sendmsg.+failed', - r'recvmsg.+failed', - ] - self.skip_sanitizer = False - - def tearDown(self): + def teardown_method(self): self.stop() - # detect errors and failures for current test - - def list2reason(exc_list): - if exc_list and exc_list[-1][0] is self: - return exc_list[-1][1] - - if hasattr(self, '_outcome'): - result = self.defaultTestResult() - self._feedErrorsToResult(result, self._outcome.errors) - else: - result = getattr( - self, '_outcomeForDoCleanups', self._resultForDoCleanups - ) - - success = not list2reason(result.errors) and not list2reason( - result.failures - ) - # check unit.log for alerts - unit_log = self.testdir + '/unit.log' + unit_log = self.temp_dir + '/unit.log' with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: - self._check_alerts(f.read()) + _check_alerts(f.read()) # remove unit.log - if not TestUnit.save_log and success: - shutil.rmtree(self.testdir) - + if not option.save_log: + shutil.rmtree(self.temp_dir) else: - self._print_log() + _print_log() - self.assertListEqual(self.stop_errors, [None, None], 'stop errors') + assert self.stop_errors == [None, None], 'stop errors' def stop(self): if not self._started: @@ -301,121 +212,3 @@ class TestUnit(unittest.TestCase): if fail: return 'Fail to stop process' - - def waitforfiles(self, *files): - for i in range(50): - wait = False - ret = False - - for f in files: - if not os.path.exists(f): - wait = True - break - - if wait: - time.sleep(0.1) - - else: - ret = True - break - - return ret - - def public_dir(self, path): - os.chmod(path, 0o777) - - for root, dirs, files in os.walk(path): - for d in dirs: - os.chmod(os.path.join(root, d), 0o777) - for f in files: - os.chmod(os.path.join(root, f), 0o777) - - def _check_alerts(self, log): - found = False - - alerts = re.findall('.+\[alert\].+', log) - - if alerts: - print('All alerts/sanitizer errors found in log:') - [print(alert) for alert in alerts] - found = True - - if self.skip_alerts: - for skip in self.skip_alerts: - alerts = [al for al in alerts if re.search(skip, al) is None] - - if alerts: - self._print_log(log) - self.assertFalse(alerts, 'alert(s)') - - if not self.skip_sanitizer: - sanitizer_errors = re.findall('.+Sanitizer.+', log) - - if sanitizer_errors: - self._print_log(log) - self.assertFalse(sanitizer_errors, 'sanitizer error(s)') - - if found: - print('skipped.') - - @staticmethod - def _parse_args(): - parser = argparse.ArgumentParser(add_help=False) - - parser.add_argument( - '-d', - '--detailed', - dest='detailed', - action='store_true', - help='Detailed output for tests', - ) - parser.add_argument( - '-l', - '--log', - dest='save_log', - action='store_true', - help='Save unit.log after the test execution', - ) - parser.add_argument( - '-r', - '--reprint_log', - dest='print_log', - action='store_true', - help='Print unit.log to stdout in case of errors', - ) - parser.add_argument( - '-u', - '--unsafe', - dest='unsafe', - action='store_true', - help='Run unsafe tests', - ) - - return parser.parse_known_args() - - @staticmethod - def _set_args(args): - TestUnit.detailed = args.detailed - TestUnit.save_log = args.save_log - TestUnit.print_log = args.print_log - TestUnit.unsafe = args.unsafe - - # set stdout to non-blocking - - if TestUnit.detailed or TestUnit.print_log: - fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) - - def _print_log(self, data=None): - path = self.testdir + '/unit.log' - - print('Path to unit.log:\n' + path + '\n') - - if TestUnit.print_log: - os.set_blocking(sys.stdout.fileno(), True) - sys.stdout.flush() - - if data is None: - with open(path, 'r', encoding='utf-8', errors='ignore') as f: - shutil.copyfileobj(f, sys.stdout) - else: - sys.stdout.write(data) -- cgit From 806135f1c93c09bb513efc1341d084951b080278 Mon Sep 17 00:00:00 2001 From: hongzhidao Date: Fri, 28 Aug 2020 00:53:36 -0400 Subject: Router: fixed "pass" to upstreams. Messed up return values in nxt_upstream_find() caused error in applying any configuration with a valid "pass" value in router configuration pointing to upstream. That wasn't the case in "listeners" objects, where the return value wasn't checked. Also, it caused segfault in cases where the "pass" option was configured with variables and resulting value was pointing to a non-existent upstream. Added missing return checks as well to catch possible memory allocation errors. The bug was introduced in d32bc428f46b. This closes #472 issue on GitHub. --- src/nxt_http_route.c | 6 +++- src/nxt_router.c | 4 +++ src/nxt_upstream.c | 4 +-- test/test_routing.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/nxt_http_route.c b/src/nxt_http_route.c index 0b2103cd..36e003ae 100644 --- a/src/nxt_http_route.c +++ b/src/nxt_http_route.c @@ -1594,6 +1594,7 @@ nxt_http_action_t * nxt_http_action_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_str_t *name) { + nxt_int_t ret; nxt_http_action_t *action; action = nxt_mp_alloc(tmcf->router_conf->mem_pool, @@ -1605,7 +1606,10 @@ nxt_http_action_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, action->name = *name; action->handler = NULL; - nxt_http_action_resolve(task, tmcf, action); + ret = nxt_http_action_resolve(task, tmcf, action); + if (nxt_slow_path(ret != NXT_OK)) { + return NULL; + } return action; } diff --git a/src/nxt_router.c b/src/nxt_router.c index ea14c6fb..b7408c3c 100644 --- a/src/nxt_router.c +++ b/src/nxt_router.c @@ -1725,6 +1725,10 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, tmcf->router_conf, &lscf.application); } + + if (nxt_slow_path(skcf->action == NULL)) { + goto fail; + } } } diff --git a/src/nxt_upstream.c b/src/nxt_upstream.c index c8ecbbe6..9f81b286 100644 --- a/src/nxt_upstream.c +++ b/src/nxt_upstream.c @@ -86,11 +86,11 @@ nxt_upstream_find(nxt_upstreams_t *upstreams, nxt_str_t *name, action->u.upstream_number = i; action->handler = nxt_upstream_handler; - return NXT_DECLINED; + return NXT_OK; } } - return NXT_OK; + return NXT_DECLINED; } diff --git a/test/test_routing.py b/test/test_routing.py index 4107f57e..734825ef 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -329,11 +329,90 @@ class TestRouting(TestApplicationProto): [{"match": {"method": "GET"}}], 'routes' ), 'route pass absent configure' + def test_routes_route_pass(self): + assert 'success' in self.conf( + { + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, + }, + "two": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, + }, + }, + } + ) + + assert 'success' in self.conf( + [{"action": {"pass": "routes"}}], 'routes' + ) + assert 'success' in self.conf( + [{"action": {"pass": "applications/app"}}], 'routes' + ) + assert 'success' in self.conf( + [{"action": {"pass": "upstreams/one"}}], 'routes' + ) + def test_routes_route_pass_absent(self): assert 'error' in self.conf( [{"match": {"method": "GET"}, "action": {}}], 'routes' ), 'route pass absent configure' + def test_routes_route_pass_invalid(self): + assert 'success' in self.conf( + { + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, + }, + "two": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, + }, + }, + } + ) + + assert 'error' in self.conf( + [{"action": {"pass": "blah"}}], 'routes' + ), 'route pass invalid' + assert 'error' in self.conf( + [{"action": {"pass": "routes/blah"}}], 'routes' + ), 'route pass routes invalid' + assert 'error' in self.conf( + [{"action": {"pass": "applications/blah"}}], 'routes' + ), 'route pass applications invalid' + assert 'error' in self.conf( + [{"action": {"pass": "upstreams/blah"}}], 'routes' + ), 'route pass upstreams invalid' + def test_routes_action_unique(self): assert 'success' in self.conf( { -- cgit From 09f4db1fc0ae5c1ad649316c1b97934af628736b Mon Sep 17 00:00:00 2001 From: Igor Sysoev Date: Fri, 18 Sep 2020 13:20:02 +0300 Subject: Fixed use-after-free error during reconfiguration. An idle connection was not removed from idle connection list if the connections detected that listening socket had been closed. --- src/nxt_h1proto.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c index b34be019..f340ea1e 100644 --- a/src/nxt_h1proto.c +++ b/src/nxt_h1proto.c @@ -1829,6 +1829,8 @@ nxt_h1p_idle_close(nxt_task_t *task, void *obj, void *data) nxt_debug(task, "h1p idle close"); + nxt_queue_remove(&c->link); + nxt_h1p_idle_response(task, c); } -- cgit From 6cfbf4ba791000705efeed4d29a212f6bd86821c Mon Sep 17 00:00:00 2001 From: Igor Sysoev Date: Fri, 18 Sep 2020 13:20:05 +0300 Subject: Fixed segmentation fault during reconfiguration. --- src/nxt_h1proto.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c index f340ea1e..94b74929 100644 --- a/src/nxt_h1proto.c +++ b/src/nxt_h1proto.c @@ -1749,7 +1749,15 @@ nxt_h1p_conn_timer_value(nxt_conn_t *c, uintptr_t data) joint = c->listen->socket.data; - return nxt_value_at(nxt_msec_t, joint->socket_conf, data); + if (nxt_fast_path(joint != NULL)) { + return nxt_value_at(nxt_msec_t, joint->socket_conf, data); + } + + /* + * Listening socket had been closed while + * connection was in keep-alive state. + */ + return 1; } -- cgit From 6b9882fc142cab4a15a272991096ef4db260bf0f Mon Sep 17 00:00:00 2001 From: Igor Sysoev Date: Fri, 18 Sep 2020 13:20:06 +0300 Subject: Fixed segmentation fault during reconfiguration. If idle connection was closed before h1proto had been allocated then c->socket.data is NULL. This happens if nxt_h1p_idle_response() is called by nxt_h1p_idle_close(). However, h1p->conn_write_tail is used only in nxt_h1p_request_send() that would not be called after nxt_h1p_idle_response(). The bug was introduced in f237e8c553fd. --- src/nxt_h1proto.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c index 94b74929..17046187 100644 --- a/src/nxt_h1proto.c +++ b/src/nxt_h1proto.c @@ -1873,10 +1873,9 @@ nxt_h1p_idle_timeout(nxt_task_t *task, void *obj, void *data) static void nxt_h1p_idle_response(nxt_task_t *task, nxt_conn_t *c) { - u_char *p; - size_t size; - nxt_buf_t *out, *last; - nxt_h1proto_t *h1p; + u_char *p; + size_t size; + nxt_buf_t *out, *last; size = nxt_length(NXT_H1P_IDLE_TIMEOUT) + nxt_http_date_cache.size @@ -1906,9 +1905,6 @@ nxt_h1p_idle_response(nxt_task_t *task, nxt_conn_t *c) last->completion_handler = nxt_h1p_idle_response_sent; last->parent = c; - h1p = c->socket.data; - h1p->conn_write_tail = &last->next; - c->write = out; c->write_state = &nxt_h1p_timeout_response_state; -- cgit From f16ae01b12e612701f44c028f188d1ded58d0358 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Fri, 18 Sep 2020 13:41:58 +0300 Subject: Python: app module callable name configuration. Now it is possible to specify the name of the application callable using optional parameter 'callable'. Default value is 'application'. This closes #290 issue on GitHub. --- src/nxt_application.h | 1 + src/nxt_conf_validation.c | 6 ++++++ src/nxt_main_process.c | 6 ++++++ src/python/nxt_python.c | 13 ++++++++----- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/nxt_application.h b/src/nxt_application.h index 3144dc3f..cb49a033 100644 --- a/src/nxt_application.h +++ b/src/nxt_application.h @@ -50,6 +50,7 @@ typedef struct { char *home; nxt_str_t path; nxt_str_t module; + char *callable; } nxt_python_app_conf_t; diff --git a/src/nxt_conf_validation.c b/src/nxt_conf_validation.c index 9dd4f715..4364057b 100644 --- a/src/nxt_conf_validation.c +++ b/src/nxt_conf_validation.c @@ -773,6 +773,12 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_python_members[] = { NULL, NULL }, + { nxt_string("callable"), + NXT_CONF_VLDT_STRING, + 0, + NULL, + NULL }, + NXT_CONF_VLDT_NEXT(&nxt_conf_vldt_common_members) }; diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c index 544a0cbd..d2edab1d 100644 --- a/src/nxt_main_process.c +++ b/src/nxt_main_process.c @@ -191,6 +191,12 @@ static nxt_conf_map_t nxt_python_app_conf[] = { NXT_CONF_MAP_STR, offsetof(nxt_common_app_conf_t, u.python.module), }, + + { + nxt_string("callable"), + NXT_CONF_MAP_CSTRZ, + offsetof(nxt_common_app_conf_t, u.python.callable), + }, }; diff --git a/src/python/nxt_python.c b/src/python/nxt_python.c index 5b6021bb..7d4589ed 100644 --- a/src/python/nxt_python.c +++ b/src/python/nxt_python.c @@ -60,6 +60,7 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) char *nxt_py_module; size_t len; PyObject *obj, *pypath, *module; + const char *callable; nxt_unit_ctx_t *unit_ctx; nxt_unit_init_t python_init; nxt_common_app_conf_t *app_conf; @@ -199,16 +200,18 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) goto fail; } - obj = PyDict_GetItemString(PyModule_GetDict(module), "application"); + callable = (c->callable != NULL) ? c->callable : "application"; + + obj = PyDict_GetItemString(PyModule_GetDict(module), callable); if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"application\" " - "from module \"%s\"", nxt_py_module); + nxt_alert(task, "Python failed to get \"%s\" " + "from module \"%s\"", callable, nxt_py_module); goto fail; } if (nxt_slow_path(PyCallable_Check(obj) == 0)) { - nxt_alert(task, "\"application\" in module \"%s\" " - "is not a callable object", nxt_py_module); + nxt_alert(task, "\"%s\" in module \"%s\" " + "is not a callable object", callable, nxt_py_module); goto fail; } -- cgit From 8ee96c224a42c47d641bc41424d932bb8c76773a Mon Sep 17 00:00:00 2001 From: Artem Konev Date: Fri, 18 Sep 2020 11:46:15 +0100 Subject: Updated racially charged language in messages and comments. --- src/nxt_isolation.c | 6 +++--- src/nxt_unit.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nxt_isolation.c b/src/nxt_isolation.c index 0ff63e50..ac7a37e8 100644 --- a/src/nxt_isolation.c +++ b/src/nxt_isolation.c @@ -740,7 +740,7 @@ nxt_isolation_pivot_root(nxt_task_t *task, const char *path) */ if (nxt_slow_path(mount("", "/", "", MS_SLAVE|MS_REC, "") != 0)) { - nxt_alert(task, "failed to make / a slave mount %E", nxt_errno); + nxt_alert(task, "mount(\"/\", MS_SLAVE|MS_REC) failed: %E", nxt_errno); return NXT_ERROR; } @@ -764,8 +764,8 @@ nxt_isolation_pivot_root(nxt_task_t *task, const char *path) } /* - * Make oldroot a slave mount to avoid unmounts getting propagated to the - * host. + * Demote the oldroot mount to avoid unmounts getting propagated to + * the host. */ if (nxt_slow_path(mount("", ".", "", MS_SLAVE | MS_REC, NULL) != 0)) { nxt_alert(task, "failed to bind mount rootfs %E", nxt_errno); diff --git a/src/nxt_unit.h b/src/nxt_unit.h index 67244cf4..eb3c30fa 100644 --- a/src/nxt_unit.h +++ b/src/nxt_unit.h @@ -187,7 +187,7 @@ struct nxt_unit_read_info_s { /* * Initialize Unit application library with necessary callbacks and - * ready/reply port parameters, send 'READY' response to master. + * ready/reply port parameters, send 'READY' response to main. */ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *); -- cgit From 7842c2d980512c10e23317da6261dad6bb9dffcc Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Fri, 18 Sep 2020 19:37:56 +0100 Subject: Added .hgignore file. --- .hgignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .hgignore diff --git a/.hgignore b/.hgignore new file mode 100644 index 00000000..d4f37fde --- /dev/null +++ b/.hgignore @@ -0,0 +1,6 @@ +^build/ +^Makefile$ +\.pyc$ +\.cache$ +\.pytest_cache$ +__pycache__/ -- cgit From af964488bad7fe00855749520d929ea1cb0de9d4 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 21 Sep 2020 10:32:14 +0300 Subject: Tests: changing Python application callable name. --- test/python/callable/wsgi.py | 7 +++++++ test/test_python_application.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 test/python/callable/wsgi.py diff --git a/test/python/callable/wsgi.py b/test/python/callable/wsgi.py new file mode 100644 index 00000000..365f82fa --- /dev/null +++ b/test/python/callable/wsgi.py @@ -0,0 +1,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/test_python_application.py b/test/test_python_application.py index d1079116..e6ad6a01 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -797,3 +797,31 @@ last line: 987654321 obj = self.getjson()['body'] assert obj['UID'] == nobody_uid, 'root uid group=root' assert obj['GID'] == 0, 'root gid group=root' + + def test_python_application_callable(self): + skip_alert(r'Python failed to get "blah" from module') + self.load('callable') + + assert self.get()['status'] == 204, 'default application response' + + assert 'success' in self.conf( + '"app"', 'applications/callable/callable' + ) + + assert self.get()['status'] == 200, 'callable response' + + assert 'success' in self.conf( + '"blah"', 'applications/callable/callable' + ) + + assert self.get()['status'] not in [200, 204], 'callable response inv' + + assert 'success' in self.conf( + '"app"', 'applications/callable/callable' + ) + + assert self.get()['status'] == 200, 'callable response 2' + + assert 'success' in self.conf_delete('applications/callable/callable') + + assert self.get()['status'] == 204, 'default response 2' -- cgit From 449652afa16160aaf2e36c81b64b0220e6ba96b1 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 21 Sep 2020 21:18:13 +0300 Subject: Tests: added multiple headers concatenation test. --- test/test_python_application.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/test_python_application.py b/test/test_python_application.py index e6ad6a01..434ceb19 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -20,14 +20,18 @@ class TestPythonApplication(TestApplicationPython): body = 'Test body string.' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Content-Type': 'text/html', - 'Custom-Header': 'blah', - 'Connection': 'close', - }, - body=body, + resp = self.http( + b"""POST / HTTP/1.1 +Host: localhost +Content-Length: %d +Custom-Header: blah +Custom-hEader: Blah +Content-Type: text/html +Connection: close +custom-header: BLAH + +%s""" % (len(body), body.encode()), + raw=True, ) assert resp['status'] == 200, 'status' @@ -52,7 +56,7 @@ class TestPythonApplication(TestApplicationPython): 'Request-Uri': '/', 'Http-Host': 'localhost', 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', + 'Custom-Header': 'blah, Blah, BLAH', 'Wsgi-Version': '(1, 0)', 'Wsgi-Url-Scheme': 'http', 'Wsgi-Multithread': 'False', -- cgit From 39008c1f05b30e9ac51cf7c46743c9d2dc34f505 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 21 Sep 2020 21:24:42 +0100 Subject: Tests: added test for "idle_timeout" with empty payload. --- test/test_settings.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/test_settings.py b/test/test_settings.py index 59b4a048..797af5ed 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -203,6 +203,24 @@ Connection: close assert resp['status'] == 408, 'status idle timeout' + def test_settings_idle_timeout_2(self): + self.load('empty') + + assert self.get()['status'] == 200, 'init' + + self.conf({'http': {'idle_timeout': 1}}, 'settings') + + _, sock = self.http(b'', start=True, raw=True, no_recv=True) + + time.sleep(2) + + assert ( + self.get( + headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock + )['status'] + == 408 + ), 'status idle timeout' + def test_settings_max_body_size(self): self.load('empty') -- cgit From 98c86c415caa5d40744f26f72c0d20a0bde058ee Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 21 Sep 2020 21:29:34 +0100 Subject: Tests: added variable test with nonexistent upstream. --- test/test_variables.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/test_variables.py b/test/test_variables.py index 0fa4296c..c458b636 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -87,6 +87,29 @@ class TestVariables(TestApplicationProto): self.conf_routes("\"routes/blah$method}\"") assert self.get()['status'] == 205 + def test_variables_upstream(self): + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "upstreams$uri"}, + "*:7081": {"pass": "routes/one"}, + }, + "upstreams": { + "1": { + "servers": { + "127.0.0.1:7081": {}, + }, + }, + }, + "routes": { + "one": [{"action": {"return": 200}}], + }, + }, + ), 'upstreams initial configuration' + + assert self.get(url='/1')['status'] == 200 + assert self.get(url='/2')['status'] == 404 + def test_variables_invalid(self): def check_variables(routes): assert 'error' in self.conf( -- cgit From 767c4cb50899d6086430ff4ed86d06352ffa974d Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 22 Sep 2020 12:40:18 +0300 Subject: Tests: using dict.get() method with default value. No functional changes. Only code readability improved. --- test/unit/http.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/test/unit/http.py b/test/unit/http.py index 54d32c06..e5231b3d 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -14,20 +14,15 @@ from conftest import option class TestHTTP(TestUnit): def http(self, start_str, **kwargs): - sock_type = ( - 'ipv4' if 'sock_type' not in kwargs else kwargs['sock_type'] - ) - port = 7080 if 'port' not in kwargs else kwargs['port'] - url = '/' if 'url' not in kwargs else kwargs['url'] + 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 = ( - {'Host': 'localhost', 'Connection': 'close'} - if 'headers' not in kwargs - else kwargs['headers'] - ) + headers = kwargs.get('headers', + {'Host': 'localhost', 'Connection': 'close'}) - body = b'' if 'body' not in kwargs else kwargs['body'] + body = kwargs.get('body', b'') crlf = '\r\n' if 'addr' not in kwargs: @@ -92,7 +87,7 @@ class TestHTTP(TestUnit): sock.sendall(req) - encoding = 'utf-8' if 'encoding' not in kwargs else kwargs['encoding'] + encoding = kwargs.get('encoding', 'utf-8') self.log_out(req, encoding) @@ -178,12 +173,8 @@ class TestHTTP(TestUnit): def recvall(self, sock, **kwargs): timeout_default = 60 - timeout = ( - timeout_default - if 'read_timeout' not in kwargs - else kwargs['read_timeout'] - ) - buff_size = 4096 if 'buff_size' not in kwargs else kwargs['buff_size'] + timeout = kwargs.get('read_timeout', timeout_default) + buff_size = kwargs.get('buff_size', 4096) data = b'' while True: -- cgit From dc49c561e204fd70008aa6af8c6171ef7d227db1 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 22 Sep 2020 12:40:35 +0300 Subject: Tests: improved response receiving while upgrade. The patch required to process non-101 response. --- test/unit/applications/websockets.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index f1ddf630..cb9f40aa 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -58,10 +58,7 @@ class TestApplicationWebsocket(TestApplicationProto): resp += sock.recv(4096).decode() - if ( - re.search('101 Switching Protocols', resp) - and resp[-4:] == '\r\n\r\n' - ): + if (resp.startswith('HTTP/') and '\r\n\r\n' in resp): resp = self._resp_to_dict(resp) break -- cgit From 1fc51cf140d22958d6dcf7e7729826b0f9671fd7 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 22 Sep 2020 19:53:19 +0300 Subject: Tests: introduced module name configuration. Also fixed problem with "/" in application name. --- test/unit/applications/lang/python.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index dcdd2ffe..75d34722 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -3,17 +3,22 @@ import shutil import pytest from unit.applications.proto import TestApplicationProto +from urllib.parse import quote from conftest import option class TestApplicationPython(TestApplicationProto): application_type = "python" + load_module = "wsgi" - def load(self, script, name=None, **kwargs): + def load(self, script, name=None, module=None, **kwargs): print() if name is None: name = script + if module is None: + module = self.load_module + if script[0] == '/': script_path = script else: @@ -37,14 +42,16 @@ class TestApplicationPython(TestApplicationProto): self._load_conf( { - "listeners": {"*:7080": {"pass": "applications/" + name}}, + "listeners": { + "*:7080": {"pass": "applications/" + quote(name, '')} + }, "applications": { name: { "type": appication_type, "processes": {"spare": 0}, "path": script_path, "working_directory": script_path, - "module": "wsgi", + "module": module, } }, }, -- cgit From efe65dee4de9d429a3cd0d7e88e4999a8c89081a Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 24 Sep 2020 09:47:27 +0300 Subject: Tests: prerequisites check improved by using callable. This is required for more flexible Python version check since ASGI works for Python 3.5+. Version check via 'startswith()' function removed as not consistent. --- test/conftest.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 8683a023..6bc871e2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -67,32 +67,40 @@ def pytest_generate_tests(metafunc): type = cls.application_type + def generate_tests(versions): + metafunc.fixturenames.append('tmp_ct') + metafunc.parametrize('tmp_ct', range(len(versions))) + + for i, version in enumerate(versions): + option.generated_tests[ + metafunc.function.__name__ + '[{}]'.format(i) + ] = (type + ' ' + version) + # take available module from option and generate tests for each version - for module in cls.prerequisites['modules']: + for module, prereq_version in cls.prerequisites['modules'].items(): if module in option.available['modules']: - prereq_version = cls.prerequisites['modules'][module] available_versions = option.available['modules'][module] if prereq_version == 'all': - metafunc.fixturenames.append('tmp_ct') - metafunc.parametrize('tmp_ct', range(len(available_versions))) - - for i in range(len(available_versions)): - version = available_versions[i] - option.generated_tests[ - metafunc.function.__name__ + '[{}]'.format(i) - ] = (type + ' ' + version) + generate_tests(available_versions) + elif prereq_version == 'any': option.generated_tests[metafunc.function.__name__] = ( type + ' ' + available_versions[0] ) + elif callable(prereq_version): + generate_tests( + list(filter(prereq_version, available_versions)) + ) + else: - for version in available_versions: - if version.startswith(prereq_version): - option.generated_tests[metafunc.function.__name__] = ( - type + ' ' + version - ) + raise ValueError( + """ +Unexpected prerequisite version "%s" for module "%s" in %s. +'all', 'any' or callable expected.""" + % (str(prereq_version), module, str(cls)) + ) def pytest_sessionstart(session): -- cgit From 409545494627690a644d1a9024b0d4bdb8f49acb Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Sun, 27 Sep 2020 23:27:19 +0100 Subject: Tests: added pytest.ini. --- test/pytest.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/pytest.ini diff --git a/test/pytest.ini b/test/pytest.ini new file mode 100644 index 00000000..c672788a --- /dev/null +++ b/test/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -rs -vvv +python_functions = test_* -- cgit From 4de6ffa63fb11bc37d47dcddc305bbf6f5768a84 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Sun, 27 Sep 2020 23:46:31 +0100 Subject: Tests: tuned delay in test_settings_idle_timeout_2. --- test/test_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_settings.py b/test/test_settings.py index 797af5ed..89f55703 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -212,7 +212,7 @@ Connection: close _, sock = self.http(b'', start=True, raw=True, no_recv=True) - time.sleep(2) + time.sleep(3) assert ( self.get( -- cgit From c4b000f9cc377b6a13777eb10e858c90de6264fe Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 29 Sep 2020 22:57:46 +0300 Subject: Supporting HTTP/1.0 keep-alive. The Apache HTTP server benchmarking tool, ab, issues HTTP/1.0 requests with the 'Connection: Keep-Alive' header and expects a 'Connection: Keep-Alive' header in the response. --- src/nxt_h1proto.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c index 17046187..7c695549 100644 --- a/src/nxt_h1proto.c +++ b/src/nxt_h1proto.c @@ -734,9 +734,16 @@ nxt_h1p_connection(void *ctx, nxt_http_field_t *field, uintptr_t data) r = ctx; field->hopbyhop = 1; - if (field->value_length == 5 && nxt_memcmp(field->value, "close", 5) == 0) { + if (field->value_length == 5 + && nxt_memcasecmp(field->value, "close", 5) == 0) + { r->proto.h1->keepalive = 0; + } else if (field->value_length == 10 + && nxt_memcasecmp(field->value, "keep-alive", 10) == 0) + { + r->proto.h1->keepalive = 1; + } else if (field->value_length == 7 && nxt_memcasecmp(field->value, "upgrade", 7) == 0) { -- cgit From c721a5378dd467054f672a40e677be56bad53c4d Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 29 Sep 2020 22:57:56 +0300 Subject: Fixing request buffer memory leakage in router. The issue was introduced in changeset 1d84b9e4b459. The request buffer was transferred via the shared application queue, but the buffer position and the 'sent' flag were not updated after the buffer had been sent. --- src/nxt_router.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/nxt_router.c b/src/nxt_router.c index b7408c3c..a3218047 100644 --- a/src/nxt_router.c +++ b/src/nxt_router.c @@ -4941,6 +4941,9 @@ nxt_router_app_prepare_request(nxt_task_t *task, nxt_debug(task, "queue is not empty"); } + buf->is_port_mmap_sent = 1; + buf->mem.pos = buf->mem.free; + } else { nxt_alert(task, "stream #%uD, app '%V': failed to send app message", req_rpc_data->stream, &app->name); -- cgit From 61eba6eef1558ed741d2bf5f1f3f5bc0481e587a Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 29 Sep 2020 22:58:04 +0300 Subject: Wrapping libunit's malloc() and free() calls for logging purposes. This change aids heap usage analysis in applications. The alloc and free functions are also required for lvlhash due to the upcoming threading support, because using main nxt_memalign() and nxt_free() isn't safe in a multithreaded app environment. The reason is that these functions may use thread-local structures which aren't initialized properly in applications. --- src/nxt_lvlhsh.c | 14 ------ src/nxt_malloc.c | 16 +++++++ src/nxt_unit.c | 133 ++++++++++++++++++++++++++++++++++--------------------- 3 files changed, 98 insertions(+), 65 deletions(-) diff --git a/src/nxt_lvlhsh.c b/src/nxt_lvlhsh.c index ec433341..d10dbc58 100644 --- a/src/nxt_lvlhsh.c +++ b/src/nxt_lvlhsh.c @@ -1015,17 +1015,3 @@ nxt_lvlhsh_retrieve(nxt_lvlhsh_t *lh, const nxt_lvlhsh_proto_t *proto, return NULL; } - - -void * -nxt_lvlhsh_alloc(void *data, size_t size) -{ - return nxt_memalign(size, size); -} - - -void -nxt_lvlhsh_free(void *data, void *p) -{ - nxt_free(p); -} diff --git a/src/nxt_malloc.c b/src/nxt_malloc.c index 910ef7cd..fed58e96 100644 --- a/src/nxt_malloc.c +++ b/src/nxt_malloc.c @@ -81,6 +81,22 @@ nxt_realloc(void *p, size_t size) } +/* nxt_lvlhsh_* functions moved here to avoid references from nxt_lvlhsh.c. */ + +void * +nxt_lvlhsh_alloc(void *data, size_t size) +{ + return nxt_memalign(size, size); +} + + +void +nxt_lvlhsh_free(void *data, void *p) +{ + nxt_free(p); +} + + #if (NXT_DEBUG) void diff --git a/src/nxt_unit.c b/src/nxt_unit.c index b7c01f95..a4015ce5 100644 --- a/src/nxt_unit.c +++ b/src/nxt_unit.c @@ -184,6 +184,10 @@ static nxt_unit_request_info_t *nxt_unit_request_hash_find( nxt_unit_ctx_t *ctx, uint32_t stream, int remove); static char * nxt_unit_snprint_prefix(char *p, char *end, pid_t pid, int level); +static void *nxt_unit_lvlhsh_alloc(void *data, size_t size); +static void nxt_unit_lvlhsh_free(void *data, void *p); +static void *nxt_unit_malloc(size_t size); +static void nxt_unit_free(void *p); static int nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length); @@ -532,7 +536,7 @@ nxt_unit_create(nxt_unit_init_t *init) nxt_unit_impl_t *lib; nxt_unit_callbacks_t *cb; - lib = malloc(sizeof(nxt_unit_impl_t) + init->request_data_size); + lib = nxt_unit_malloc(sizeof(nxt_unit_impl_t) + init->request_data_size); if (nxt_slow_path(lib == NULL)) { nxt_unit_alert(NULL, "failed to allocate unit struct"); @@ -587,7 +591,7 @@ nxt_unit_create(nxt_unit_init_t *init) fail: - free(lib); + nxt_unit_free(lib); return NULL; } @@ -711,7 +715,7 @@ nxt_unit_lib_release(nxt_unit_impl_t *lib) nxt_unit_mmaps_destroy(&lib->incoming); nxt_unit_mmaps_destroy(&lib->outgoing); - free(lib); + nxt_unit_free(lib); } } @@ -1389,7 +1393,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, return NXT_UNIT_AGAIN; } - port_impl = malloc(sizeof(nxt_unit_port_impl_t)); + port_impl = nxt_unit_malloc(sizeof(nxt_unit_port_impl_t)); if (nxt_slow_path(port_impl == NULL)) { nxt_unit_alert(ctx, "check_response_port: malloc(%d) failed", (int) sizeof(nxt_unit_port_impl_t)); @@ -1413,7 +1417,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, pthread_mutex_unlock(&lib->mutex); - free(port); + nxt_unit_free(port); return NXT_UNIT_ERROR; } @@ -1427,7 +1431,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, pthread_mutex_unlock(&lib->mutex); - free(port); + nxt_unit_free(port); return NXT_UNIT_ERROR; } @@ -1635,7 +1639,7 @@ nxt_unit_request_info_get(nxt_unit_ctx_t *ctx) if (nxt_queue_is_empty(&ctx_impl->free_req)) { pthread_mutex_unlock(&ctx_impl->mutex); - req_impl = malloc(sizeof(nxt_unit_request_info_impl_t) + req_impl = nxt_unit_malloc(sizeof(nxt_unit_request_info_impl_t) + lib->request_data_size); if (nxt_slow_path(req_impl == NULL)) { return NULL; @@ -1723,7 +1727,7 @@ nxt_unit_request_info_free(nxt_unit_request_info_impl_t *req_impl) nxt_queue_remove(&req_impl->link); if (req_impl != &ctx_impl->req) { - free(req_impl); + nxt_unit_free(req_impl); } } @@ -1742,7 +1746,7 @@ nxt_unit_websocket_frame_get(nxt_unit_ctx_t *ctx) if (nxt_queue_is_empty(&ctx_impl->free_ws)) { pthread_mutex_unlock(&ctx_impl->mutex); - ws_impl = malloc(sizeof(nxt_unit_websocket_frame_impl_t)); + ws_impl = nxt_unit_malloc(sizeof(nxt_unit_websocket_frame_impl_t)); if (nxt_slow_path(ws_impl == NULL)) { return NULL; } @@ -1788,7 +1792,7 @@ nxt_unit_websocket_frame_free(nxt_unit_websocket_frame_impl_t *ws_impl) { nxt_queue_remove(&ws_impl->link); - free(ws_impl); + nxt_unit_free(ws_impl); } @@ -2325,7 +2329,7 @@ nxt_unit_mmap_buf_get(nxt_unit_ctx_t *ctx) if (ctx_impl->free_buf == NULL) { pthread_mutex_unlock(&ctx_impl->mutex); - mmap_buf = malloc(sizeof(nxt_unit_mmap_buf_t)); + mmap_buf = nxt_unit_malloc(sizeof(nxt_unit_mmap_buf_t)); if (nxt_slow_path(mmap_buf == NULL)) { return NULL; } @@ -2643,7 +2647,7 @@ nxt_unit_free_outgoing_buf(nxt_unit_mmap_buf_t *mmap_buf) } if (mmap_buf->free_ptr != NULL) { - free(mmap_buf->free_ptr); + nxt_unit_free(mmap_buf->free_ptr); mmap_buf->free_ptr = NULL; } @@ -2685,7 +2689,7 @@ nxt_unit_read_buf_get_impl(nxt_unit_ctx_impl_t *ctx_impl) return rbuf; } - rbuf = malloc(sizeof(nxt_unit_read_buf_t)); + rbuf = nxt_unit_malloc(sizeof(nxt_unit_read_buf_t)); if (nxt_fast_path(rbuf != NULL)) { rbuf->ctx_impl = ctx_impl; @@ -3044,7 +3048,7 @@ nxt_unit_request_preread(nxt_unit_request_info_t *req, size_t size) return NULL; } - mmap_buf->free_ptr = malloc(size); + mmap_buf->free_ptr = nxt_unit_malloc(size); if (nxt_slow_path(mmap_buf->free_ptr == NULL)) { nxt_unit_req_alert(req, "preread: failed to allocate buf memory"); nxt_unit_mmap_buf_release(mmap_buf); @@ -3327,7 +3331,7 @@ nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) size = ws_impl->buf->buf.end - ws_impl->buf->buf.start; - b = malloc(size); + b = nxt_unit_malloc(size); if (nxt_slow_path(b == NULL)) { return NXT_UNIT_ERROR; } @@ -3835,7 +3839,7 @@ nxt_unit_get_outgoing_buf(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, mmap_buf->plain_ptr = local_buf; } else { - mmap_buf->free_ptr = malloc(size + sizeof(nxt_port_msg_t)); + mmap_buf->free_ptr = nxt_unit_malloc(size + sizeof(nxt_port_msg_t)); if (nxt_slow_path(mmap_buf->free_ptr == NULL)) { return NXT_UNIT_ERROR; } @@ -4005,7 +4009,7 @@ nxt_unit_process_release(nxt_unit_process_t *process) if (c == 1) { nxt_unit_debug(NULL, "destroy process #%d", (int) process->pid); - free(process); + nxt_unit_free(process); } } @@ -4022,7 +4026,7 @@ nxt_unit_mmaps_destroy(nxt_unit_mmaps_t *mmaps) munmap(mm->hdr, PORT_MMAP_SIZE); } - free(mmaps->elts); + nxt_unit_free(mmaps->elts); } pthread_mutex_destroy(&mmaps->mutex); @@ -4294,8 +4298,8 @@ nxt_unit_lvlhsh_pid_test(nxt_lvlhsh_query_t *lhq, void *data) static const nxt_lvlhsh_proto_t lvlhsh_processes_proto nxt_aligned(64) = { NXT_LVLHSH_DEFAULT, nxt_unit_lvlhsh_pid_test, - nxt_lvlhsh_alloc, - nxt_lvlhsh_free, + nxt_unit_lvlhsh_alloc, + nxt_unit_lvlhsh_free, }; @@ -4324,7 +4328,7 @@ nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) return process; } - process = malloc(sizeof(nxt_unit_process_t)); + process = nxt_unit_malloc(sizeof(nxt_unit_process_t)); if (nxt_slow_path(process == NULL)) { nxt_unit_alert(NULL, "failed to allocate process for #%d", (int) pid); @@ -4349,7 +4353,7 @@ nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) default: nxt_unit_alert(NULL, "process %d insert failed", (int) pid); - free(process); + nxt_unit_free(process); process = NULL; break; } @@ -4920,7 +4924,8 @@ nxt_unit_ctx_alloc(nxt_unit_ctx_t *ctx, void *data) lib = nxt_container_of(ctx->unit, nxt_unit_impl_t, unit); - new_ctx = malloc(sizeof(nxt_unit_ctx_impl_t) + lib->request_data_size); + new_ctx = nxt_unit_malloc(sizeof(nxt_unit_ctx_impl_t) + + lib->request_data_size); if (nxt_slow_path(new_ctx == NULL)) { nxt_unit_alert(ctx, "failed to allocate context"); @@ -4929,7 +4934,7 @@ nxt_unit_ctx_alloc(nxt_unit_ctx_t *ctx, void *data) rc = nxt_unit_ctx_init(lib, new_ctx, data); if (nxt_slow_path(rc != NXT_UNIT_OK)) { - free(new_ctx); + nxt_unit_free(new_ctx); return NULL; } @@ -5008,7 +5013,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) while (ctx_impl->free_buf != NULL) { mmap_buf = ctx_impl->free_buf; nxt_unit_mmap_buf_unlink(mmap_buf); - free(mmap_buf); + nxt_unit_free(mmap_buf); } nxt_queue_each(req_impl, &ctx_impl->free_req, @@ -5035,7 +5040,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) } if (ctx_impl != &lib->main_ctx) { - free(ctx_impl); + nxt_unit_free(ctx_impl); } nxt_unit_lib_release(lib); @@ -5220,7 +5225,7 @@ nxt_inline void nxt_unit_port_release(nxt_unit_port_t *port) : sizeof(nxt_port_queue_t)); } - free(port_impl); + nxt_unit_free(port_impl); } } @@ -5336,7 +5341,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) process->next_port_id = port->id.id + 1; } - new_port = malloc(sizeof(nxt_unit_port_impl_t)); + new_port = nxt_unit_malloc(sizeof(nxt_unit_port_impl_t)); if (nxt_slow_path(new_port == NULL)) { nxt_unit_alert(ctx, "add_port: %d,%d malloc() failed", port->id.pid, port->id.id); @@ -5351,7 +5356,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) nxt_unit_alert(ctx, "add_port: %d,%d hash_add failed", port->id.pid, port->id.id); - free(new_port); + nxt_unit_free(new_port); new_port = NULL; @@ -6016,8 +6021,8 @@ nxt_unit_port_hash_test(nxt_lvlhsh_query_t *lhq, void *data) static const nxt_lvlhsh_proto_t lvlhsh_ports_proto nxt_aligned(64) = { NXT_LVLHSH_DEFAULT, nxt_unit_port_hash_test, - nxt_lvlhsh_alloc, - nxt_lvlhsh_free, + nxt_unit_lvlhsh_alloc, + nxt_unit_lvlhsh_free, }; @@ -6115,8 +6120,8 @@ nxt_unit_request_hash_test(nxt_lvlhsh_query_t *lhq, void *data) static const nxt_lvlhsh_proto_t lvlhsh_requests_proto nxt_aligned(64) = { NXT_LVLHSH_DEFAULT, nxt_unit_request_hash_test, - nxt_lvlhsh_alloc, - nxt_lvlhsh_free, + nxt_unit_lvlhsh_alloc, + nxt_unit_lvlhsh_free, }; @@ -6346,24 +6351,61 @@ nxt_unit_snprint_prefix(char *p, char *end, pid_t pid, int level) } -/* The function required by nxt_lvlhsh_alloc() and nxt_lvlvhsh_free(). */ - -void * -nxt_memalign(size_t alignment, size_t size) +static void * +nxt_unit_lvlhsh_alloc(void *data, size_t size) { - void *p; - nxt_err_t err; + int err; + void *p; - err = posix_memalign(&p, alignment, size); + err = posix_memalign(&p, size, size); if (nxt_fast_path(err == 0)) { + nxt_unit_debug(NULL, "posix_memalign(%d, %d): %p", + (int) size, (int) size, p); return p; } + nxt_unit_alert(NULL, "posix_memalign(%d, %d) failed: %s (%d)", + (int) size, (int) size, strerror(err), err); return NULL; } +static void +nxt_unit_lvlhsh_free(void *data, void *p) +{ + nxt_unit_free(p); +} + + +static void * +nxt_unit_malloc(size_t size) +{ + void *p; + + p = malloc(size); + + if (nxt_fast_path(p != NULL)) { + nxt_unit_debug(NULL, "malloc(%d): %p", (int) size, p); + + } else { + nxt_unit_alert(NULL, "malloc(%d) failed: %s (%d)", + (int) size, strerror(errno), errno); + } + + return p; +} + + +static void +nxt_unit_free(void *p) +{ + nxt_unit_debug(NULL, "free(%p)", p); + + free(p); +} + + static int nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length) { @@ -6390,14 +6432,3 @@ nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length) return 0; } - - -#if (NXT_DEBUG) - -void -nxt_free(void *p) -{ - free(p); -} - -#endif -- cgit From 67d33fac66d37327ce038e1538d07f353afe87e8 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Tue, 29 Sep 2020 23:23:32 +0300 Subject: MIME: added AVIF and APNG image formats. AVIF is a modern image format based on the AV1 video codec. It generally has better compression than other widely used formats (WebP, JPEG, PNG, and GIF) and is designed to supersede them. Support was already added to the latest version of Chrome. APNG extends PNG to permit animated images that work similarly to animated GIF. It's supported by most modern browsers. Also removed duplicated ".svg" entry. --- src/nxt_http_static.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/nxt_http_static.c b/src/nxt_http_static.c index ee18be1b..5687ef2c 100644 --- a/src/nxt_http_static.c +++ b/src/nxt_http_static.c @@ -469,15 +469,18 @@ nxt_http_static_mtypes_init(nxt_mp_t *mp, nxt_lvlhsh_t *hash) { nxt_string("text/html"), ".htm" }, { nxt_string("text/css"), ".css" }, - { nxt_string("image/svg+xml"), ".svg" }, { nxt_string("image/svg+xml"), ".svg" }, { nxt_string("image/webp"), ".webp" }, { nxt_string("image/png"), ".png" }, + { nxt_string("image/apng"), ".apng" }, { nxt_string("image/jpeg"), ".jpeg" }, { nxt_string("image/jpeg"), ".jpg" }, { nxt_string("image/gif"), ".gif" }, { nxt_string("image/x-icon"), ".ico" }, + { nxt_string("image/avif"), ".avif" }, + { nxt_string("image/avif-sequence"), ".avifs" }, + { nxt_string("font/woff"), ".woff" }, { nxt_string("font/woff2"), ".woff2" }, { nxt_string("font/otf"), ".otf" }, -- cgit From 153e8a87792ecc5dee12ba1f261fe1340a800a90 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Wed, 30 Sep 2020 01:17:09 +0300 Subject: Fixing leakage caused by incorrect in_hash flag cleanup. Large-bodied requests are added to the request hash to be found when the body arrives. However, changeset 1d84b9e4b459 introduced a bug: the 'in_hash' flag, used to remove the request from the hash at request release, was cleared after the first successful request lookup. As a result, the entry was never removed. --- src/nxt_unit.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nxt_unit.c b/src/nxt_unit.c index a4015ce5..28295e22 100644 --- a/src/nxt_unit.c +++ b/src/nxt_unit.c @@ -6202,7 +6202,9 @@ nxt_unit_request_hash_find(nxt_unit_ctx_t *ctx, uint32_t stream, int remove) case NXT_OK: req_impl = nxt_container_of(lhq.value, nxt_unit_request_info_impl_t, req); - req_impl->in_hash = 0; + if (remove) { + req_impl->in_hash = 0; + } return lhq.value; -- cgit From c5cb2432c473a00a8af69b8930b268552afce85b Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Wed, 30 Sep 2020 16:36:57 +0300 Subject: Fixing router connection pool leakage. The connection's local socket address is allocated from the connection pool before the request is passed to the application; however, with keep-alive connections, this field was unconditionally reset by a socket configuration value that could be NULL. For the next request, the address was allocated again from the same connection pool. Nonetheless, all leaked addresses were released when the connection was closed. The issue introduced in changeset 5c7dd85fabd5. --- src/nxt_h1proto.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c index 7c695549..dc23d7c4 100644 --- a/src/nxt_h1proto.c +++ b/src/nxt_h1proto.c @@ -503,7 +503,10 @@ nxt_h1p_conn_request_init(nxt_task_t *task, void *obj, void *data) joint->count++; r->conf = joint; - c->local = joint->socket_conf->sockaddr; + + if (c->local == NULL) { + c->local = joint->socket_conf->sockaddr; + } nxt_h1p_conn_request_header_parse(task, c, h1p); return; -- cgit From 1fe1518ab1b9ad5dac6f2f8a1e41571cbc4d96e9 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 30 Sep 2020 22:45:58 +0100 Subject: Tests: fixed test_static_space_in_name. --- test/test_static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_static.py b/test/test_static.py index fc8bb0a9..2290bc38 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -129,7 +129,7 @@ class TestStatic(TestApplicationProto): ), 'file name enclosing' try: - print('файл') + open(self.temp_dir + '/ф а', 'a').close() utf8 = True except: -- cgit From d491527555c076695a4202577198e12bf0b919ec Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Thu, 1 Oct 2020 10:17:00 +0100 Subject: Tests: minor fixes. Fixed temporary dir removing. Fixed printing path to log. Module checks moved to the separate file. --- test/conftest.py | 33 +++++++++++++++++++++++++++------ test/test_java_isolation_rootfs.py | 3 ++- test/unit/applications/lang/go.py | 16 +--------------- test/unit/applications/lang/java.py | 8 +++++--- test/unit/applications/lang/node.py | 13 +------------ test/unit/applications/tls.py | 21 --------------------- test/unit/check/go.py | 29 +++++++++++++++++++++++++++++ test/unit/check/node.py | 6 ++++++ test/unit/check/tls.py | 13 +++++++++++++ test/unit/main.py | 23 +++++------------------ 10 files changed, 89 insertions(+), 76 deletions(-) create mode 100644 test/unit/check/go.py create mode 100644 test/unit/check/node.py create mode 100644 test/unit/check/tls.py diff --git a/test/conftest.py b/test/conftest.py index 6bc871e2..8e9009b6 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,6 +2,7 @@ import fcntl import os import platform import pytest +import shutil import signal import stat import subprocess @@ -10,6 +11,10 @@ import re import tempfile import time +from unit.check.go import check_go +from unit.check.node import check_node +from unit.check.tls import check_openssl + def pytest_addoption(parser): parser.addoption( @@ -132,6 +137,20 @@ def pytest_sessionstart(session): else: option.available['modules'][module[0]].append(module[1]) + # discover modules from check + + option.available['modules']['openssl'] = check_openssl(unit['unitd']) + option.available['modules']['go'] = check_go( + option.current_dir, unit['temp_dir'], option.test_dir + ) + option.available['modules']['node'] = check_node(option.current_dir) + + # remove None values + + option.available['modules'] = { + k: v for k, v in option.available['modules'].items() if v is not None + } + unit_stop() @@ -216,6 +235,7 @@ def unit_stop(): p.kill() return 'Could not terminate unit' + shutil.rmtree(unit_instance['temp_dir']) def public_dir(path): os.chmod(path, 0o777) @@ -265,31 +285,32 @@ def _check_alerts(log): alerts = [al for al in alerts if re.search(skip, al) is None] if alerts: - _print_log(log) + _print_log(data=log) assert not alerts, 'alert(s)' if not option.skip_sanitizer: sanitizer_errors = re.findall('.+Sanitizer.+', log) if sanitizer_errors: - _print_log(log) + _print_log(data=log) assert not sanitizer_errors, 'sanitizer error(s)' if found: print('skipped.') -def _print_log(data=None): - unit_log = unit_instance['log'] +def _print_log(path=None, data=None): + if path is None: + path = unit_instance['log'] - print('Path to unit.log:\n' + unit_log + '\n') + print('Path to unit.log:\n' + path + '\n') if option.print_log: os.set_blocking(sys.stdout.fileno(), True) sys.stdout.flush() if data is None: - with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: + with open(path, 'r', encoding='utf-8', errors='ignore') as f: shutil.copyfileobj(f, sys.stdout) else: sys.stdout.write(data) diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py index fa227469..df3ccdfa 100644 --- a/test/test_java_isolation_rootfs.py +++ b/test/test_java_isolation_rootfs.py @@ -3,6 +3,7 @@ import subprocess import pytest from unit.applications.lang.java import TestApplicationJava +from conftest import option class TestJavaIsolationRootfs(TestApplicationJava): @@ -23,7 +24,7 @@ class TestJavaIsolationRootfs(TestApplicationJava): [ "mount", "--bind", - self.pardir + "/build", + option.current_dir + "/build", self.temp_dir + "/jars", ], stderr=subprocess.STDOUT, diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 069bdecb..946b5421 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -6,26 +6,12 @@ from conftest import option class TestApplicationGo(TestApplicationProto): - @classmethod - def setup_class(cls, complete_check=True): - unit = super().setup_class(complete_check=False) - - # check go module - - go_app = TestApplicationGo() - go_app.temp_dir = unit.temp_dir - proc = go_app.prepare_env('empty', 'app') - if proc and proc.returncode == 0: - cls.available['modules']['go'] = [] - - return unit if not complete_check else unit.complete() - def prepare_env(self, script, name, static=False): if not os.path.exists(self.temp_dir + '/go'): os.mkdir(self.temp_dir + '/go') env = os.environ.copy() - env['GOPATH'] = self.pardir + '/build/go' + env['GOPATH'] = option.current_dir + '/build/go' if static: args = [ diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index b5511883..93427709 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -49,10 +49,12 @@ class TestApplicationJava(TestApplicationProto): if not os.path.isdir(classes_path): os.makedirs(classes_path) - classpath = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar' + classpath = ( + option.current_dir + '/build/tomcat-servlet-api-9.0.13.jar' + ) ws_jars = glob.glob( - self.pardir + '/build/websocket-api-java-*.jar' + option.current_dir + '/build/websocket-api-java-*.jar' ) if not ws_jars: @@ -78,7 +80,7 @@ class TestApplicationJava(TestApplicationProto): "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "unit_jars": self.pardir + '/build', + "unit_jars": option.current_dir + '/build', "type": 'java', "processes": {"spare": 0}, "working_directory": script_path, diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index f510acce..dbb7036b 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -7,17 +7,6 @@ from conftest import option, public_dir class TestApplicationNode(TestApplicationProto): - @classmethod - def setup_class(cls, complete_check=True): - unit = super().setup_class(complete_check=False) - - # check node module - - if os.path.exists(unit.pardir + '/node/node_modules'): - cls.available['modules']['node'] = [] - - return unit if not complete_check else unit.complete() - def load(self, script, name='app.js', **kwargs): # copy application @@ -28,7 +17,7 @@ class TestApplicationNode(TestApplicationProto): # copy modules shutil.copytree( - self.pardir + '/node/node_modules', + option.current_dir + '/node/node_modules', self.temp_dir + '/node/node_modules', ) diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index 5453eef0..7c95f27b 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -15,27 +15,6 @@ class TestApplicationTLS(TestApplicationProto): self.context.check_hostname = False self.context.verify_mode = ssl.CERT_NONE - @classmethod - def setup_class(cls, complete_check=True): - unit = super().setup_class(complete_check=False) - - # check tls module - - try: - subprocess.check_output(['which', 'openssl']) - - output = subprocess.check_output( - [unit.unitd, '--version'], stderr=subprocess.STDOUT - ) - - if re.search('--openssl', output.decode()): - cls.available['modules']['openssl'] = [] - - except: - pass - - return unit if not complete_check else unit.complete() - def certificate(self, name='default', load=True): self.openssl_conf() diff --git a/test/unit/check/go.py b/test/unit/check/go.py new file mode 100644 index 00000000..dd2150eb --- /dev/null +++ b/test/unit/check/go.py @@ -0,0 +1,29 @@ +import os +import subprocess + + +def check_go(current_dir, temp_dir, test_dir): + if not os.path.exists(temp_dir + '/go'): + os.mkdir(temp_dir + '/go') + + env = os.environ.copy() + env['GOPATH'] = current_dir + '/build/go' + + try: + process = subprocess.Popen( + [ + 'go', + 'build', + '-o', + temp_dir + '/go/app', + test_dir + '/go/empty/app.go', + ], + env=env, + ) + process.communicate() + + if process.returncode == 0: + return True + + except: + return None diff --git a/test/unit/check/node.py b/test/unit/check/node.py new file mode 100644 index 00000000..236ba7b5 --- /dev/null +++ b/test/unit/check/node.py @@ -0,0 +1,6 @@ +import os + + +def check_node(current_dir): + if os.path.exists(current_dir + '/node/node_modules'): + return True diff --git a/test/unit/check/tls.py b/test/unit/check/tls.py new file mode 100644 index 00000000..b878ff7d --- /dev/null +++ b/test/unit/check/tls.py @@ -0,0 +1,13 @@ +import re +import subprocess + + +def check_openssl(unitd): + subprocess.check_output(['which', 'openssl']) + + output = subprocess.check_output( + [unitd, '--version'], stderr=subprocess.STDOUT + ) + + if re.search('--openssl', output.decode()): + return True diff --git a/test/unit/main.py b/test/unit/main.py index 18ea326e..053ce145 100644 --- a/test/unit/main.py +++ b/test/unit/main.py @@ -16,11 +16,6 @@ from multiprocessing import Process class TestUnit(): - - pardir = os.path.abspath( - os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) - ) - @classmethod def setup_class(cls, complete_check=True): cls.available = option.available @@ -41,17 +36,9 @@ class TestUnit(): break if m is None: - _print_log() + _print_log(path=unit.temp_dir + '/unit.log') exit("Unit is writing log too long") - # discover available modules from unit.log - - for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): - if module[0] not in cls.available['modules']: - cls.available['modules'][module[0]] = [module[1]] - else: - cls.available['modules'][module[0]].append(module[1]) - def check(available, prerequisites): missed = [] @@ -102,7 +89,7 @@ class TestUnit(): self._run() def _run(self): - build_dir = self.pardir + '/build' + build_dir = option.current_dir + '/build' self.unitd = build_dir + '/unitd' if not os.path.isfile(self.unitd): @@ -122,7 +109,7 @@ class TestUnit(): [ self.unitd, '--no-daemon', - '--modules', self.pardir + '/build', + '--modules', build_dir, '--state', self.temp_dir + '/state', '--pid', self.temp_dir + '/unit.pid', '--log', self.temp_dir + '/unit.log', @@ -135,7 +122,7 @@ class TestUnit(): atexit.register(self.stop) if not waitforfiles(self.temp_dir + '/control.unit.sock'): - _print_log() + _print_log(path=self.temp_dir + '/unit.log') exit("Could not start unit") self._started = True @@ -155,7 +142,7 @@ class TestUnit(): if not option.save_log: shutil.rmtree(self.temp_dir) else: - _print_log() + _print_log(path=self.temp_dir) assert self.stop_errors == [None, None], 'stop errors' -- cgit From c2eb245b32870b6360079ff9a4b063a7cd84d585 Mon Sep 17 00:00:00 2001 From: Tiago Natel de Moura Date: Wed, 9 Sep 2020 19:28:44 +0100 Subject: PHP: fixed "rootfs" isolation dependency on system mounts. --- auto/modules/php | 30 +------- src/nxt_php_sapi.c | 162 ++++++++++++++++++++++++++++-------------- test/test_php_isolation.py | 51 ------------- test/test_python_isolation.py | 30 ++++++++ 4 files changed, 138 insertions(+), 135 deletions(-) diff --git a/auto/modules/php b/auto/modules/php index 848fc1bc..41eeb1c3 100644 --- a/auto/modules/php +++ b/auto/modules/php @@ -59,12 +59,6 @@ NXT_PHP_MODULE=${NXT_PHP_MODULE=${NXT_PHP##*/}} NXT_PHP_LIB_PATH=${NXT_PHP_LIB_PATH=} NXT_PHP_LIB_STATIC=${NXT_PHP_LIB_STATIC=no} NXT_PHP_ADDITIONAL_FLAGS= -NXT_PHP_REALPATH=realpath - - -if [ -z `which $NXT_PHP_REALPATH` ]; then - NXT_PHP_REALPATH="readlink -e" -fi $echo "configuring PHP module" @@ -81,12 +75,6 @@ if /bin/sh -c "${NXT_PHP_CONFIG} --version" >> $NXT_AUTOCONF_ERR 2>&1; then NXT_PHP_VERSION="`${NXT_PHP_CONFIG} --version`" NXT_PHP_EXT_DIR="`${NXT_PHP_CONFIG} --extension-dir`" - NXT_PHP_LIBC_DIR="`${CC} --print-file-name=libc.so`" - NXT_PHP_LIBC_DIR="`$NXT_PHP_REALPATH $NXT_PHP_LIBC_DIR`" - NXT_PHP_LIBC_DIR="`dirname $NXT_PHP_LIBC_DIR`" - NXT_PHP_SYSLIB_DIR="`${CC} --print-file-name=libtinfo.so`" - NXT_PHP_SYSLIB_DIR="`$NXT_PHP_REALPATH $NXT_PHP_SYSLIB_DIR`" - NXT_PHP_SYSLIB_DIR="`dirname $NXT_PHP_SYSLIB_DIR`" $echo " + PHP SAPI: [`${NXT_PHP_CONFIG} --php-sapis`]" @@ -228,21 +216,6 @@ if grep ^$NXT_PHP_MODULE: $NXT_MAKEFILE 2>&1 > /dev/null; then fi -NXT_PHP_MOUNTS_HEADER=nxt_${NXT_PHP_MODULE}_mounts.h - -cat << END > $NXT_BUILD_DIR/$NXT_PHP_MOUNTS_HEADER -static const nxt_fs_mount_t nxt_php_mounts[] = { - {(u_char *) "$NXT_PHP_EXT_DIR", (u_char *) "$NXT_PHP_EXT_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, - {(u_char *) "$NXT_PHP_LIBC_DIR", (u_char *) "$NXT_PHP_LIBC_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, - {(u_char *) "$NXT_PHP_SYSLIB_DIR", (u_char *) "$NXT_PHP_SYSLIB_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, -}; - -END - - $echo " + PHP module: ${NXT_PHP_MODULE}.unit.so" . auto/cc/deps @@ -268,8 +241,7 @@ for nxt_src in $NXT_PHP_MODULE_SRCS; do cat << END >> $NXT_MAKEFILE $NXT_BUILD_DIR/$nxt_obj: $nxt_src $NXT_VERSION_H - \$(CC) -c \$(CFLAGS) -DNXT_PHP_MOUNTS_H=\"$NXT_PHP_MOUNTS_HEADER\" \\ - $NXT_PHP_ADDITIONAL_FLAGS \$(NXT_INCS) \\ + \$(CC) -c \$(CFLAGS) $NXT_PHP_ADDITIONAL_FLAGS \$(NXT_INCS) \\ $NXT_PHP_INCLUDE -DNXT_ZEND_SIGNAL_STARTUP=$NXT_ZEND_SIGNAL_STARTUP \\ $nxt_dep_flags \\ -o $NXT_BUILD_DIR/$nxt_obj $nxt_src diff --git a/src/nxt_php_sapi.c b/src/nxt_php_sapi.c index d7e5b476..de329ad7 100644 --- a/src/nxt_php_sapi.c +++ b/src/nxt_php_sapi.c @@ -14,8 +14,6 @@ #include #include -#include NXT_PHP_MOUNTS_H - #if PHP_VERSION_ID >= 50400 #define NXT_HAVE_PHP_IGNORE_CWD 1 @@ -79,9 +77,13 @@ typedef void (*zif_handler)(INTERNAL_FUNCTION_PARAMETERS); #endif +static nxt_int_t nxt_php_setup(nxt_task_t *task, nxt_process_t *process, + nxt_common_app_conf_t *conf); static nxt_int_t nxt_php_start(nxt_task_t *task, nxt_process_data_t *data); static nxt_int_t nxt_php_set_target(nxt_task_t *task, nxt_php_target_t *target, nxt_conf_value_t *conf); +static nxt_int_t nxt_php_set_ini_path(nxt_task_t *task, nxt_str_t *path, + char *workdir); static void nxt_php_set_options(nxt_task_t *task, nxt_conf_value_t *options, int type); static nxt_int_t nxt_php_alter_option(nxt_str_t *name, nxt_str_t *value, @@ -252,9 +254,9 @@ NXT_EXPORT nxt_app_module_t nxt_app_module = { compat, nxt_string("php"), PHP_VERSION, - nxt_php_mounts, - nxt_nitems(nxt_php_mounts), NULL, + 0, + nxt_php_setup, nxt_php_start, }; @@ -269,55 +271,20 @@ static void ***tsrm_ls; static nxt_int_t -nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) +nxt_php_setup(nxt_task_t *task, nxt_process_t *process, + nxt_common_app_conf_t *conf) { - u_char *p; - uint32_t next; - nxt_str_t ini_path, name; - nxt_int_t ret; - nxt_uint_t n; - nxt_unit_ctx_t *unit_ctx; - nxt_unit_init_t php_init; - nxt_conf_value_t *value; - nxt_php_app_conf_t *c; - nxt_common_app_conf_t *conf; + nxt_str_t ini_path; + nxt_int_t ret; + nxt_conf_value_t *value; + nxt_php_app_conf_t *c; static nxt_str_t file_str = nxt_string("file"); static nxt_str_t user_str = nxt_string("user"); static nxt_str_t admin_str = nxt_string("admin"); - conf = data->app; c = &conf->u.php; - n = (c->targets != NULL) ? nxt_conf_object_members_count(c->targets) : 1; - - nxt_php_targets = nxt_zalloc(sizeof(nxt_php_target_t) * n); - if (nxt_slow_path(nxt_php_targets == NULL)) { - return NXT_ERROR; - } - - if (c->targets != NULL) { - next = 0; - - for (n = 0; /* void */; n++) { - value = nxt_conf_next_object_member(c->targets, &name, &next); - if (value == NULL) { - break; - } - - ret = nxt_php_set_target(task, &nxt_php_targets[n], value); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - } else { - ret = nxt_php_set_target(task, &nxt_php_targets[0], conf->self); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - #ifdef ZTS #if PHP_VERSION_ID >= 70400 @@ -347,15 +314,12 @@ nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) if (value != NULL) { nxt_conf_get_string(value, &ini_path); - p = nxt_malloc(ini_path.length + 1); - if (nxt_slow_path(p == NULL)) { + ret = nxt_php_set_ini_path(task, &ini_path, + conf->working_directory); + + if (nxt_slow_path(ret != NXT_OK)) { return NXT_ERROR; } - - nxt_php_sapi_module.php_ini_path_override = (char *) p; - - p = nxt_cpymem(p, ini_path.start, ini_path.length); - *p = '\0'; } } @@ -372,6 +336,55 @@ nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) nxt_php_set_options(task, value, ZEND_INI_USER); } + return NXT_OK; +} + + +static nxt_int_t +nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) +{ + uint32_t next; + nxt_int_t ret; + nxt_str_t name; + nxt_uint_t n; + nxt_unit_ctx_t *unit_ctx; + nxt_unit_init_t php_init; + nxt_conf_value_t *value; + nxt_php_app_conf_t *c; + nxt_common_app_conf_t *conf; + + conf = data->app; + c = &conf->u.php; + + n = (c->targets != NULL) ? nxt_conf_object_members_count(c->targets) : 1; + + nxt_php_targets = nxt_zalloc(sizeof(nxt_php_target_t) * n); + if (nxt_slow_path(nxt_php_targets == NULL)) { + return NXT_ERROR; + } + + if (c->targets != NULL) { + next = 0; + + for (n = 0; /* void */; n++) { + value = nxt_conf_next_object_member(c->targets, &name, &next); + if (value == NULL) { + break; + } + + ret = nxt_php_set_target(task, &nxt_php_targets[n], value); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + } else { + ret = nxt_php_set_target(task, &nxt_php_targets[0], conf->self); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + ret = nxt_unit_default_init(task, &php_init); if (nxt_slow_path(ret != NXT_OK)) { nxt_alert(task, "nxt_unit_default_init() failed"); @@ -388,9 +401,8 @@ nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) nxt_php_unit_ctx = unit_ctx; - nxt_unit_run(unit_ctx); - - nxt_unit_done(unit_ctx); + nxt_unit_run(nxt_php_unit_ctx); + nxt_unit_done(nxt_php_unit_ctx); exit(0); @@ -512,6 +524,46 @@ nxt_php_set_target(nxt_task_t *task, nxt_php_target_t *target, } +static nxt_int_t +nxt_php_set_ini_path(nxt_task_t *task, nxt_str_t *ini_path, char *workdir) +{ + size_t wdlen; + u_char *p, *start; + + if (ini_path->start[0] == '/' || workdir == NULL) { + p = nxt_malloc(ini_path->length + 1); + if (nxt_slow_path(p == NULL)) { + return NXT_ERROR; + } + + start = p; + + } else { + wdlen = nxt_strlen(workdir); + + p = nxt_malloc(wdlen + ini_path->length + 2); + if (nxt_slow_path(p == NULL)) { + return NXT_ERROR; + } + + start = p; + + p = nxt_cpymem(p, workdir, wdlen); + + if (workdir[wdlen - 1] != '/') { + *p++ = '/'; + } + } + + p = nxt_cpymem(p, ini_path->start, ini_path->length); + *p = '\0'; + + nxt_php_sapi_module.php_ini_path_override = (char *) start; + + return NXT_OK; +} + + static void nxt_php_set_options(nxt_task_t *task, nxt_conf_value_t *options, int type) { diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index f4170f1b..556bd387 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -85,54 +85,3 @@ class TestPHPIsolation(TestApplicationPHP): assert 'json' in extensions, 'json in extensions list' assert 'unit' in extensions, 'unit in extensions list' - - def test_php_isolation_rootfs_no_language_libs(self, is_su): - isolation_features = self.available['features']['isolation'].keys() - - if not is_su: - if 'user' not in isolation_features: - pytest.skip('requires unprivileged userns or root') - - if not 'unprivileged_userns_clone' in isolation_features: - pytest.skip('requires unprivileged userns or root') - - if 'mnt' not in isolation_features: - pytest.skip('requires mnt ns') - - isolation = { - 'rootfs': option.test_dir, - 'automount': {'language_deps': False}, - 'namespaces': {'credential': not is_su, 'mount': not is_su}, - } - - self.load('list-extensions', isolation=isolation) - - assert 'success' in self.conf( - '"/php/list-extensions"', 'applications/list-extensions/root' - ) - - assert 'success' in self.conf( - {'file': '/php/list-extensions/php.ini'}, - 'applications/list-extensions/options', - ) - - assert 'success' in self.conf( - '"/php/list-extensions"', - 'applications/list-extensions/working_directory', - ) - - extensions = self.getjson()['body'] - - assert 'unit' in extensions, 'unit in extensions list' - assert 'json' not in extensions, 'json not in extensions list' - - assert 'success' in self.conf( - {'language_deps': True}, - 'applications/list-extensions/isolation/automount', - ) - - extensions = self.getjson()['body'] - - assert 'unit' in extensions, 'unit in extensions list 2' - assert 'json' in extensions, 'json in extensions list 2' - diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 564ec79c..59ac670a 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -65,3 +65,33 @@ class TestPythonIsolation(TestApplicationPython): assert ( ret['body']['FileExists'] == True ), 'application exists in rootfs' + + def test_python_isolation_rootfs_no_language_deps(self, is_su): + isolation_features = self.available['features']['isolation'].keys() + + if 'mnt' not in isolation_features: + pytest.skip('requires mnt ns') + + if not is_su: + if 'user' not in isolation_features: + pytest.skip('requires unprivileged userns or root') + + if not 'unprivileged_userns_clone' in isolation_features: + pytest.skip('requires unprivileged userns or root') + + isolation = { + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': self.temp_dir, + 'automount': {'language_deps': False} + } + + self.load('empty', isolation=isolation) + + assert (self.get()['status'] != 200), 'disabled language_deps' + + isolation['automount']['language_deps'] = True + + self.load('empty', isolation=isolation) + + assert (self.get()['status'] == 200), 'enabled language_deps' + -- cgit From bbc6d2470afe8bfc8a97427b0b576080466bd31a Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 1 Oct 2020 23:55:10 +0300 Subject: Publishing libunit's malloc() and free() wrappers for apps. --- src/nxt_unit.c | 102 ++++++++++++++++++++++++++++++--------------------------- src/nxt_unit.h | 4 +++ 2 files changed, 57 insertions(+), 49 deletions(-) diff --git a/src/nxt_unit.c b/src/nxt_unit.c index 28295e22..4e468124 100644 --- a/src/nxt_unit.c +++ b/src/nxt_unit.c @@ -74,7 +74,8 @@ static void nxt_unit_request_info_free(nxt_unit_request_info_impl_t *req); static nxt_unit_websocket_frame_impl_t *nxt_unit_websocket_frame_get( nxt_unit_ctx_t *ctx); static void nxt_unit_websocket_frame_release(nxt_unit_websocket_frame_t *ws); -static void nxt_unit_websocket_frame_free(nxt_unit_websocket_frame_impl_t *ws); +static void nxt_unit_websocket_frame_free(nxt_unit_ctx_t *ctx, + nxt_unit_websocket_frame_impl_t *ws); static nxt_unit_mmap_buf_t *nxt_unit_mmap_buf_get(nxt_unit_ctx_t *ctx); static void nxt_unit_mmap_buf_release(nxt_unit_mmap_buf_t *mmap_buf); static int nxt_unit_mmap_buf_send(nxt_unit_request_info_t *req, @@ -119,8 +120,7 @@ static void nxt_unit_mmap_release(nxt_unit_ctx_t *ctx, nxt_port_mmap_header_t *hdr, void *start, uint32_t size); static int nxt_unit_send_shm_ack(nxt_unit_ctx_t *ctx, pid_t pid); -static nxt_unit_process_t *nxt_unit_process_get(nxt_unit_impl_t *lib, - pid_t pid); +static nxt_unit_process_t *nxt_unit_process_get(nxt_unit_ctx_t *ctx, pid_t pid); static nxt_unit_process_t *nxt_unit_process_find(nxt_unit_impl_t *lib, pid_t pid, int remove); static nxt_unit_process_t *nxt_unit_process_pop_first(nxt_unit_impl_t *lib); @@ -186,8 +186,6 @@ static nxt_unit_request_info_t *nxt_unit_request_hash_find( static char * nxt_unit_snprint_prefix(char *p, char *end, pid_t pid, int level); static void *nxt_unit_lvlhsh_alloc(void *data, size_t size); static void nxt_unit_lvlhsh_free(void *data, void *p); -static void *nxt_unit_malloc(size_t size); -static void nxt_unit_free(void *p); static int nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length); @@ -536,7 +534,8 @@ nxt_unit_create(nxt_unit_init_t *init) nxt_unit_impl_t *lib; nxt_unit_callbacks_t *cb; - lib = nxt_unit_malloc(sizeof(nxt_unit_impl_t) + init->request_data_size); + lib = nxt_unit_malloc(NULL, + sizeof(nxt_unit_impl_t) + init->request_data_size); if (nxt_slow_path(lib == NULL)) { nxt_unit_alert(NULL, "failed to allocate unit struct"); @@ -591,7 +590,7 @@ nxt_unit_create(nxt_unit_init_t *init) fail: - nxt_unit_free(lib); + nxt_unit_free(NULL, lib); return NULL; } @@ -715,7 +714,7 @@ nxt_unit_lib_release(nxt_unit_impl_t *lib) nxt_unit_mmaps_destroy(&lib->incoming); nxt_unit_mmaps_destroy(&lib->outgoing); - nxt_unit_free(lib); + nxt_unit_free(NULL, lib); } } @@ -1393,7 +1392,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, return NXT_UNIT_AGAIN; } - port_impl = nxt_unit_malloc(sizeof(nxt_unit_port_impl_t)); + port_impl = nxt_unit_malloc(ctx, sizeof(nxt_unit_port_impl_t)); if (nxt_slow_path(port_impl == NULL)) { nxt_unit_alert(ctx, "check_response_port: malloc(%d) failed", (int) sizeof(nxt_unit_port_impl_t)); @@ -1417,7 +1416,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, pthread_mutex_unlock(&lib->mutex); - nxt_unit_free(port); + nxt_unit_free(ctx, port); return NXT_UNIT_ERROR; } @@ -1431,7 +1430,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, pthread_mutex_unlock(&lib->mutex); - nxt_unit_free(port); + nxt_unit_free(ctx, port); return NXT_UNIT_ERROR; } @@ -1639,8 +1638,8 @@ nxt_unit_request_info_get(nxt_unit_ctx_t *ctx) if (nxt_queue_is_empty(&ctx_impl->free_req)) { pthread_mutex_unlock(&ctx_impl->mutex); - req_impl = nxt_unit_malloc(sizeof(nxt_unit_request_info_impl_t) - + lib->request_data_size); + req_impl = nxt_unit_malloc(ctx, sizeof(nxt_unit_request_info_impl_t) + + lib->request_data_size); if (nxt_slow_path(req_impl == NULL)) { return NULL; } @@ -1727,7 +1726,7 @@ nxt_unit_request_info_free(nxt_unit_request_info_impl_t *req_impl) nxt_queue_remove(&req_impl->link); if (req_impl != &ctx_impl->req) { - nxt_unit_free(req_impl); + nxt_unit_free(&ctx_impl->ctx, req_impl); } } @@ -1746,7 +1745,7 @@ nxt_unit_websocket_frame_get(nxt_unit_ctx_t *ctx) if (nxt_queue_is_empty(&ctx_impl->free_ws)) { pthread_mutex_unlock(&ctx_impl->mutex); - ws_impl = nxt_unit_malloc(sizeof(nxt_unit_websocket_frame_impl_t)); + ws_impl = nxt_unit_malloc(ctx, sizeof(nxt_unit_websocket_frame_impl_t)); if (nxt_slow_path(ws_impl == NULL)) { return NULL; } @@ -1788,11 +1787,12 @@ nxt_unit_websocket_frame_release(nxt_unit_websocket_frame_t *ws) static void -nxt_unit_websocket_frame_free(nxt_unit_websocket_frame_impl_t *ws_impl) +nxt_unit_websocket_frame_free(nxt_unit_ctx_t *ctx, + nxt_unit_websocket_frame_impl_t *ws_impl) { nxt_queue_remove(&ws_impl->link); - nxt_unit_free(ws_impl); + nxt_unit_free(ctx, ws_impl); } @@ -2329,7 +2329,7 @@ nxt_unit_mmap_buf_get(nxt_unit_ctx_t *ctx) if (ctx_impl->free_buf == NULL) { pthread_mutex_unlock(&ctx_impl->mutex); - mmap_buf = nxt_unit_malloc(sizeof(nxt_unit_mmap_buf_t)); + mmap_buf = nxt_unit_malloc(ctx, sizeof(nxt_unit_mmap_buf_t)); if (nxt_slow_path(mmap_buf == NULL)) { return NULL; } @@ -2647,7 +2647,7 @@ nxt_unit_free_outgoing_buf(nxt_unit_mmap_buf_t *mmap_buf) } if (mmap_buf->free_ptr != NULL) { - nxt_unit_free(mmap_buf->free_ptr); + nxt_unit_free(&mmap_buf->ctx_impl->ctx, mmap_buf->free_ptr); mmap_buf->free_ptr = NULL; } @@ -2689,7 +2689,7 @@ nxt_unit_read_buf_get_impl(nxt_unit_ctx_impl_t *ctx_impl) return rbuf; } - rbuf = nxt_unit_malloc(sizeof(nxt_unit_read_buf_t)); + rbuf = nxt_unit_malloc(&ctx_impl->ctx, sizeof(nxt_unit_read_buf_t)); if (nxt_fast_path(rbuf != NULL)) { rbuf->ctx_impl = ctx_impl; @@ -3048,7 +3048,7 @@ nxt_unit_request_preread(nxt_unit_request_info_t *req, size_t size) return NULL; } - mmap_buf->free_ptr = nxt_unit_malloc(size); + mmap_buf->free_ptr = nxt_unit_malloc(req->ctx, size); if (nxt_slow_path(mmap_buf->free_ptr == NULL)) { nxt_unit_req_alert(req, "preread: failed to allocate buf memory"); nxt_unit_mmap_buf_release(mmap_buf); @@ -3331,7 +3331,7 @@ nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) size = ws_impl->buf->buf.end - ws_impl->buf->buf.start; - b = nxt_unit_malloc(size); + b = nxt_unit_malloc(ws->req->ctx, size); if (nxt_slow_path(b == NULL)) { return NXT_UNIT_ERROR; } @@ -3839,7 +3839,8 @@ nxt_unit_get_outgoing_buf(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, mmap_buf->plain_ptr = local_buf; } else { - mmap_buf->free_ptr = nxt_unit_malloc(size + sizeof(nxt_port_msg_t)); + mmap_buf->free_ptr = nxt_unit_malloc(ctx, + size + sizeof(nxt_port_msg_t)); if (nxt_slow_path(mmap_buf->free_ptr == NULL)) { return NXT_UNIT_ERROR; } @@ -4009,7 +4010,7 @@ nxt_unit_process_release(nxt_unit_process_t *process) if (c == 1) { nxt_unit_debug(NULL, "destroy process #%d", (int) process->pid); - nxt_unit_free(process); + nxt_unit_free(NULL, process); } } @@ -4026,7 +4027,7 @@ nxt_unit_mmaps_destroy(nxt_unit_mmaps_t *mmaps) munmap(mm->hdr, PORT_MMAP_SIZE); } - nxt_unit_free(mmaps->elts); + nxt_unit_free(NULL, mmaps->elts); } pthread_mutex_destroy(&mmaps->mutex); @@ -4314,11 +4315,14 @@ nxt_unit_process_lhq_pid(nxt_lvlhsh_query_t *lhq, pid_t *pid) static nxt_unit_process_t * -nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) +nxt_unit_process_get(nxt_unit_ctx_t *ctx, pid_t pid) { + nxt_unit_impl_t *lib; nxt_unit_process_t *process; nxt_lvlhsh_query_t lhq; + lib = nxt_container_of(ctx->unit, nxt_unit_impl_t, unit); + nxt_unit_process_lhq_pid(&lhq, &pid); if (nxt_lvlhsh_find(&lib->processes, &lhq) == NXT_OK) { @@ -4328,9 +4332,9 @@ nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) return process; } - process = nxt_unit_malloc(sizeof(nxt_unit_process_t)); + process = nxt_unit_malloc(ctx, sizeof(nxt_unit_process_t)); if (nxt_slow_path(process == NULL)) { - nxt_unit_alert(NULL, "failed to allocate process for #%d", (int) pid); + nxt_unit_alert(ctx, "failed to allocate process for #%d", (int) pid); return NULL; } @@ -4351,9 +4355,9 @@ nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) break; default: - nxt_unit_alert(NULL, "process %d insert failed", (int) pid); + nxt_unit_alert(ctx, "process %d insert failed", (int) pid); - nxt_unit_free(process); + nxt_unit_free(ctx, process); process = NULL; break; } @@ -4924,8 +4928,8 @@ nxt_unit_ctx_alloc(nxt_unit_ctx_t *ctx, void *data) lib = nxt_container_of(ctx->unit, nxt_unit_impl_t, unit); - new_ctx = nxt_unit_malloc(sizeof(nxt_unit_ctx_impl_t) - + lib->request_data_size); + new_ctx = nxt_unit_malloc(ctx, sizeof(nxt_unit_ctx_impl_t) + + lib->request_data_size); if (nxt_slow_path(new_ctx == NULL)) { nxt_unit_alert(ctx, "failed to allocate context"); @@ -4934,7 +4938,7 @@ nxt_unit_ctx_alloc(nxt_unit_ctx_t *ctx, void *data) rc = nxt_unit_ctx_init(lib, new_ctx, data); if (nxt_slow_path(rc != NXT_UNIT_OK)) { - nxt_unit_free(new_ctx); + nxt_unit_free(ctx, new_ctx); return NULL; } @@ -5013,7 +5017,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) while (ctx_impl->free_buf != NULL) { mmap_buf = ctx_impl->free_buf; nxt_unit_mmap_buf_unlink(mmap_buf); - nxt_unit_free(mmap_buf); + nxt_unit_free(&ctx_impl->ctx, mmap_buf); } nxt_queue_each(req_impl, &ctx_impl->free_req, @@ -5026,7 +5030,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) nxt_queue_each(ws_impl, &ctx_impl->free_ws, nxt_unit_websocket_frame_impl_t, link) { - nxt_unit_websocket_frame_free(ws_impl); + nxt_unit_websocket_frame_free(&ctx_impl->ctx, ws_impl); } nxt_queue_loop; @@ -5040,7 +5044,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) } if (ctx_impl != &lib->main_ctx) { - nxt_unit_free(ctx_impl); + nxt_unit_free(&lib->main_ctx.ctx, ctx_impl); } nxt_unit_lib_release(lib); @@ -5092,7 +5096,7 @@ nxt_unit_create_port(nxt_unit_ctx_t *ctx) pthread_mutex_lock(&lib->mutex); - process = nxt_unit_process_get(lib, lib->pid); + process = nxt_unit_process_get(ctx, lib->pid); if (nxt_slow_path(process == NULL)) { pthread_mutex_unlock(&lib->mutex); @@ -5225,7 +5229,7 @@ nxt_inline void nxt_unit_port_release(nxt_unit_port_t *port) : sizeof(nxt_port_queue_t)); } - nxt_unit_free(port_impl); + nxt_unit_free(NULL, port_impl); } } @@ -5332,7 +5336,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) port->id.pid, port->id.id, port->in_fd, port->out_fd, queue); - process = nxt_unit_process_get(lib, port->id.pid); + process = nxt_unit_process_get(ctx, port->id.pid); if (nxt_slow_path(process == NULL)) { goto unlock; } @@ -5341,7 +5345,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) process->next_port_id = port->id.id + 1; } - new_port = nxt_unit_malloc(sizeof(nxt_unit_port_impl_t)); + new_port = nxt_unit_malloc(ctx, sizeof(nxt_unit_port_impl_t)); if (nxt_slow_path(new_port == NULL)) { nxt_unit_alert(ctx, "add_port: %d,%d malloc() failed", port->id.pid, port->id.id); @@ -5356,7 +5360,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) nxt_unit_alert(ctx, "add_port: %d,%d hash_add failed", port->id.pid, port->id.id); - nxt_unit_free(new_port); + nxt_unit_free(ctx, new_port); new_port = NULL; @@ -6376,22 +6380,22 @@ nxt_unit_lvlhsh_alloc(void *data, size_t size) static void nxt_unit_lvlhsh_free(void *data, void *p) { - nxt_unit_free(p); + nxt_unit_free(NULL, p); } -static void * -nxt_unit_malloc(size_t size) +void * +nxt_unit_malloc(nxt_unit_ctx_t *ctx, size_t size) { void *p; p = malloc(size); if (nxt_fast_path(p != NULL)) { - nxt_unit_debug(NULL, "malloc(%d): %p", (int) size, p); + nxt_unit_debug(ctx, "malloc(%d): %p", (int) size, p); } else { - nxt_unit_alert(NULL, "malloc(%d) failed: %s (%d)", + nxt_unit_alert(ctx, "malloc(%d) failed: %s (%d)", (int) size, strerror(errno), errno); } @@ -6399,10 +6403,10 @@ nxt_unit_malloc(size_t size) } -static void -nxt_unit_free(void *p) +void +nxt_unit_free(nxt_unit_ctx_t *ctx, void *p) { - nxt_unit_debug(NULL, "free(%p)", p); + nxt_unit_debug(ctx, "free(%p)", p); free(p); } diff --git a/src/nxt_unit.h b/src/nxt_unit.h index eb3c30fa..e90f0781 100644 --- a/src/nxt_unit.h +++ b/src/nxt_unit.h @@ -322,6 +322,10 @@ int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws); void nxt_unit_websocket_done(nxt_unit_websocket_frame_t *ws); +void *nxt_unit_malloc(nxt_unit_ctx_t *ctx, size_t size); + +void nxt_unit_free(nxt_unit_ctx_t *ctx, void *p); + #if defined __has_attribute #if __has_attribute(format) -- cgit From c4c2f90c5b532c1ec283d211e0fd50e4538c2a51 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 1 Oct 2020 23:55:23 +0300 Subject: Python: ASGI server introduced. This closes #461 issue on GitHub. --- auto/modules/python | 5 + src/python/nxt_python.c | 28 +- src/python/nxt_python.h | 29 + src/python/nxt_python_asgi.c | 1227 ++++++++++++++++++++++++++++++++ src/python/nxt_python_asgi.h | 60 ++ src/python/nxt_python_asgi_http.c | 591 +++++++++++++++ src/python/nxt_python_asgi_lifespan.c | 505 +++++++++++++ src/python/nxt_python_asgi_str.c | 141 ++++ src/python/nxt_python_asgi_str.h | 69 ++ src/python/nxt_python_asgi_websocket.c | 1084 ++++++++++++++++++++++++++++ src/python/nxt_python_wsgi.c | 18 - 11 files changed, 3728 insertions(+), 29 deletions(-) create mode 100644 src/python/nxt_python_asgi.c create mode 100644 src/python/nxt_python_asgi.h create mode 100644 src/python/nxt_python_asgi_http.c create mode 100644 src/python/nxt_python_asgi_lifespan.c create mode 100644 src/python/nxt_python_asgi_str.c create mode 100644 src/python/nxt_python_asgi_str.h create mode 100644 src/python/nxt_python_asgi_websocket.c diff --git a/auto/modules/python b/auto/modules/python index afb1b586..48f6e5ef 100644 --- a/auto/modules/python +++ b/auto/modules/python @@ -168,6 +168,11 @@ $echo >> $NXT_MAKEFILE NXT_PYTHON_MODULE_SRCS=" \ src/python/nxt_python.c \ + src/python/nxt_python_asgi.c \ + src/python/nxt_python_asgi_http.c \ + src/python/nxt_python_asgi_lifespan.c \ + src/python/nxt_python_asgi_str.c \ + src/python/nxt_python_asgi_websocket.c \ src/python/nxt_python_wsgi.c \ " diff --git a/src/python/nxt_python.c b/src/python/nxt_python.c index 7d4589ed..01534a47 100644 --- a/src/python/nxt_python.c +++ b/src/python/nxt_python.c @@ -15,14 +15,6 @@ #include NXT_PYTHON_MOUNTS_H -#if PY_MAJOR_VERSION == 3 -#define PyString_FromStringAndSize(str, size) \ - PyUnicode_DecodeLatin1((str), (size), "strict") - -#else -#define PyUnicode_InternInPlace PyString_InternInPlace -#endif - static nxt_int_t nxt_python_start(nxt_task_t *task, nxt_process_data_t *data); static void nxt_python_atexit(void); @@ -56,7 +48,7 @@ static char *nxt_py_home; static nxt_int_t nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) { - int rc; + int rc, asgi; char *nxt_py_module; size_t len; PyObject *obj, *pypath, *module; @@ -226,7 +218,15 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) python_init.shm_limit = data->app->shm_limit; - rc = nxt_python_wsgi_init(task, &python_init); + asgi = nxt_python_asgi_check(nxt_py_application); + + if (asgi) { + rc = nxt_python_asgi_init(task, &python_init); + + } else { + rc = nxt_python_wsgi_init(task, &python_init); + } + if (nxt_slow_path(rc == NXT_ERROR)) { goto fail; } @@ -236,7 +236,12 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) goto fail; } - rc = nxt_python_wsgi_run(unit_ctx); + if (asgi) { + rc = nxt_python_asgi_run(unit_ctx); + + } else { + rc = nxt_python_wsgi_run(unit_ctx); + } nxt_unit_done(unit_ctx); @@ -300,6 +305,7 @@ static void nxt_python_atexit(void) { nxt_python_wsgi_done(); + nxt_python_asgi_done(); Py_XDECREF(nxt_py_stderr_flush); Py_XDECREF(nxt_py_application); diff --git a/src/python/nxt_python.h b/src/python/nxt_python.h index 417df7fd..3211026b 100644 --- a/src/python/nxt_python.h +++ b/src/python/nxt_python.h @@ -8,9 +8,32 @@ #include +#include #include +#if PY_MAJOR_VERSION == 3 +#define NXT_PYTHON_BYTES_TYPE "bytestring" + +#define PyString_FromStringAndSize(str, size) \ + PyUnicode_DecodeLatin1((str), (size), "strict") +#define PyString_AS_STRING PyUnicode_DATA + +#else +#define NXT_PYTHON_BYTES_TYPE "string" + +#define PyBytes_FromStringAndSize PyString_FromStringAndSize +#define PyBytes_Check PyString_Check +#define PyBytes_GET_SIZE PyString_GET_SIZE +#define PyBytes_AS_STRING PyString_AS_STRING +#define PyUnicode_InternInPlace PyString_InternInPlace +#define PyUnicode_AsUTF8 PyString_AS_STRING +#endif + +#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 5 +#define NXT_HAVE_ASGI 1 +#endif + extern PyObject *nxt_py_application; typedef struct { @@ -18,6 +41,7 @@ typedef struct { PyObject **object_p; } nxt_python_string_t; + nxt_int_t nxt_python_init_strings(nxt_python_string_t *pstr); void nxt_python_done_strings(nxt_python_string_t *pstr); @@ -27,5 +51,10 @@ nxt_int_t nxt_python_wsgi_init(nxt_task_t *task, nxt_unit_init_t *init); int nxt_python_wsgi_run(nxt_unit_ctx_t *ctx); void nxt_python_wsgi_done(void); +int nxt_python_asgi_check(PyObject *obj); +nxt_int_t nxt_python_asgi_init(nxt_task_t *task, nxt_unit_init_t *init); +nxt_int_t nxt_python_asgi_run(nxt_unit_ctx_t *ctx); +void nxt_python_asgi_done(void); + #endif /* _NXT_PYTHON_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi.c b/src/python/nxt_python_asgi.c new file mode 100644 index 00000000..72408ea1 --- /dev/null +++ b/src/python/nxt_python_asgi.c @@ -0,0 +1,1227 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include + +#if (NXT_HAVE_ASGI) + +#include +#include +#include +#include +#include +#include + + +static void nxt_py_asgi_request_handler(nxt_unit_request_info_t *req); + +static PyObject *nxt_py_asgi_create_http_scope(nxt_unit_request_info_t *req); +static PyObject *nxt_py_asgi_create_address(nxt_unit_sptr_t *sptr, uint8_t len, + uint16_t port); +static PyObject *nxt_py_asgi_create_header(nxt_unit_field_t *f); +static PyObject *nxt_py_asgi_create_subprotocols(nxt_unit_field_t *f); + +static int nxt_py_asgi_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port); +static void nxt_py_asgi_remove_port(nxt_unit_t *lib, nxt_unit_port_t *port); +static void nxt_py_asgi_quit(nxt_unit_ctx_t *ctx); +static void nxt_py_asgi_shm_ack_handler(nxt_unit_ctx_t *ctx); + +static PyObject *nxt_py_asgi_port_read(PyObject *self, PyObject *args); + + +PyObject *nxt_py_loop_run_until_complete; +PyObject *nxt_py_loop_create_future; +PyObject *nxt_py_loop_create_task; + +nxt_queue_t nxt_py_asgi_drain_queue; + +static PyObject *nxt_py_loop_call_soon; +static PyObject *nxt_py_quit_future; +static PyObject *nxt_py_quit_future_set_result; +static PyObject *nxt_py_loop_add_reader; +static PyObject *nxt_py_loop_remove_reader; +static PyObject *nxt_py_port_read; + +static PyMethodDef nxt_py_port_read_method = + {"unit_port_read", nxt_py_asgi_port_read, METH_VARARGS, ""}; + +#define NXT_UNIT_HASH_WS_PROTOCOL 0xED0A + + +int +nxt_python_asgi_check(PyObject *obj) +{ + int res; + PyObject *call; + PyCodeObject *code; + + if (PyFunction_Check(obj)) { + code = (PyCodeObject *) PyFunction_GET_CODE(obj); + + return (code->co_flags & CO_COROUTINE) != 0; + } + + if (PyMethod_Check(obj)) { + obj = PyMethod_GET_FUNCTION(obj); + + code = (PyCodeObject *) PyFunction_GET_CODE(obj); + + return (code->co_flags & CO_COROUTINE) != 0; + } + + call = PyObject_GetAttrString(obj, "__call__"); + + if (call == NULL) { + return 0; + } + + if (PyFunction_Check(call)) { + code = (PyCodeObject *) PyFunction_GET_CODE(call); + + res = (code->co_flags & CO_COROUTINE) != 0; + + } else { + if (PyMethod_Check(call)) { + obj = PyMethod_GET_FUNCTION(call); + + code = (PyCodeObject *) PyFunction_GET_CODE(obj); + + res = (code->co_flags & CO_COROUTINE) != 0; + + } else { + res = 0; + } + } + + Py_DECREF(call); + + return res; +} + + +nxt_int_t +nxt_python_asgi_init(nxt_task_t *task, nxt_unit_init_t *init) +{ + PyObject *asyncio, *loop, *get_event_loop; + nxt_int_t rc; + + nxt_debug(task, "asgi_init"); + + if (nxt_slow_path(nxt_py_asgi_str_init() != NXT_OK)) { + nxt_alert(task, "Python failed to init string objects"); + return NXT_ERROR; + } + + asyncio = PyImport_ImportModule("asyncio"); + if (nxt_slow_path(asyncio == NULL)) { + nxt_alert(task, "Python failed to import module 'asyncio'"); + nxt_python_print_exception(); + return NXT_ERROR; + } + + loop = NULL; + get_event_loop = PyDict_GetItemString(PyModule_GetDict(asyncio), + "get_event_loop"); + if (nxt_slow_path(get_event_loop == NULL)) { + nxt_alert(task, + "Python failed to get 'get_event_loop' from module 'asyncio'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(get_event_loop) == 0)) { + nxt_alert(task, "'asyncio.get_event_loop' is not a callable object"); + goto fail; + } + + loop = PyObject_CallObject(get_event_loop, NULL); + if (nxt_slow_path(loop == NULL)) { + nxt_alert(task, "Python failed to call 'asyncio.get_event_loop'"); + goto fail; + } + + nxt_py_loop_create_task = PyObject_GetAttrString(loop, "create_task"); + if (nxt_slow_path(nxt_py_loop_create_task == NULL)) { + nxt_alert(task, "Python failed to get 'loop.create_task'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_create_task) == 0)) { + nxt_alert(task, "'loop.create_task' is not a callable object"); + goto fail; + } + + nxt_py_loop_add_reader = PyObject_GetAttrString(loop, "add_reader"); + if (nxt_slow_path(nxt_py_loop_add_reader == NULL)) { + nxt_alert(task, "Python failed to get 'loop.add_reader'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_add_reader) == 0)) { + nxt_alert(task, "'loop.add_reader' is not a callable object"); + goto fail; + } + + nxt_py_loop_remove_reader = PyObject_GetAttrString(loop, "remove_reader"); + if (nxt_slow_path(nxt_py_loop_remove_reader == NULL)) { + nxt_alert(task, "Python failed to get 'loop.remove_reader'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_remove_reader) == 0)) { + nxt_alert(task, "'loop.remove_reader' is not a callable object"); + goto fail; + } + + nxt_py_loop_call_soon = PyObject_GetAttrString(loop, "call_soon"); + if (nxt_slow_path(nxt_py_loop_call_soon == NULL)) { + nxt_alert(task, "Python failed to get 'loop.call_soon'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_call_soon) == 0)) { + nxt_alert(task, "'loop.call_soon' is not a callable object"); + goto fail; + } + + nxt_py_loop_run_until_complete = PyObject_GetAttrString(loop, + "run_until_complete"); + if (nxt_slow_path(nxt_py_loop_run_until_complete == NULL)) { + nxt_alert(task, "Python failed to get 'loop.run_until_complete'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_run_until_complete) == 0)) { + nxt_alert(task, "'loop.run_until_complete' is not a callable object"); + goto fail; + } + + nxt_py_loop_create_future = PyObject_GetAttrString(loop, "create_future"); + if (nxt_slow_path(nxt_py_loop_create_future == NULL)) { + nxt_alert(task, "Python failed to get 'loop.create_future'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_create_future) == 0)) { + nxt_alert(task, "'loop.create_future' is not a callable object"); + goto fail; + } + + nxt_py_quit_future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(nxt_py_quit_future == NULL)) { + nxt_alert(task, "Python failed to create Future "); + nxt_python_print_exception(); + goto fail; + } + + nxt_py_quit_future_set_result = PyObject_GetAttrString(nxt_py_quit_future, + "set_result"); + if (nxt_slow_path(nxt_py_quit_future_set_result == NULL)) { + nxt_alert(task, "Python failed to get 'future.set_result'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_quit_future_set_result) == 0)) { + nxt_alert(task, "'future.set_result' is not a callable object"); + goto fail; + } + + nxt_py_port_read = PyCFunction_New(&nxt_py_port_read_method, NULL); + if (nxt_slow_path(nxt_py_port_read == NULL)) { + nxt_alert(task, "Python failed to initialize the 'port_read' function"); + goto fail; + } + + nxt_queue_init(&nxt_py_asgi_drain_queue); + + if (nxt_slow_path(nxt_py_asgi_http_init(task) == NXT_ERROR)) { + goto fail; + } + + if (nxt_slow_path(nxt_py_asgi_websocket_init(task) == NXT_ERROR)) { + goto fail; + } + + rc = nxt_py_asgi_lifespan_startup(task); + if (nxt_slow_path(rc == NXT_ERROR)) { + goto fail; + } + + init->callbacks.request_handler = nxt_py_asgi_request_handler; + init->callbacks.data_handler = nxt_py_asgi_http_data_handler; + init->callbacks.websocket_handler = nxt_py_asgi_websocket_handler; + init->callbacks.close_handler = nxt_py_asgi_websocket_close_handler; + init->callbacks.quit = nxt_py_asgi_quit; + init->callbacks.shm_ack_handler = nxt_py_asgi_shm_ack_handler; + init->callbacks.add_port = nxt_py_asgi_add_port; + init->callbacks.remove_port = nxt_py_asgi_remove_port; + + Py_DECREF(loop); + Py_DECREF(asyncio); + + return NXT_OK; + +fail: + + Py_XDECREF(loop); + Py_DECREF(asyncio); + + return NXT_ERROR; +} + + +nxt_int_t +nxt_python_asgi_run(nxt_unit_ctx_t *ctx) +{ + PyObject *res; + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_run_until_complete, + nxt_py_quit_future, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(ctx, "Python failed to call loop.run_until_complete"); + nxt_python_print_exception(); + + return NXT_ERROR; + } + + Py_DECREF(res); + + nxt_py_asgi_lifespan_shutdown(); + + return NXT_OK; +} + + +static void +nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) +{ + PyObject *scope, *res, *task, *receive, *send, *done, *asgi; + + if (req->request->websocket_handshake) { + asgi = nxt_py_asgi_websocket_create(req); + + } else { + asgi = nxt_py_asgi_http_create(req); + } + + if (nxt_slow_path(asgi == NULL)) { + nxt_unit_req_alert(req, "Python failed to create asgi object"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + return; + } + + receive = PyObject_GetAttrString(asgi, "receive"); + if (nxt_slow_path(receive == NULL)) { + nxt_unit_req_alert(req, "Python failed to get 'receive' method"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_asgi; + } + + send = PyObject_GetAttrString(asgi, "send"); + if (nxt_slow_path(receive == NULL)) { + nxt_unit_req_alert(req, "Python failed to get 'send' method"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_receive; + } + + done = PyObject_GetAttrString(asgi, "_done"); + if (nxt_slow_path(receive == NULL)) { + nxt_unit_req_alert(req, "Python failed to get '_done' method"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_send; + } + + scope = nxt_py_asgi_create_http_scope(req); + if (nxt_slow_path(scope == NULL)) { + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_done; + } + + req->data = asgi; + + res = PyObject_CallFunctionObjArgs(nxt_py_application, + scope, receive, send, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(req, "Python failed to call the application"); + nxt_python_print_exception(); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_scope; + } + + if (nxt_slow_path(!PyCoro_CheckExact(res))) { + nxt_unit_req_error(req, "Application result type is not a coroutine"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + Py_DECREF(res); + + goto release_scope; + } + + task = PyObject_CallFunctionObjArgs(nxt_py_loop_create_task, res, NULL); + if (nxt_slow_path(task == NULL)) { + nxt_unit_req_error(req, "Python failed to call the create_task"); + nxt_python_print_exception(); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + Py_DECREF(res); + + goto release_scope; + } + + Py_DECREF(res); + + res = PyObject_CallMethodObjArgs(task, nxt_py_add_done_callback_str, done, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(req, + "Python failed to call 'task.add_done_callback'"); + nxt_python_print_exception(); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_task; + } + + Py_DECREF(res); +release_task: + Py_DECREF(task); +release_scope: + Py_DECREF(scope); +release_done: + Py_DECREF(done); +release_send: + Py_DECREF(send); +release_receive: + Py_DECREF(receive); +release_asgi: + Py_DECREF(asgi); +} + + +static PyObject * +nxt_py_asgi_create_http_scope(nxt_unit_request_info_t *req) +{ + char *p, *target, *query; + uint32_t target_length, i; + PyObject *scope, *v, *type, *scheme; + PyObject *headers, *header; + nxt_unit_field_t *f; + nxt_unit_request_t *r; + + static const nxt_str_t ws_protocol = nxt_string("sec-websocket-protocol"); + +#define SET_ITEM(dict, key, value) \ + if (nxt_slow_path(PyDict_SetItem(dict, nxt_py_ ## key ## _str, value) \ + == -1)) \ + { \ + nxt_unit_req_alert(req, "Python failed to set '" \ + #dict "." #key "' item"); \ + goto fail; \ + } + + v = NULL; + headers = NULL; + + r = req->request; + + if (r->websocket_handshake) { + type = nxt_py_websocket_str; + scheme = r->tls ? nxt_py_wss_str : nxt_py_ws_str; + + } else { + type = nxt_py_http_str; + scheme = r->tls ? nxt_py_https_str : nxt_py_http_str; + } + + scope = nxt_py_asgi_new_scope(req, type, nxt_py_2_1_str); + if (nxt_slow_path(scope == NULL)) { + return NULL; + } + + p = nxt_unit_sptr_get(&r->version); + SET_ITEM(scope, http_version, p[7] == '1' ? nxt_py_1_1_str + : nxt_py_1_0_str) + SET_ITEM(scope, scheme, scheme) + + v = PyString_FromStringAndSize(nxt_unit_sptr_get(&r->method), + r->method_length); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'method' string"); + goto fail; + } + + SET_ITEM(scope, method, v) + Py_DECREF(v); + + v = PyUnicode_DecodeUTF8(nxt_unit_sptr_get(&r->path), r->path_length, + "replace"); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'path' string"); + goto fail; + } + + SET_ITEM(scope, path, v) + Py_DECREF(v); + + target = nxt_unit_sptr_get(&r->target); + query = nxt_unit_sptr_get(&r->query); + + if (r->query.offset != 0) { + target_length = query - target - 1; + + } else { + target_length = r->target_length; + } + + v = PyBytes_FromStringAndSize(target, target_length); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'raw_path' string"); + goto fail; + } + + SET_ITEM(scope, raw_path, v) + Py_DECREF(v); + + v = PyBytes_FromStringAndSize(query, r->query_length); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'query' string"); + goto fail; + } + + SET_ITEM(scope, query_string, v) + Py_DECREF(v); + + v = nxt_py_asgi_create_address(&r->remote, r->remote_length, 0); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'client' pair"); + goto fail; + } + + SET_ITEM(scope, client, v) + Py_DECREF(v); + + v = nxt_py_asgi_create_address(&r->local, r->local_length, 80); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'server' pair"); + goto fail; + } + + SET_ITEM(scope, server, v) + Py_DECREF(v); + + v = NULL; + + headers = PyTuple_New(r->fields_count); + if (nxt_slow_path(headers == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'headers' object"); + goto fail; + } + + for (i = 0; i < r->fields_count; i++) { + f = r->fields + i; + + header = nxt_py_asgi_create_header(f); + if (nxt_slow_path(header == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'header' pair"); + goto fail; + } + + PyTuple_SET_ITEM(headers, i, header); + + if (f->hash == NXT_UNIT_HASH_WS_PROTOCOL + && f->name_length == ws_protocol.length + && f->value_length > 0 + && r->websocket_handshake) + { + v = nxt_py_asgi_create_subprotocols(f); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Failed to create subprotocols"); + goto fail; + } + + SET_ITEM(scope, subprotocols, v); + Py_DECREF(v); + } + } + + SET_ITEM(scope, headers, headers) + Py_DECREF(headers); + + return scope; + +fail: + + Py_XDECREF(v); + Py_XDECREF(headers); + Py_DECREF(scope); + + return NULL; + +#undef SET_ITEM +} + + +static PyObject * +nxt_py_asgi_create_address(nxt_unit_sptr_t *sptr, uint8_t len, uint16_t port) +{ + char *p, *s; + PyObject *pair, *v; + + pair = PyTuple_New(2); + if (nxt_slow_path(pair == NULL)) { + return NULL; + } + + p = nxt_unit_sptr_get(sptr); + s = memchr(p, ':', len); + + v = PyString_FromStringAndSize(p, s == NULL ? len : s - p); + if (nxt_slow_path(v == NULL)) { + Py_DECREF(pair); + + return NULL; + } + + PyTuple_SET_ITEM(pair, 0, v); + + if (s != NULL) { + p += len; + v = PyLong_FromString(s + 1, &p, 10); + + } else { + v = PyLong_FromLong(port); + } + + if (nxt_slow_path(v == NULL)) { + Py_DECREF(pair); + + return NULL; + } + + PyTuple_SET_ITEM(pair, 1, v); + + return pair; +} + + +static PyObject * +nxt_py_asgi_create_header(nxt_unit_field_t *f) +{ + char c, *name; + uint8_t pos; + PyObject *header, *v; + + header = PyTuple_New(2); + if (nxt_slow_path(header == NULL)) { + return NULL; + } + + name = nxt_unit_sptr_get(&f->name); + + for (pos = 0; pos < f->name_length; pos++) { + c = name[pos]; + if (c >= 'A' && c <= 'Z') { + name[pos] = (c | 0x20); + } + } + + v = PyBytes_FromStringAndSize(name, f->name_length); + if (nxt_slow_path(v == NULL)) { + Py_DECREF(header); + + return NULL; + } + + PyTuple_SET_ITEM(header, 0, v); + + v = PyBytes_FromStringAndSize(nxt_unit_sptr_get(&f->value), + f->value_length); + if (nxt_slow_path(v == NULL)) { + Py_DECREF(header); + + return NULL; + } + + PyTuple_SET_ITEM(header, 1, v); + + return header; +} + + +static PyObject * +nxt_py_asgi_create_subprotocols(nxt_unit_field_t *f) +{ + char *v; + uint32_t i, n, start; + PyObject *res, *proto; + + v = nxt_unit_sptr_get(&f->value); + n = 1; + + for (i = 0; i < f->value_length; i++) { + if (v[i] == ',') { + n++; + } + } + + res = PyTuple_New(n); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + n = 0; + start = 0; + + for (i = 0; i < f->value_length; ) { + if (v[i] != ',') { + i++; + + continue; + } + + if (i - start > 0) { + proto = PyString_FromStringAndSize(v + start, i - start); + if (nxt_slow_path(proto == NULL)) { + goto fail; + } + + PyTuple_SET_ITEM(res, n, proto); + + n++; + } + + do { + i++; + } while (i < f->value_length && v[i] == ' '); + + start = i; + } + + if (i - start > 0) { + proto = PyString_FromStringAndSize(v + start, i - start); + if (nxt_slow_path(proto == NULL)) { + goto fail; + } + + PyTuple_SET_ITEM(res, n, proto); + } + + return res; + +fail: + + Py_DECREF(res); + + return NULL; +} + + +static int +nxt_py_asgi_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port) +{ + int nb; + PyObject *res; + + if (port->in_fd == -1) { + return NXT_UNIT_OK; + } + + nb = 1; + + if (nxt_slow_path(ioctl(port->in_fd, FIONBIO, &nb) == -1)) { + nxt_unit_alert(ctx, "ioctl(%d, FIONBIO, 0) failed: %s (%d)", + port->in_fd, strerror(errno), errno); + + return NXT_UNIT_ERROR; + } + + nxt_unit_debug(ctx, "asgi_add_port %d %p %p", port->in_fd, ctx, port); + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_add_reader, + PyLong_FromLong(port->in_fd), + nxt_py_port_read, + PyLong_FromVoidPtr(ctx), + PyLong_FromVoidPtr(port), NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(ctx, "Python failed to add_reader"); + + return NXT_UNIT_ERROR; + } + + Py_DECREF(res); + + return NXT_UNIT_OK; +} + + +static void +nxt_py_asgi_remove_port(nxt_unit_t *lib, nxt_unit_port_t *port) +{ + PyObject *res; + + nxt_unit_debug(NULL, "asgi_remove_port %d %p", port->in_fd, port); + + if (port->in_fd == -1) { + return; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_remove_reader, + PyLong_FromLong(port->in_fd), NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Python failed to remove_reader"); + } + + Py_DECREF(res); +} + + +static void +nxt_py_asgi_quit(nxt_unit_ctx_t *ctx) +{ + PyObject *res; + + nxt_unit_debug(ctx, "asgi_quit %p", ctx); + + res = PyObject_CallFunctionObjArgs(nxt_py_quit_future_set_result, + PyLong_FromLong(0), NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(ctx, "Python failed to set_result"); + } + + Py_DECREF(res); +} + + +static void +nxt_py_asgi_shm_ack_handler(nxt_unit_ctx_t *ctx) +{ + int rc; + nxt_queue_link_t *lnk; + + while (!nxt_queue_is_empty(&nxt_py_asgi_drain_queue)) { + lnk = nxt_queue_first(&nxt_py_asgi_drain_queue); + + rc = nxt_py_asgi_http_drain(lnk); + if (rc == NXT_UNIT_AGAIN) { + break; + } + + nxt_queue_remove(lnk); + } +} + + +static PyObject * +nxt_py_asgi_port_read(PyObject *self, PyObject *args) +{ + int rc; + PyObject *arg; + Py_ssize_t n; + nxt_unit_ctx_t *ctx; + nxt_unit_port_t *port; + + n = PyTuple_GET_SIZE(args); + + if (n != 2) { + nxt_unit_alert(NULL, + "nxt_py_asgi_port_read: invalid number of arguments %d", + (int) n); + + return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); + } + + arg = PyTuple_GET_ITEM(args, 0); + if (nxt_slow_path(arg == NULL || PyLong_Check(arg) == 0)) { + return PyErr_Format(PyExc_TypeError, + "the first argument is not a long"); + } + + ctx = PyLong_AsVoidPtr(arg); + + arg = PyTuple_GET_ITEM(args, 1); + if (nxt_slow_path(arg == NULL || PyLong_Check(arg) == 0)) { + return PyErr_Format(PyExc_TypeError, + "the second argument is not a long"); + } + + port = PyLong_AsVoidPtr(arg); + + nxt_unit_debug(ctx, "asgi_port_read %p %p", ctx, port); + + rc = nxt_unit_process_port_msg(ctx, port); + + if (nxt_slow_path(rc == NXT_UNIT_ERROR)) { + return PyErr_Format(PyExc_RuntimeError, + "error processing port message"); + } + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_enum_headers(PyObject *headers, nxt_py_asgi_enum_header_cb cb, + void *data) +{ + int i; + PyObject *iter, *header, *h_iter, *name, *val, *res; + + iter = PyObject_GetIter(headers); + if (nxt_slow_path(iter == NULL)) { + return PyErr_Format(PyExc_TypeError, "'headers' is not an iterable"); + } + + for (i = 0; /* void */; i++) { + header = PyIter_Next(iter); + if (header == NULL) { + break; + } + + h_iter = PyObject_GetIter(header); + if (nxt_slow_path(h_iter == NULL)) { + Py_DECREF(header); + Py_DECREF(iter); + + return PyErr_Format(PyExc_TypeError, + "'headers' item #%d is not an iterable", i); + } + + name = PyIter_Next(h_iter); + if (nxt_slow_path(name == NULL || !PyBytes_Check(name))) { + Py_XDECREF(name); + Py_DECREF(h_iter); + Py_DECREF(header); + Py_DECREF(iter); + + return PyErr_Format(PyExc_TypeError, + "'headers' item #%d 'name' is not a byte string", i); + } + + val = PyIter_Next(h_iter); + if (nxt_slow_path(val == NULL || !PyBytes_Check(val))) { + Py_XDECREF(val); + Py_DECREF(h_iter); + Py_DECREF(header); + Py_DECREF(iter); + + return PyErr_Format(PyExc_TypeError, + "'headers' item #%d 'value' is not a byte string", i); + } + + res = cb(data, i, name, val); + + Py_DECREF(name); + Py_DECREF(val); + Py_DECREF(h_iter); + Py_DECREF(header); + + if (nxt_slow_path(res == NULL)) { + Py_DECREF(iter); + + return NULL; + } + + Py_DECREF(res); + } + + Py_DECREF(iter); + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_calc_size(void *data, int i, PyObject *name, PyObject *val) +{ + nxt_py_asgi_calc_size_ctx_t *ctx; + + ctx = data; + + ctx->fields_count++; + ctx->fields_size += PyBytes_GET_SIZE(name) + PyBytes_GET_SIZE(val); + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_add_field(void *data, int i, PyObject *name, PyObject *val) +{ + int rc; + char *name_str, *val_str; + uint32_t name_len, val_len; + nxt_off_t content_length; + nxt_unit_request_info_t *req; + nxt_py_asgi_add_field_ctx_t *ctx; + + name_str = PyBytes_AS_STRING(name); + name_len = PyBytes_GET_SIZE(name); + + val_str = PyBytes_AS_STRING(val); + val_len = PyBytes_GET_SIZE(val); + + ctx = data; + req = ctx->req; + + rc = nxt_unit_response_add_field(req, name_str, name_len, + val_str, val_len); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to add header #%d", i); + } + + if (req->response->fields[i].hash == NXT_UNIT_HASH_CONTENT_LENGTH) { + content_length = nxt_off_t_parse((u_char *) val_str, val_len); + if (nxt_slow_path(content_length < 0)) { + nxt_unit_req_error(req, "failed to parse Content-Length " + "value %.*s", (int) val_len, val_str); + + return PyErr_Format(PyExc_ValueError, + "Failed to parse Content-Length: '%.*s'", + (int) val_len, val_str); + } + + ctx->content_length = content_length; + } + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_set_result_soon(nxt_unit_request_info_t *req, PyObject *future, + PyObject *result) +{ + PyObject *set_result, *res; + + if (nxt_slow_path(result == NULL)) { + Py_DECREF(future); + + return NULL; + } + + set_result = PyObject_GetAttrString(future, "set_result"); + if (nxt_slow_path(set_result == NULL)) { + nxt_unit_req_alert(req, "failed to get 'set_result' for future"); + + Py_CLEAR(future); + + goto cleanup; + } + + if (nxt_slow_path(PyCallable_Check(set_result) == 0)) { + nxt_unit_req_alert(req, "'future.set_result' is not a callable"); + + Py_CLEAR(future); + + goto cleanup; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_call_soon, set_result, + result, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(req, "Python failed to call 'loop.call_soon'"); + nxt_python_print_exception(); + + Py_CLEAR(future); + } + + Py_XDECREF(res); + +cleanup: + + Py_DECREF(set_result); + Py_DECREF(result); + + return future; +} + + +PyObject * +nxt_py_asgi_new_msg(nxt_unit_request_info_t *req, PyObject *type) +{ + PyObject *msg; + + msg = PyDict_New(); + if (nxt_slow_path(msg == NULL)) { + nxt_unit_req_alert(req, "Python failed to create message dict"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create message dict"); + } + + if (nxt_slow_path(PyDict_SetItem(msg, nxt_py_type_str, type) == -1)) { + nxt_unit_req_alert(req, "Python failed to set 'msg.type' item"); + + Py_DECREF(msg); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'msg.type' item"); + } + + return msg; +} + + +PyObject * +nxt_py_asgi_new_scope(nxt_unit_request_info_t *req, PyObject *type, + PyObject *spec_version) +{ + PyObject *scope, *asgi; + + scope = PyDict_New(); + if (nxt_slow_path(scope == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'scope' dict"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create 'scope' dict"); + } + + if (nxt_slow_path(PyDict_SetItem(scope, nxt_py_type_str, type) == -1)) { + nxt_unit_req_alert(req, "Python failed to set 'scope.type' item"); + + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'scope.type' item"); + } + + asgi = PyDict_New(); + if (nxt_slow_path(asgi == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'asgi' dict"); + nxt_python_print_exception(); + + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create 'asgi' dict"); + } + + if (nxt_slow_path(PyDict_SetItem(scope, nxt_py_asgi_str, asgi) == -1)) { + nxt_unit_req_alert(req, "Python failed to set 'scope.asgi' item"); + + Py_DECREF(asgi); + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'scope.asgi' item"); + } + + if (nxt_slow_path(PyDict_SetItem(asgi, nxt_py_version_str, + nxt_py_3_0_str) == -1)) + { + nxt_unit_req_alert(req, "Python failed to set 'asgi.version' item"); + + Py_DECREF(asgi); + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'asgi.version' item"); + } + + if (nxt_slow_path(PyDict_SetItem(asgi, nxt_py_spec_version_str, + spec_version) == -1)) + { + nxt_unit_req_alert(req, + "Python failed to set 'asgi.spec_version' item"); + + Py_DECREF(asgi); + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'asgi.spec_version' item"); + } + + Py_DECREF(asgi); + + return scope; +} + + +void +nxt_py_asgi_dealloc(PyObject *self) +{ + PyObject_Del(self); +} + + +PyObject * +nxt_py_asgi_await(PyObject *self) +{ + Py_INCREF(self); + return self; +} + + +PyObject * +nxt_py_asgi_iter(PyObject *self) +{ + Py_INCREF(self); + return self; +} + + +PyObject * +nxt_py_asgi_next(PyObject *self) +{ + return NULL; +} + + +void +nxt_python_asgi_done(void) +{ + nxt_py_asgi_str_done(); + + Py_XDECREF(nxt_py_quit_future); + Py_XDECREF(nxt_py_quit_future_set_result); + Py_XDECREF(nxt_py_loop_run_until_complete); + Py_XDECREF(nxt_py_loop_create_future); + Py_XDECREF(nxt_py_loop_create_task); + Py_XDECREF(nxt_py_loop_call_soon); + Py_XDECREF(nxt_py_loop_add_reader); + Py_XDECREF(nxt_py_loop_remove_reader); + Py_XDECREF(nxt_py_port_read); +} + +#else /* !(NXT_HAVE_ASGI) */ + + +int +nxt_python_asgi_check(PyObject *obj) +{ + return 0; +} + + +nxt_int_t +nxt_python_asgi_init(nxt_task_t *task, nxt_unit_init_t *init) +{ + nxt_alert(task, "ASGI not implemented"); + return NXT_ERROR; +} + + +nxt_int_t +nxt_python_asgi_run(nxt_unit_ctx_t *ctx) +{ + nxt_unit_alert(ctx, "ASGI not implemented"); + return NXT_ERROR; +} + + +void +nxt_python_asgi_done(void) +{ +} + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi.h b/src/python/nxt_python_asgi.h new file mode 100644 index 00000000..24337c37 --- /dev/null +++ b/src/python/nxt_python_asgi.h @@ -0,0 +1,60 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_PYTHON_ASGI_H_INCLUDED_ +#define _NXT_PYTHON_ASGI_H_INCLUDED_ + + +typedef PyObject * (*nxt_py_asgi_enum_header_cb)(void *ctx, int i, + PyObject *name, PyObject *val); + +typedef struct { + uint32_t fields_count; + uint32_t fields_size; +} nxt_py_asgi_calc_size_ctx_t; + +typedef struct { + nxt_unit_request_info_t *req; + uint64_t content_length; +} nxt_py_asgi_add_field_ctx_t; + +PyObject *nxt_py_asgi_enum_headers(PyObject *headers, + nxt_py_asgi_enum_header_cb cb, void *data); + +PyObject *nxt_py_asgi_calc_size(void *data, int i, PyObject *n, PyObject *v); +PyObject *nxt_py_asgi_add_field(void *data, int i, PyObject *n, PyObject *v); + +PyObject *nxt_py_asgi_set_result_soon(nxt_unit_request_info_t *req, + PyObject *future, PyObject *result); +PyObject *nxt_py_asgi_new_msg(nxt_unit_request_info_t *req, PyObject *type); +PyObject *nxt_py_asgi_new_scope(nxt_unit_request_info_t *req, PyObject *type, + PyObject *spec_version); + +void nxt_py_asgi_dealloc(PyObject *self); +PyObject *nxt_py_asgi_await(PyObject *self); +PyObject *nxt_py_asgi_iter(PyObject *self); +PyObject *nxt_py_asgi_next(PyObject *self); + +nxt_int_t nxt_py_asgi_http_init(nxt_task_t *task); +PyObject *nxt_py_asgi_http_create(nxt_unit_request_info_t *req); +void nxt_py_asgi_http_data_handler(nxt_unit_request_info_t *req); +int nxt_py_asgi_http_drain(nxt_queue_link_t *lnk); + +nxt_int_t nxt_py_asgi_websocket_init(nxt_task_t *task); +PyObject *nxt_py_asgi_websocket_create(nxt_unit_request_info_t *req); +void nxt_py_asgi_websocket_handler(nxt_unit_websocket_frame_t *ws); +void nxt_py_asgi_websocket_close_handler(nxt_unit_request_info_t *req); + +nxt_int_t nxt_py_asgi_lifespan_startup(nxt_task_t *task); +nxt_int_t nxt_py_asgi_lifespan_shutdown(void); + +extern PyObject *nxt_py_loop_run_until_complete; +extern PyObject *nxt_py_loop_create_future; +extern PyObject *nxt_py_loop_create_task; + +extern nxt_queue_t nxt_py_asgi_drain_queue; + + +#endif /* _NXT_PYTHON_ASGI_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi_http.c b/src/python/nxt_python_asgi_http.c new file mode 100644 index 00000000..b07d61d6 --- /dev/null +++ b/src/python/nxt_python_asgi_http.c @@ -0,0 +1,591 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include + +#if (NXT_HAVE_ASGI) + +#include +#include +#include +#include +#include + + +typedef struct { + PyObject_HEAD + nxt_unit_request_info_t *req; + nxt_queue_link_t link; + PyObject *receive_future; + PyObject *send_future; + uint64_t content_length; + uint64_t bytes_sent; + int complete; + PyObject *send_body; + Py_ssize_t send_body_off; +} nxt_py_asgi_http_t; + + +static PyObject *nxt_py_asgi_http_receive(PyObject *self, PyObject *none); +static PyObject *nxt_py_asgi_http_read_msg(nxt_py_asgi_http_t *http); +static PyObject *nxt_py_asgi_http_send(PyObject *self, PyObject *dict); +static PyObject *nxt_py_asgi_http_response_start(nxt_py_asgi_http_t *http, + PyObject *dict); +static PyObject *nxt_py_asgi_http_response_body(nxt_py_asgi_http_t *http, + PyObject *dict); +static PyObject *nxt_py_asgi_http_done(PyObject *self, PyObject *future); + + +static PyMethodDef nxt_py_asgi_http_methods[] = { + { "receive", nxt_py_asgi_http_receive, METH_NOARGS, 0 }, + { "send", nxt_py_asgi_http_send, METH_O, 0 }, + { "_done", nxt_py_asgi_http_done, METH_O, 0 }, + { NULL, NULL, 0, 0 } +}; + +static PyAsyncMethods nxt_py_asgi_async_methods = { + .am_await = nxt_py_asgi_await, +}; + +static PyTypeObject nxt_py_asgi_http_type = { + PyVarObject_HEAD_INIT(NULL, 0) + + .tp_name = "unit._asgi_http", + .tp_basicsize = sizeof(nxt_py_asgi_http_t), + .tp_dealloc = nxt_py_asgi_dealloc, + .tp_as_async = &nxt_py_asgi_async_methods, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "unit ASGI HTTP request object", + .tp_iter = nxt_py_asgi_iter, + .tp_iternext = nxt_py_asgi_next, + .tp_methods = nxt_py_asgi_http_methods, +}; + +static Py_ssize_t nxt_py_asgi_http_body_buf_size = 32 * 1024 * 1024; + + +nxt_int_t +nxt_py_asgi_http_init(nxt_task_t *task) +{ + if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_http_type) != 0)) { + nxt_alert(task, "Python failed to initialize the 'http' type object"); + return NXT_ERROR; + } + + return NXT_OK; +} + + +PyObject * +nxt_py_asgi_http_create(nxt_unit_request_info_t *req) +{ + nxt_py_asgi_http_t *http; + + http = PyObject_New(nxt_py_asgi_http_t, &nxt_py_asgi_http_type); + + if (nxt_fast_path(http != NULL)) { + http->req = req; + http->receive_future = NULL; + http->send_future = NULL; + http->content_length = -1; + http->bytes_sent = 0; + http->complete = 0; + http->send_body = NULL; + http->send_body_off = 0; + } + + return (PyObject *) http; +} + + +static PyObject * +nxt_py_asgi_http_receive(PyObject *self, PyObject *none) +{ + PyObject *msg, *future; + nxt_py_asgi_http_t *http; + nxt_unit_request_info_t *req; + + http = (nxt_py_asgi_http_t *) self; + req = http->req; + + nxt_unit_req_debug(req, "asgi_http_receive"); + + msg = nxt_py_asgi_http_read_msg(http); + if (nxt_slow_path(msg == NULL)) { + return NULL; + } + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_req_alert(req, "Python failed to create Future object"); + nxt_python_print_exception(); + + Py_DECREF(msg); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + if (msg != Py_None) { + return nxt_py_asgi_set_result_soon(req, future, msg); + } + + http->receive_future = future; + Py_INCREF(http->receive_future); + + Py_DECREF(msg); + + return future; +} + + +static PyObject * +nxt_py_asgi_http_read_msg(nxt_py_asgi_http_t *http) +{ + char *body_buf; + ssize_t read_res; + PyObject *msg, *body; + Py_ssize_t size; + nxt_unit_request_info_t *req; + + req = http->req; + + size = req->content_length; + + if (size > nxt_py_asgi_http_body_buf_size) { + size = nxt_py_asgi_http_body_buf_size; + } + + if (size > 0) { + body = PyBytes_FromStringAndSize(NULL, size); + if (nxt_slow_path(body == NULL)) { + nxt_unit_req_alert(req, "Python failed to create body byte string"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Bytes object"); + } + + body_buf = PyBytes_AS_STRING(body); + + read_res = nxt_unit_request_read(req, body_buf, size); + + } else { + body = NULL; + read_res = 0; + } + + if (read_res > 0 || read_res == size) { + msg = nxt_py_asgi_new_msg(req, nxt_py_http_request_str); + if (nxt_slow_path(msg == NULL)) { + Py_XDECREF(body); + + return NULL; + } + +#define SET_ITEM(dict, key, value) \ + if (nxt_slow_path(PyDict_SetItem(dict, nxt_py_ ## key ## _str, value) \ + == -1)) \ + { \ + nxt_unit_req_alert(req, \ + "Python failed to set '" #dict "." #key "' item"); \ + PyErr_SetString(PyExc_RuntimeError, \ + "Python failed to set '" #dict "." #key "' item"); \ + goto fail; \ + } + + if (body != NULL) { + SET_ITEM(msg, body, body) + } + + if (req->content_length > 0) { + SET_ITEM(msg, more_body, Py_True) + } + +#undef SET_ITEM + + Py_XDECREF(body); + + return msg; + } + + Py_XDECREF(body); + + Py_RETURN_NONE; + +fail: + + Py_DECREF(msg); + Py_XDECREF(body); + + return NULL; +} + + +static PyObject * +nxt_py_asgi_http_send(PyObject *self, PyObject *dict) +{ + PyObject *type; + const char *type_str; + Py_ssize_t type_len; + nxt_py_asgi_http_t *http; + + static const nxt_str_t response_start = nxt_string("http.response.start"); + static const nxt_str_t response_body = nxt_string("http.response.body"); + + http = (nxt_py_asgi_http_t *) self; + + type = PyDict_GetItem(dict, nxt_py_type_str); + if (nxt_slow_path(type == NULL || !PyUnicode_Check(type))) { + nxt_unit_req_error(http->req, "asgi_http_send: " + "'type' is not a unicode string"); + return PyErr_Format(PyExc_TypeError, "'type' is not a unicode string"); + } + + type_str = PyUnicode_AsUTF8AndSize(type, &type_len); + + nxt_unit_req_debug(http->req, "asgi_http_send type is '%.*s'", + (int) type_len, type_str); + + if (type_len == (Py_ssize_t) response_start.length + && memcmp(type_str, response_start.start, type_len) == 0) + { + return nxt_py_asgi_http_response_start(http, dict); + } + + if (type_len == (Py_ssize_t) response_body.length + && memcmp(type_str, response_body.start, type_len) == 0) + { + return nxt_py_asgi_http_response_body(http, dict); + } + + nxt_unit_req_error(http->req, "asgi_http_send: unexpected 'type': '%.*s'", + (int) type_len, type_str); + + return PyErr_Format(PyExc_AssertionError, "unexpected 'type': '%U'", type); +} + + +static PyObject * +nxt_py_asgi_http_response_start(nxt_py_asgi_http_t *http, PyObject *dict) +{ + int rc; + PyObject *status, *headers, *res; + nxt_py_asgi_calc_size_ctx_t calc_size_ctx; + nxt_py_asgi_add_field_ctx_t add_field_ctx; + + status = PyDict_GetItem(dict, nxt_py_status_str); + if (nxt_slow_path(status == NULL || !PyLong_Check(status))) { + nxt_unit_req_error(http->req, "asgi_http_response_start: " + "'status' is not an integer"); + return PyErr_Format(PyExc_TypeError, "'status' is not an integer"); + } + + calc_size_ctx.fields_size = 0; + calc_size_ctx.fields_count = 0; + + headers = PyDict_GetItem(dict, nxt_py_headers_str); + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_calc_size, + &calc_size_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + Py_DECREF(res); + } + + rc = nxt_unit_response_init(http->req, PyLong_AsLong(status), + calc_size_ctx.fields_count, + calc_size_ctx.fields_size); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to allocate response object"); + } + + add_field_ctx.req = http->req; + add_field_ctx.content_length = -1; + + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_add_field, + &add_field_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + Py_DECREF(res); + } + + http->content_length = add_field_ctx.content_length; + + Py_INCREF(http); + return (PyObject *) http; +} + + +static PyObject * +nxt_py_asgi_http_response_body(nxt_py_asgi_http_t *http, PyObject *dict) +{ + int rc; + char *body_str; + ssize_t sent; + PyObject *body, *more_body, *future; + Py_ssize_t body_len, body_off; + + body = PyDict_GetItem(dict, nxt_py_body_str); + if (nxt_slow_path(body != NULL && !PyBytes_Check(body))) { + return PyErr_Format(PyExc_TypeError, "'body' is not a byte string"); + } + + more_body = PyDict_GetItem(dict, nxt_py_more_body_str); + if (nxt_slow_path(more_body != NULL && !PyBool_Check(more_body))) { + return PyErr_Format(PyExc_TypeError, "'more_body' is not a bool"); + } + + if (nxt_slow_path(http->complete)) { + return PyErr_Format(PyExc_RuntimeError, + "Unexpected ASGI message 'http.response.body' " + "sent, after response already completed"); + } + + if (nxt_slow_path(http->send_future != NULL)) { + return PyErr_Format(PyExc_RuntimeError, "Concurrent send"); + } + + if (body != NULL) { + body_str = PyBytes_AS_STRING(body); + body_len = PyBytes_GET_SIZE(body); + + nxt_unit_req_debug(http->req, "asgi_http_response_body: %d, %d", + (int) body_len, (more_body == Py_True) ); + + if (nxt_slow_path(http->bytes_sent + body_len + > http->content_length)) + { + return PyErr_Format(PyExc_RuntimeError, + "Response content longer than Content-Length"); + } + + body_off = 0; + + while (body_len > 0) { + sent = nxt_unit_response_write_nb(http->req, body_str, body_len, 0); + if (nxt_slow_path(sent < 0)) { + return PyErr_Format(PyExc_RuntimeError, "failed to send body"); + } + + if (nxt_slow_path(sent == 0)) { + nxt_unit_req_debug(http->req, "asgi_http_response_body: " + "out of shared memory, %d", + (int) body_len); + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_req_alert(http->req, + "Python failed to create Future object"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + http->send_body = body; + Py_INCREF(http->send_body); + http->send_body_off = body_off; + + nxt_queue_insert_tail(&nxt_py_asgi_drain_queue, &http->link); + + http->send_future = future; + Py_INCREF(http->send_future); + + return future; + } + + body_str += sent; + body_len -= sent; + body_off += sent; + http->bytes_sent += sent; + } + + } else { + nxt_unit_req_debug(http->req, "asgi_http_response_body: 0, %d", + (more_body == Py_True) ); + + if (!nxt_unit_response_is_sent(http->req)) { + rc = nxt_unit_response_send(http->req); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to send response"); + } + } + } + + if (more_body == NULL || more_body == Py_False) { + http->complete = 1; + } + + Py_INCREF(http); + return (PyObject *) http; +} + + +void +nxt_py_asgi_http_data_handler(nxt_unit_request_info_t *req) +{ + PyObject *msg, *future, *res; + nxt_py_asgi_http_t *http; + + http = req->data; + + nxt_unit_req_debug(req, "asgi_http_data_handler"); + + if (http->receive_future == NULL) { + return; + } + + msg = nxt_py_asgi_http_read_msg(http); + if (nxt_slow_path(msg == NULL)) { + return; + } + + if (msg == Py_None) { + Py_DECREF(msg); + return; + } + + future = http->receive_future; + http->receive_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, msg, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(req, "'set_result' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + Py_DECREF(msg); +} + + +int +nxt_py_asgi_http_drain(nxt_queue_link_t *lnk) +{ + char *body_str; + ssize_t sent; + PyObject *future, *exc, *res; + Py_ssize_t body_len; + nxt_py_asgi_http_t *http; + + http = nxt_container_of(lnk, nxt_py_asgi_http_t, link); + + body_str = PyBytes_AS_STRING(http->send_body) + http->send_body_off; + body_len = PyBytes_GET_SIZE(http->send_body) - http->send_body_off; + + nxt_unit_req_debug(http->req, "asgi_http_drain: %d", (int) body_len); + + while (body_len > 0) { + sent = nxt_unit_response_write_nb(http->req, body_str, body_len, 0); + if (nxt_slow_path(sent < 0)) { + goto fail; + } + + if (nxt_slow_path(sent == 0)) { + return NXT_UNIT_AGAIN; + } + + body_str += sent; + body_len -= sent; + + http->send_body_off += sent; + http->bytes_sent += sent; + } + + Py_CLEAR(http->send_body); + + future = http->send_future; + http->send_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, Py_None, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(http->req, "'set_result' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + return NXT_UNIT_OK; + +fail: + + exc = PyObject_CallFunctionObjArgs(PyExc_RuntimeError, + nxt_py_failed_to_send_body_str, + NULL); + if (nxt_slow_path(exc == NULL)) { + nxt_unit_req_alert(http->req, "RuntimeError create failed"); + nxt_python_print_exception(); + + exc = Py_None; + Py_INCREF(exc); + } + + future = http->send_future; + http->send_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_exception_str, exc, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(http->req, "'set_exception' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + Py_DECREF(exc); + + return NXT_UNIT_ERROR; +} + + +static PyObject * +nxt_py_asgi_http_done(PyObject *self, PyObject *future) +{ + int rc; + PyObject *res; + nxt_py_asgi_http_t *http; + + http = (nxt_py_asgi_http_t *) self; + + nxt_unit_req_debug(http->req, "asgi_http_done"); + + /* + * Get Future.result() and it raises an exception, if coroutine exited + * with exception. + */ + res = PyObject_CallMethodObjArgs(future, nxt_py_result_str, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(http->req, + "Python failed to call 'future.result()'"); + nxt_python_print_exception(); + + rc = NXT_UNIT_ERROR; + + } else { + Py_DECREF(res); + + rc = NXT_UNIT_OK; + } + + nxt_unit_request_done(http->req, rc); + + Py_RETURN_NONE; +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi_lifespan.c b/src/python/nxt_python_asgi_lifespan.c new file mode 100644 index 00000000..14d0ee97 --- /dev/null +++ b/src/python/nxt_python_asgi_lifespan.c @@ -0,0 +1,505 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include + +#if (NXT_HAVE_ASGI) + +#include +#include +#include + + +typedef struct { + PyObject_HEAD + int disabled; + int startup_received; + int startup_sent; + int shutdown_received; + int shutdown_sent; + int shutdown_called; + PyObject *startup_future; + PyObject *shutdown_future; + PyObject *receive_future; +} nxt_py_asgi_lifespan_t; + + +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( + nxt_py_asgi_lifespan_t *lifespan, int v, PyObject *dict); +static PyObject *nxt_py_asgi_lifespan_send_(nxt_py_asgi_lifespan_t *lifespan, + int v, int *sent, PyObject **future); +static PyObject *nxt_py_asgi_lifespan_send_shutdown( + nxt_py_asgi_lifespan_t *lifespan, int v, PyObject *dict); +static PyObject *nxt_py_asgi_lifespan_disable(nxt_py_asgi_lifespan_t *lifespan); +static PyObject *nxt_py_asgi_lifespan_done(PyObject *self, PyObject *future); + + +static nxt_py_asgi_lifespan_t *nxt_py_lifespan; + +static PyMethodDef nxt_py_asgi_lifespan_methods[] = { + { "receive", nxt_py_asgi_lifespan_receive, METH_NOARGS, 0 }, + { "send", nxt_py_asgi_lifespan_send, METH_O, 0 }, + { "_done", nxt_py_asgi_lifespan_done, METH_O, 0 }, + { NULL, NULL, 0, 0 } +}; + +static PyAsyncMethods nxt_py_asgi_async_methods = { + .am_await = nxt_py_asgi_await, +}; + +static PyTypeObject nxt_py_asgi_lifespan_type = { + PyVarObject_HEAD_INIT(NULL, 0) + + .tp_name = "unit._asgi_lifespan", + .tp_basicsize = sizeof(nxt_py_asgi_lifespan_t), + .tp_dealloc = nxt_py_asgi_dealloc, + .tp_as_async = &nxt_py_asgi_async_methods, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "unit ASGI Lifespan object", + .tp_iter = nxt_py_asgi_iter, + .tp_iternext = nxt_py_asgi_next, + .tp_methods = nxt_py_asgi_lifespan_methods, +}; + + +nxt_int_t +nxt_py_asgi_lifespan_startup(nxt_task_t *task) +{ + PyObject *scope, *res, *py_task, *receive, *send, *done; + nxt_int_t rc; + nxt_py_asgi_lifespan_t *lifespan; + + if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_lifespan_type) != 0)) { + nxt_alert(task, + "Python failed to initialize the 'asgi_lifespan' type object"); + return NXT_ERROR; + } + + lifespan = PyObject_New(nxt_py_asgi_lifespan_t, &nxt_py_asgi_lifespan_type); + if (nxt_slow_path(lifespan == NULL)) { + nxt_alert(task, "Python failed to create lifespan object"); + return NXT_ERROR; + } + + rc = NXT_ERROR; + + receive = PyObject_GetAttrString((PyObject *) lifespan, "receive"); + if (nxt_slow_path(receive == NULL)) { + nxt_alert(task, "Python failed to get 'receive' method"); + goto release_lifespan; + } + + send = PyObject_GetAttrString((PyObject *) lifespan, "send"); + if (nxt_slow_path(receive == NULL)) { + nxt_alert(task, "Python failed to get 'send' method"); + goto release_receive; + } + + done = PyObject_GetAttrString((PyObject *) lifespan, "_done"); + if (nxt_slow_path(receive == NULL)) { + nxt_alert(task, "Python failed to get '_done' method"); + goto release_send; + } + + lifespan->startup_future = PyObject_CallObject(nxt_py_loop_create_future, + NULL); + if (nxt_slow_path(lifespan->startup_future == NULL)) { + nxt_unit_alert(NULL, "Python failed to create Future object"); + nxt_python_print_exception(); + + goto release_done; + } + + lifespan->disabled = 0; + lifespan->startup_received = 0; + lifespan->startup_sent = 0; + lifespan->shutdown_received = 0; + lifespan->shutdown_sent = 0; + lifespan->shutdown_called = 0; + lifespan->shutdown_future = NULL; + lifespan->receive_future = NULL; + + scope = nxt_py_asgi_new_scope(NULL, nxt_py_lifespan_str, nxt_py_2_0_str); + if (nxt_slow_path(scope == NULL)) { + goto release_future; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_application, + scope, receive, send, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_log(task, NXT_LOG_ERR, "Python failed to call the application"); + nxt_python_print_exception(); + goto release_scope; + } + + if (nxt_slow_path(!PyCoro_CheckExact(res))) { + nxt_log(task, NXT_LOG_ERR, + "Application result type is not a coroutine"); + Py_DECREF(res); + goto release_scope; + } + + py_task = PyObject_CallFunctionObjArgs(nxt_py_loop_create_task, res, NULL); + if (nxt_slow_path(py_task == NULL)) { + nxt_log(task, NXT_LOG_ERR, "Python failed to call the create_task"); + nxt_python_print_exception(); + Py_DECREF(res); + goto release_scope; + } + + Py_DECREF(res); + + res = PyObject_CallMethodObjArgs(py_task, nxt_py_add_done_callback_str, + done, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_log(task, NXT_LOG_ERR, + "Python failed to call 'task.add_done_callback'"); + nxt_python_print_exception(); + goto release_task; + } + + Py_DECREF(res); + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_run_until_complete, + lifespan->startup_future, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_alert(task, "Python failed to call loop.run_until_complete"); + nxt_python_print_exception(); + goto release_task; + } + + Py_DECREF(res); + + if (lifespan->startup_sent == 1 || lifespan->disabled) { + nxt_py_lifespan = lifespan; + Py_INCREF(nxt_py_lifespan); + + rc = NXT_OK; + } + +release_task: + Py_DECREF(py_task); +release_scope: + Py_DECREF(scope); +release_future: + Py_CLEAR(lifespan->startup_future); +release_done: + Py_DECREF(done); +release_send: + Py_DECREF(send); +release_receive: + Py_DECREF(receive); +release_lifespan: + Py_DECREF(lifespan); + + return rc; +} + + +nxt_int_t +nxt_py_asgi_lifespan_shutdown(void) +{ + PyObject *msg, *future, *res; + nxt_py_asgi_lifespan_t *lifespan; + + if (nxt_slow_path(nxt_py_lifespan == NULL || nxt_py_lifespan->disabled)) { + return NXT_OK; + } + + lifespan = nxt_py_lifespan; + lifespan->shutdown_called = 1; + + if (lifespan->receive_future != NULL) { + future = lifespan->receive_future; + lifespan->receive_future = NULL; + + msg = nxt_py_asgi_new_msg(NULL, nxt_py_lifespan_shutdown_str); + + if (nxt_fast_path(msg != NULL)) { + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + msg, NULL); + Py_XDECREF(res); + Py_DECREF(msg); + } + + Py_DECREF(future); + } + + if (lifespan->shutdown_sent) { + return NXT_OK; + } + + lifespan->shutdown_future = PyObject_CallObject(nxt_py_loop_create_future, + NULL); + if (nxt_slow_path(lifespan->shutdown_future == NULL)) { + nxt_unit_alert(NULL, "Python failed to create Future object"); + nxt_python_print_exception(); + return NXT_ERROR; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_run_until_complete, + lifespan->shutdown_future, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Python failed to call loop.run_until_complete"); + nxt_python_print_exception(); + return NXT_ERROR; + } + + Py_DECREF(res); + Py_CLEAR(lifespan->shutdown_future); + + return NXT_OK; +} + + +static PyObject * +nxt_py_asgi_lifespan_receive(PyObject *self, PyObject *none) +{ + PyObject *msg, *future; + nxt_py_asgi_lifespan_t *lifespan; + + lifespan = (nxt_py_asgi_lifespan_t *) self; + + nxt_unit_debug(NULL, "asgi_lifespan_receive"); + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_alert(NULL, "Python failed to create Future object"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + if (!lifespan->startup_received) { + lifespan->startup_received = 1; + + msg = nxt_py_asgi_new_msg(NULL, nxt_py_lifespan_startup_str); + + return nxt_py_asgi_set_result_soon(NULL, future, msg); + } + + if (lifespan->shutdown_called && !lifespan->shutdown_received) { + lifespan->shutdown_received = 1; + + msg = nxt_py_asgi_new_msg(NULL, nxt_py_lifespan_shutdown_str); + + return nxt_py_asgi_set_result_soon(NULL, future, msg); + } + + Py_INCREF(future); + lifespan->receive_future = future; + + return future; +} + + +static PyObject * +nxt_py_asgi_lifespan_send(PyObject *self, PyObject *dict) +{ + PyObject *type, *msg; + const char *type_str; + Py_ssize_t type_len; + nxt_py_asgi_lifespan_t *lifespan; + + static const nxt_str_t startup_complete + = nxt_string("lifespan.startup.complete"); + static const nxt_str_t startup_failed + = nxt_string("lifespan.startup.failed"); + static const nxt_str_t shutdown_complete + = nxt_string("lifespan.shutdown.complete"); + static const nxt_str_t shutdown_failed + = nxt_string("lifespan.shutdown.failed"); + + lifespan = (nxt_py_asgi_lifespan_t *) self; + + type = PyDict_GetItem(dict, nxt_py_type_str); + if (nxt_slow_path(type == NULL || !PyUnicode_Check(type))) { + nxt_unit_error(NULL, + "asgi_lifespan_send: 'type' is not a unicode string"); + return PyErr_Format(PyExc_TypeError, + "'type' is not a unicode string"); + } + + type_str = PyUnicode_AsUTF8AndSize(type, &type_len); + + nxt_unit_debug(NULL, "asgi_lifespan_send type is '%.*s'", + (int) type_len, type_str); + + if (type_len == (Py_ssize_t) startup_complete.length + && memcmp(type_str, startup_complete.start, type_len) == 0) + { + return nxt_py_asgi_lifespan_send_startup(lifespan, 0, NULL); + } + + if (type_len == (Py_ssize_t) startup_failed.length + && memcmp(type_str, startup_failed.start, type_len) == 0) + { + msg = PyDict_GetItem(dict, nxt_py_message_str); + return nxt_py_asgi_lifespan_send_startup(lifespan, 1, msg); + } + + if (type_len == (Py_ssize_t) shutdown_complete.length + && memcmp(type_str, shutdown_complete.start, type_len) == 0) + { + return nxt_py_asgi_lifespan_send_shutdown(lifespan, 0, NULL); + } + + if (type_len == (Py_ssize_t) shutdown_failed.length + && memcmp(type_str, shutdown_failed.start, type_len) == 0) + { + msg = PyDict_GetItem(dict, nxt_py_message_str); + return nxt_py_asgi_lifespan_send_shutdown(lifespan, 1, msg); + } + + return nxt_py_asgi_lifespan_disable(lifespan); +} + + +static PyObject * +nxt_py_asgi_lifespan_send_startup(nxt_py_asgi_lifespan_t *lifespan, int v, + PyObject *message) +{ + const char *message_str; + Py_ssize_t message_len; + + if (nxt_slow_path(v != 0)) { + nxt_unit_error(NULL, "Application startup failed"); + + if (nxt_fast_path(message != NULL && PyUnicode_Check(message))) { + message_str = PyUnicode_AsUTF8AndSize(message, &message_len); + + nxt_unit_error(NULL, "%.*s", (int) message_len, message_str); + } + } + + return nxt_py_asgi_lifespan_send_(lifespan, v, + &lifespan->startup_sent, + &lifespan->startup_future); +} + + +static PyObject * +nxt_py_asgi_lifespan_send_(nxt_py_asgi_lifespan_t *lifespan, int v, int *sent, + PyObject **pfuture) +{ + PyObject *future, *res; + + if (*sent) { + return nxt_py_asgi_lifespan_disable(lifespan); + } + + *sent = 1 + v; + + if (*pfuture != NULL) { + future = *pfuture; + *pfuture = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + Py_None, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Failed to call 'future.set_result'"); + nxt_python_print_exception(); + + return nxt_py_asgi_lifespan_disable(lifespan); + } + + Py_DECREF(res); + Py_DECREF(future); + } + + Py_INCREF(lifespan); + + return (PyObject *) lifespan; +} + + +static PyObject * +nxt_py_asgi_lifespan_disable(nxt_py_asgi_lifespan_t *lifespan) +{ + nxt_unit_warn(NULL, "Got invalid state transition on lifespan protocol"); + + lifespan->disabled = 1; + + return PyErr_Format(PyExc_AssertionError, + "Got invalid state transition on lifespan protocol"); +} + + +static PyObject * +nxt_py_asgi_lifespan_send_shutdown(nxt_py_asgi_lifespan_t *lifespan, int v, + PyObject *message) +{ + return nxt_py_asgi_lifespan_send_(lifespan, v, + &lifespan->shutdown_sent, + &lifespan->shutdown_future); +} + + +static PyObject * +nxt_py_asgi_lifespan_done(PyObject *self, PyObject *future) +{ + PyObject *res; + nxt_py_asgi_lifespan_t *lifespan; + + nxt_unit_debug(NULL, "asgi_lifespan_done"); + + lifespan = (nxt_py_asgi_lifespan_t *) self; + + if (lifespan->startup_sent == 0) { + lifespan->disabled = 1; + } + + /* + * Get Future.result() and it raises an exception, if coroutine exited + * with exception. + */ + res = PyObject_CallMethodObjArgs(future, nxt_py_result_str, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_log(NULL, NXT_UNIT_LOG_INFO, + "ASGI Lifespan processing exception"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + + if (lifespan->startup_future != NULL) { + future = lifespan->startup_future; + lifespan->startup_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + Py_None, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Failed to call 'future.set_result'"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + } + + if (lifespan->shutdown_future != NULL) { + future = lifespan->shutdown_future; + lifespan->shutdown_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + Py_None, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Failed to call 'future.set_result'"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + } + + Py_RETURN_NONE; +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi_str.c b/src/python/nxt_python_asgi_str.c new file mode 100644 index 00000000..37fa7f04 --- /dev/null +++ b/src/python/nxt_python_asgi_str.c @@ -0,0 +1,141 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include + +#if (NXT_HAVE_ASGI) + +#include +#include + + +PyObject *nxt_py_1_0_str; +PyObject *nxt_py_1_1_str; +PyObject *nxt_py_2_0_str; +PyObject *nxt_py_2_1_str; +PyObject *nxt_py_3_0_str; +PyObject *nxt_py_add_done_callback_str; +PyObject *nxt_py_asgi_str; +PyObject *nxt_py_bad_state_str; +PyObject *nxt_py_body_str; +PyObject *nxt_py_bytes_str; +PyObject *nxt_py_client_str; +PyObject *nxt_py_code_str; +PyObject *nxt_py_done_str; +PyObject *nxt_py_exception_str; +PyObject *nxt_py_failed_to_send_body_str; +PyObject *nxt_py_headers_str; +PyObject *nxt_py_http_str; +PyObject *nxt_py_http_disconnect_str; +PyObject *nxt_py_http_request_str; +PyObject *nxt_py_http_version_str; +PyObject *nxt_py_https_str; +PyObject *nxt_py_lifespan_str; +PyObject *nxt_py_lifespan_shutdown_str; +PyObject *nxt_py_lifespan_startup_str; +PyObject *nxt_py_method_str; +PyObject *nxt_py_message_str; +PyObject *nxt_py_message_too_big_str; +PyObject *nxt_py_more_body_str; +PyObject *nxt_py_path_str; +PyObject *nxt_py_query_string_str; +PyObject *nxt_py_raw_path_str; +PyObject *nxt_py_result_str; +PyObject *nxt_py_root_path_str; +PyObject *nxt_py_scheme_str; +PyObject *nxt_py_server_str; +PyObject *nxt_py_set_exception_str; +PyObject *nxt_py_set_result_str; +PyObject *nxt_py_spec_version_str; +PyObject *nxt_py_status_str; +PyObject *nxt_py_subprotocol_str; +PyObject *nxt_py_subprotocols_str; +PyObject *nxt_py_text_str; +PyObject *nxt_py_type_str; +PyObject *nxt_py_version_str; +PyObject *nxt_py_websocket_str; +PyObject *nxt_py_websocket_accept_str; +PyObject *nxt_py_websocket_close_str; +PyObject *nxt_py_websocket_connect_str; +PyObject *nxt_py_websocket_disconnect_str; +PyObject *nxt_py_websocket_receive_str; +PyObject *nxt_py_websocket_send_str; +PyObject *nxt_py_ws_str; +PyObject *nxt_py_wss_str; + +static nxt_python_string_t nxt_py_asgi_strings[] = { + { nxt_string("1.0"), &nxt_py_1_0_str }, + { nxt_string("1.1"), &nxt_py_1_1_str }, + { nxt_string("2.0"), &nxt_py_2_0_str }, + { nxt_string("2.1"), &nxt_py_2_1_str }, + { nxt_string("3.0"), &nxt_py_3_0_str }, + { nxt_string("add_done_callback"), &nxt_py_add_done_callback_str }, + { nxt_string("asgi"), &nxt_py_asgi_str }, + { nxt_string("bad state"), &nxt_py_bad_state_str }, + { nxt_string("body"), &nxt_py_body_str }, + { nxt_string("bytes"), &nxt_py_bytes_str }, + { nxt_string("client"), &nxt_py_client_str }, + { nxt_string("code"), &nxt_py_code_str }, + { nxt_string("done"), &nxt_py_done_str }, + { nxt_string("exception"), &nxt_py_exception_str }, + { nxt_string("failed to send body"), &nxt_py_failed_to_send_body_str }, + { nxt_string("headers"), &nxt_py_headers_str }, + { nxt_string("http"), &nxt_py_http_str }, + { nxt_string("http.disconnect"), &nxt_py_http_disconnect_str }, + { nxt_string("http.request"), &nxt_py_http_request_str }, + { nxt_string("http_version"), &nxt_py_http_version_str }, + { nxt_string("https"), &nxt_py_https_str }, + { nxt_string("lifespan"), &nxt_py_lifespan_str }, + { nxt_string("lifespan.shutdown"), &nxt_py_lifespan_shutdown_str }, + { nxt_string("lifespan.startup"), &nxt_py_lifespan_startup_str }, + { nxt_string("message"), &nxt_py_message_str }, + { nxt_string("message too big"), &nxt_py_message_too_big_str }, + { nxt_string("method"), &nxt_py_method_str }, + { nxt_string("more_body"), &nxt_py_more_body_str }, + { nxt_string("path"), &nxt_py_path_str }, + { nxt_string("query_string"), &nxt_py_query_string_str }, + { nxt_string("raw_path"), &nxt_py_raw_path_str }, + { nxt_string("result"), &nxt_py_result_str }, + { nxt_string("root_path"), &nxt_py_root_path_str }, // not used + { nxt_string("scheme"), &nxt_py_scheme_str }, + { nxt_string("server"), &nxt_py_server_str }, + { nxt_string("set_exception"), &nxt_py_set_exception_str }, + { nxt_string("set_result"), &nxt_py_set_result_str }, + { nxt_string("spec_version"), &nxt_py_spec_version_str }, + { nxt_string("status"), &nxt_py_status_str }, + { nxt_string("subprotocol"), &nxt_py_subprotocol_str }, + { nxt_string("subprotocols"), &nxt_py_subprotocols_str }, + { nxt_string("text"), &nxt_py_text_str }, + { nxt_string("type"), &nxt_py_type_str }, + { nxt_string("version"), &nxt_py_version_str }, + { nxt_string("websocket"), &nxt_py_websocket_str }, + { nxt_string("websocket.accept"), &nxt_py_websocket_accept_str }, + { nxt_string("websocket.close"), &nxt_py_websocket_close_str }, + { nxt_string("websocket.connect"), &nxt_py_websocket_connect_str }, + { nxt_string("websocket.disconnect"), &nxt_py_websocket_disconnect_str }, + { nxt_string("websocket.receive"), &nxt_py_websocket_receive_str }, + { nxt_string("websocket.send"), &nxt_py_websocket_send_str }, + { nxt_string("ws"), &nxt_py_ws_str }, + { nxt_string("wss"), &nxt_py_wss_str }, + { nxt_null_string, NULL }, +}; + + +nxt_int_t +nxt_py_asgi_str_init(void) +{ + return nxt_python_init_strings(nxt_py_asgi_strings); +} + + +void +nxt_py_asgi_str_done(void) +{ + nxt_python_done_strings(nxt_py_asgi_strings); +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi_str.h b/src/python/nxt_python_asgi_str.h new file mode 100644 index 00000000..3f389c62 --- /dev/null +++ b/src/python/nxt_python_asgi_str.h @@ -0,0 +1,69 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_PYTHON_ASGI_STR_H_INCLUDED_ +#define _NXT_PYTHON_ASGI_STR_H_INCLUDED_ + + +extern PyObject *nxt_py_1_0_str; +extern PyObject *nxt_py_1_1_str; +extern PyObject *nxt_py_2_0_str; +extern PyObject *nxt_py_2_1_str; +extern PyObject *nxt_py_3_0_str; +extern PyObject *nxt_py_add_done_callback_str; +extern PyObject *nxt_py_asgi_str; +extern PyObject *nxt_py_bad_state_str; +extern PyObject *nxt_py_body_str; +extern PyObject *nxt_py_bytes_str; +extern PyObject *nxt_py_client_str; +extern PyObject *nxt_py_code_str; +extern PyObject *nxt_py_done_str; +extern PyObject *nxt_py_exception_str; +extern PyObject *nxt_py_failed_to_send_body_str; +extern PyObject *nxt_py_headers_str; +extern PyObject *nxt_py_http_str; +extern PyObject *nxt_py_http_disconnect_str; +extern PyObject *nxt_py_http_request_str; +extern PyObject *nxt_py_http_version_str; +extern PyObject *nxt_py_https_str; +extern PyObject *nxt_py_lifespan_str; +extern PyObject *nxt_py_lifespan_shutdown_str; +extern PyObject *nxt_py_lifespan_startup_str; +extern PyObject *nxt_py_method_str; +extern PyObject *nxt_py_message_str; +extern PyObject *nxt_py_message_too_big_str; +extern PyObject *nxt_py_more_body_str; +extern PyObject *nxt_py_path_str; +extern PyObject *nxt_py_query_string_str; +extern PyObject *nxt_py_result_str; +extern PyObject *nxt_py_raw_path_str; +extern PyObject *nxt_py_root_path_str; +extern PyObject *nxt_py_scheme_str; +extern PyObject *nxt_py_server_str; +extern PyObject *nxt_py_set_exception_str; +extern PyObject *nxt_py_set_result_str; +extern PyObject *nxt_py_spec_version_str; +extern PyObject *nxt_py_status_str; +extern PyObject *nxt_py_subprotocol_str; +extern PyObject *nxt_py_subprotocols_str; +extern PyObject *nxt_py_text_str; +extern PyObject *nxt_py_type_str; +extern PyObject *nxt_py_version_str; +extern PyObject *nxt_py_websocket_str; +extern PyObject *nxt_py_websocket_accept_str; +extern PyObject *nxt_py_websocket_close_str; +extern PyObject *nxt_py_websocket_connect_str; +extern PyObject *nxt_py_websocket_disconnect_str; +extern PyObject *nxt_py_websocket_receive_str; +extern PyObject *nxt_py_websocket_send_str; +extern PyObject *nxt_py_ws_str; +extern PyObject *nxt_py_wss_str; + + +nxt_int_t nxt_py_asgi_str_init(void); +void nxt_py_asgi_str_done(void); + + +#endif /* _NXT_PYTHON_ASGI_STR_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi_websocket.c b/src/python/nxt_python_asgi_websocket.c new file mode 100644 index 00000000..5a27b588 --- /dev/null +++ b/src/python/nxt_python_asgi_websocket.c @@ -0,0 +1,1084 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include + +#if (NXT_HAVE_ASGI) + +#include +#include +#include +#include +#include +#include +#include + + +enum { + NXT_WS_INIT, + NXT_WS_CONNECT, + NXT_WS_ACCEPTED, + NXT_WS_DISCONNECTED, + NXT_WS_CLOSED, +}; + + +typedef struct { + nxt_queue_link_t link; + nxt_unit_websocket_frame_t *frame; +} nxt_py_asgi_penging_frame_t; + + +typedef struct { + PyObject_HEAD + nxt_unit_request_info_t *req; + PyObject *receive_future; + PyObject *receive_exc_str; + int state; + nxt_queue_t pending_frames; + uint64_t pending_payload_len; + uint64_t pending_frame_len; + int pending_fins; +} nxt_py_asgi_websocket_t; + + +static PyObject *nxt_py_asgi_websocket_receive(PyObject *self, PyObject *none); +static PyObject *nxt_py_asgi_websocket_send(PyObject *self, PyObject *dict); +static PyObject *nxt_py_asgi_websocket_accept(nxt_py_asgi_websocket_t *ws, + PyObject *dict); +static PyObject *nxt_py_asgi_websocket_close(nxt_py_asgi_websocket_t *ws, + PyObject *dict); +static PyObject *nxt_py_asgi_websocket_send_frame(nxt_py_asgi_websocket_t *ws, + PyObject *dict); +static void nxt_py_asgi_websocket_receive_done(nxt_py_asgi_websocket_t *ws, + PyObject *msg); +static void nxt_py_asgi_websocket_receive_fail(nxt_py_asgi_websocket_t *ws, + PyObject *exc); +static void nxt_py_asgi_websocket_suspend_frame(nxt_unit_websocket_frame_t *f); +static PyObject *nxt_py_asgi_websocket_pop_msg(nxt_py_asgi_websocket_t *ws, + nxt_unit_websocket_frame_t *frame); +static uint64_t nxt_py_asgi_websocket_pending_len( + nxt_py_asgi_websocket_t *ws); +static nxt_unit_websocket_frame_t *nxt_py_asgi_websocket_pop_frame( + nxt_py_asgi_websocket_t *ws); +static PyObject *nxt_py_asgi_websocket_disconnect_msg( + nxt_py_asgi_websocket_t *ws); +static PyObject *nxt_py_asgi_websocket_done(PyObject *self, PyObject *future); + + +static PyMethodDef nxt_py_asgi_websocket_methods[] = { + { "receive", nxt_py_asgi_websocket_receive, METH_NOARGS, 0 }, + { "send", nxt_py_asgi_websocket_send, METH_O, 0 }, + { "_done", nxt_py_asgi_websocket_done, METH_O, 0 }, + { NULL, NULL, 0, 0 } +}; + +static PyAsyncMethods nxt_py_asgi_async_methods = { + .am_await = nxt_py_asgi_await, +}; + +static PyTypeObject nxt_py_asgi_websocket_type = { + PyVarObject_HEAD_INIT(NULL, 0) + + .tp_name = "unit._asgi_websocket", + .tp_basicsize = sizeof(nxt_py_asgi_websocket_t), + .tp_dealloc = nxt_py_asgi_dealloc, + .tp_as_async = &nxt_py_asgi_async_methods, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "unit ASGI WebSocket connection object", + .tp_iter = nxt_py_asgi_iter, + .tp_iternext = nxt_py_asgi_next, + .tp_methods = nxt_py_asgi_websocket_methods, +}; + +static uint64_t nxt_py_asgi_ws_max_frame_size = 1024 * 1024; +static uint64_t nxt_py_asgi_ws_max_buffer_size = 10 * 1024 * 1024; + + +nxt_int_t +nxt_py_asgi_websocket_init(nxt_task_t *task) +{ + if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_websocket_type) != 0)) { + nxt_alert(task, + "Python failed to initialize the \"asgi_websocket\" type object"); + return NXT_ERROR; + } + + return NXT_OK; +} + + +PyObject * +nxt_py_asgi_websocket_create(nxt_unit_request_info_t *req) +{ + nxt_py_asgi_websocket_t *ws; + + ws = PyObject_New(nxt_py_asgi_websocket_t, &nxt_py_asgi_websocket_type); + + if (nxt_fast_path(ws != NULL)) { + ws->req = req; + ws->receive_future = NULL; + ws->receive_exc_str = NULL; + ws->state = NXT_WS_INIT; + nxt_queue_init(&ws->pending_frames); + ws->pending_payload_len = 0; + ws->pending_frame_len = 0; + ws->pending_fins = 0; + } + + return (PyObject *) ws; +} + + +static PyObject * +nxt_py_asgi_websocket_receive(PyObject *self, PyObject *none) +{ + PyObject *future, *msg; + nxt_py_asgi_websocket_t *ws; + + ws = (nxt_py_asgi_websocket_t *) self; + + nxt_unit_req_debug(ws->req, "asgi_websocket_receive"); + + /* If exception happened out of receive() call, raise it now. */ + if (nxt_slow_path(ws->receive_exc_str != NULL)) { + PyErr_SetObject(PyExc_RuntimeError, ws->receive_exc_str); + + ws->receive_exc_str = NULL; + + return NULL; + } + + if (nxt_slow_path(ws->state == NXT_WS_CLOSED)) { + nxt_unit_req_error(ws->req, + "receive() called for closed WebSocket"); + + return PyErr_Format(PyExc_RuntimeError, + "WebSocket already closed"); + } + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_req_alert(ws->req, "Python failed to create Future object"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + if (nxt_slow_path(ws->state == NXT_WS_INIT)) { + ws->state = NXT_WS_CONNECT; + + msg = nxt_py_asgi_new_msg(ws->req, nxt_py_websocket_connect_str); + + return nxt_py_asgi_set_result_soon(ws->req, future, msg); + } + + if (ws->pending_fins > 0) { + msg = nxt_py_asgi_websocket_pop_msg(ws, NULL); + + return nxt_py_asgi_set_result_soon(ws->req, future, msg); + } + + if (nxt_slow_path(ws->state == NXT_WS_DISCONNECTED)) { + msg = nxt_py_asgi_websocket_disconnect_msg(ws); + + return nxt_py_asgi_set_result_soon(ws->req, future, msg); + } + + ws->receive_future = future; + Py_INCREF(ws->receive_future); + + return future; +} + + +static PyObject * +nxt_py_asgi_websocket_send(PyObject *self, PyObject *dict) +{ + PyObject *type; + const char *type_str; + Py_ssize_t type_len; + nxt_py_asgi_websocket_t *ws; + + static const nxt_str_t websocket_accept = nxt_string("websocket.accept"); + static const nxt_str_t websocket_close = nxt_string("websocket.close"); + static const nxt_str_t websocket_send = nxt_string("websocket.send"); + + ws = (nxt_py_asgi_websocket_t *) self; + + type = PyDict_GetItem(dict, nxt_py_type_str); + if (nxt_slow_path(type == NULL || !PyUnicode_Check(type))) { + nxt_unit_req_error(ws->req, "asgi_websocket_send: " + "'type' is not a unicode string"); + return PyErr_Format(PyExc_TypeError, + "'type' is not a unicode string"); + } + + type_str = PyUnicode_AsUTF8AndSize(type, &type_len); + + nxt_unit_req_debug(ws->req, "asgi_websocket_send type is '%.*s'", + (int) type_len, type_str); + + if (type_len == (Py_ssize_t) websocket_accept.length + && memcmp(type_str, websocket_accept.start, type_len) == 0) + { + return nxt_py_asgi_websocket_accept(ws, dict); + } + + if (type_len == (Py_ssize_t) websocket_close.length + && memcmp(type_str, websocket_close.start, type_len) == 0) + { + return nxt_py_asgi_websocket_close(ws, dict); + } + + if (type_len == (Py_ssize_t) websocket_send.length + && memcmp(type_str, websocket_send.start, type_len) == 0) + { + return nxt_py_asgi_websocket_send_frame(ws, dict); + } + + nxt_unit_req_error(ws->req, "asgi_websocket_send: " + "unexpected 'type': '%.*s'", (int) type_len, type_str); + return PyErr_Format(PyExc_AssertionError, "unexpected 'type': '%U'", type); +} + + +static PyObject * +nxt_py_asgi_websocket_accept(nxt_py_asgi_websocket_t *ws, PyObject *dict) +{ + int rc; + char *subprotocol_str; + PyObject *res, *headers, *subprotocol; + Py_ssize_t subprotocol_len; + nxt_py_asgi_calc_size_ctx_t calc_size_ctx; + nxt_py_asgi_add_field_ctx_t add_field_ctx; + + static const nxt_str_t ws_protocol = nxt_string("sec-websocket-protocol"); + + switch(ws->state) { + case NXT_WS_INIT: + return PyErr_Format(PyExc_RuntimeError, + "WebSocket connect not received"); + case NXT_WS_CONNECT: + break; + + case NXT_WS_ACCEPTED: + return PyErr_Format(PyExc_RuntimeError, "WebSocket already accepted"); + + case NXT_WS_DISCONNECTED: + return PyErr_Format(PyExc_RuntimeError, "WebSocket disconnected"); + + case NXT_WS_CLOSED: + return PyErr_Format(PyExc_RuntimeError, "WebSocket already closed"); + } + + if (nxt_slow_path(nxt_unit_response_is_websocket(ws->req))) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket already accepted"); + } + + if (nxt_slow_path(nxt_unit_response_is_sent(ws->req))) { + return PyErr_Format(PyExc_RuntimeError, "response already sent"); + } + + calc_size_ctx.fields_size = 0; + calc_size_ctx.fields_count = 0; + + headers = PyDict_GetItem(dict, nxt_py_headers_str); + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_calc_size, + &calc_size_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + } + + subprotocol = PyDict_GetItem(dict, nxt_py_subprotocol_str); + if (subprotocol != NULL && PyUnicode_Check(subprotocol)) { + subprotocol_str = PyUnicode_DATA(subprotocol); + subprotocol_len = PyUnicode_GET_LENGTH(subprotocol); + + calc_size_ctx.fields_size += ws_protocol.length + subprotocol_len; + calc_size_ctx.fields_count++; + + } else { + subprotocol_str = NULL; + subprotocol_len = 0; + } + + rc = nxt_unit_response_init(ws->req, 101, + calc_size_ctx.fields_count, + calc_size_ctx.fields_size); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to allocate response object"); + } + + add_field_ctx.req = ws->req; + add_field_ctx.content_length = -1; + + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_add_field, + &add_field_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + } + + if (subprotocol_len > 0) { + rc = nxt_unit_response_add_field(ws->req, + (const char *) ws_protocol.start, + ws_protocol.length, + subprotocol_str, subprotocol_len); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to add header"); + } + } + + rc = nxt_unit_response_send(ws->req); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, "failed to send response"); + } + + ws->state = NXT_WS_ACCEPTED; + + Py_INCREF(ws); + + return (PyObject *) ws; +} + + +static PyObject * +nxt_py_asgi_websocket_close(nxt_py_asgi_websocket_t *ws, PyObject *dict) +{ + int rc; + uint16_t status_code; + PyObject *code; + + if (nxt_slow_path(ws->state == NXT_WS_INIT)) { + return PyErr_Format(PyExc_RuntimeError, + "WebSocket connect not received"); + } + + if (nxt_slow_path(ws->state == NXT_WS_DISCONNECTED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket disconnected"); + } + + if (nxt_slow_path(ws->state == NXT_WS_CLOSED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket already closed"); + } + + if (nxt_unit_response_is_websocket(ws->req)) { + code = PyDict_GetItem(dict, nxt_py_code_str); + if (nxt_slow_path(code != NULL && !PyLong_Check(code))) { + return PyErr_Format(PyExc_TypeError, "'code' is not integer"); + } + + status_code = (code != NULL) ? htons(PyLong_AsLong(code)) + : htons(NXT_WEBSOCKET_CR_NORMAL); + + rc = nxt_unit_websocket_send(ws->req, NXT_WEBSOCKET_OP_CLOSE, + 1, &status_code, 2); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to send close frame"); + } + + } else { + rc = nxt_unit_response_init(ws->req, 403, 0, 0); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to allocate response object"); + } + + rc = nxt_unit_response_send(ws->req); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to send response"); + } + } + + ws->state = NXT_WS_CLOSED; + + Py_INCREF(ws); + + return (PyObject *) ws; +} + + +static PyObject * +nxt_py_asgi_websocket_send_frame(nxt_py_asgi_websocket_t *ws, PyObject *dict) +{ + int rc; + uint8_t opcode; + PyObject *bytes, *text; + const void *buf; + Py_ssize_t buf_size; + + if (nxt_slow_path(ws->state == NXT_WS_INIT)) { + return PyErr_Format(PyExc_RuntimeError, + "WebSocket connect not received"); + } + + if (nxt_slow_path(ws->state == NXT_WS_CONNECT)) { + return PyErr_Format(PyExc_RuntimeError, + "WebSocket not accepted yet"); + } + + if (nxt_slow_path(ws->state == NXT_WS_DISCONNECTED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket disconnected"); + } + + if (nxt_slow_path(ws->state == NXT_WS_CLOSED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket already closed"); + } + + bytes = PyDict_GetItem(dict, nxt_py_bytes_str); + if (bytes == Py_None) { + bytes = NULL; + } + + if (nxt_slow_path(bytes != NULL && !PyBytes_Check(bytes))) { + return PyErr_Format(PyExc_TypeError, + "'bytes' is not a byte string"); + } + + text = PyDict_GetItem(dict, nxt_py_text_str); + if (text == Py_None) { + text = NULL; + } + + if (nxt_slow_path(text != NULL && !PyUnicode_Check(text))) { + return PyErr_Format(PyExc_TypeError, + "'text' is not a unicode string"); + } + + if (nxt_slow_path(((bytes != NULL) ^ (text != NULL)) == 0)) { + return PyErr_Format(PyExc_ValueError, + "Exactly one of 'bytes' or 'text' must be non-None"); + } + + if (bytes != NULL) { + buf = PyBytes_AS_STRING(bytes); + buf_size = PyBytes_GET_SIZE(bytes); + opcode = NXT_WEBSOCKET_OP_BINARY; + + } else { + buf = PyUnicode_AsUTF8AndSize(text, &buf_size); + opcode = NXT_WEBSOCKET_OP_TEXT; + } + + rc = nxt_unit_websocket_send(ws->req, opcode, 1, buf, buf_size); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, "failed to send close frame"); + } + + Py_INCREF(ws); + return (PyObject *) ws; +} + + +void +nxt_py_asgi_websocket_handler(nxt_unit_websocket_frame_t *frame) +{ + uint8_t opcode; + uint16_t status_code; + uint64_t rest; + PyObject *msg, *exc; + nxt_py_asgi_websocket_t *ws; + + ws = frame->req->data; + + nxt_unit_req_debug(ws->req, "asgi_websocket_handler"); + + opcode = frame->header->opcode; + if (nxt_slow_path(opcode != NXT_WEBSOCKET_OP_CONT + && opcode != NXT_WEBSOCKET_OP_TEXT + && opcode != NXT_WEBSOCKET_OP_BINARY + && opcode != NXT_WEBSOCKET_OP_CLOSE)) + { + nxt_unit_websocket_done(frame); + + nxt_unit_req_debug(ws->req, + "asgi_websocket_handler: ignore frame with opcode %d", + opcode); + + return; + } + + if (nxt_slow_path(ws->state != NXT_WS_ACCEPTED)) { + nxt_unit_websocket_done(frame); + + goto bad_state; + } + + rest = nxt_py_asgi_ws_max_frame_size - ws->pending_frame_len; + + if (nxt_slow_path(frame->payload_len > rest)) { + nxt_unit_websocket_done(frame); + + goto too_big; + } + + rest = nxt_py_asgi_ws_max_buffer_size - ws->pending_payload_len; + + if (nxt_slow_path(frame->payload_len > rest)) { + nxt_unit_websocket_done(frame); + + goto too_big; + } + + if (ws->receive_future == NULL || frame->header->fin == 0) { + nxt_py_asgi_websocket_suspend_frame(frame); + + return; + } + + if (!nxt_queue_is_empty(&ws->pending_frames)) { + if (nxt_slow_path(opcode == NXT_WEBSOCKET_OP_TEXT + || opcode == NXT_WEBSOCKET_OP_BINARY)) + { + nxt_unit_req_alert(ws->req, + "Invalid state: pending frames with active receiver. " + "CONT frame expected. (%d)", opcode); + + PyErr_SetString(PyExc_AssertionError, + "Invalid state: pending frames with active receiver. " + "CONT frame expected."); + + nxt_unit_websocket_done(frame); + + return; + } + } + + msg = nxt_py_asgi_websocket_pop_msg(ws, frame); + if (nxt_slow_path(msg == NULL)) { + exc = PyErr_Occurred(); + Py_INCREF(exc); + + goto raise; + } + + nxt_py_asgi_websocket_receive_done(ws, msg); + + return; + +bad_state: + + if (ws->receive_future == NULL) { + ws->receive_exc_str = nxt_py_bad_state_str; + + return; + } + + exc = PyObject_CallFunctionObjArgs(PyExc_RuntimeError, + nxt_py_bad_state_str, + NULL); + if (nxt_slow_path(exc == NULL)) { + nxt_unit_req_alert(ws->req, "RuntimeError create failed"); + nxt_python_print_exception(); + + exc = Py_None; + Py_INCREF(exc); + } + + goto raise; + +too_big: + + status_code = htons(NXT_WEBSOCKET_CR_MESSAGE_TOO_BIG); + + (void) nxt_unit_websocket_send(ws->req, NXT_WEBSOCKET_OP_CLOSE, + 1, &status_code, 2); + + ws->state = NXT_WS_CLOSED; + + if (ws->receive_future == NULL) { + ws->receive_exc_str = nxt_py_message_too_big_str; + + return; + } + + exc = PyObject_CallFunctionObjArgs(PyExc_RuntimeError, + nxt_py_message_too_big_str, + NULL); + if (nxt_slow_path(exc == NULL)) { + nxt_unit_req_alert(ws->req, "RuntimeError create failed"); + nxt_python_print_exception(); + + exc = Py_None; + Py_INCREF(exc); + } + +raise: + + nxt_py_asgi_websocket_receive_fail(ws, exc); +} + + +static void +nxt_py_asgi_websocket_receive_done(nxt_py_asgi_websocket_t *ws, PyObject *msg) +{ + PyObject *future, *res; + + future = ws->receive_future; + ws->receive_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, msg, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(ws->req, "'set_result' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + Py_DECREF(msg); +} + + +static void +nxt_py_asgi_websocket_receive_fail(nxt_py_asgi_websocket_t *ws, PyObject *exc) +{ + PyObject *future, *res; + + future = ws->receive_future; + ws->receive_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_exception_str, exc, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(ws->req, "'set_exception' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + Py_DECREF(exc); +} + + +static void +nxt_py_asgi_websocket_suspend_frame(nxt_unit_websocket_frame_t *frame) +{ + int rc; + nxt_py_asgi_websocket_t *ws; + nxt_py_asgi_penging_frame_t *p; + + nxt_unit_req_debug(frame->req, "asgi_websocket_suspend_frame: " + "%d, %"PRIu64", %d", + frame->header->opcode, frame->payload_len, + frame->header->fin); + + ws = frame->req->data; + + rc = nxt_unit_websocket_retain(frame); + if (nxt_slow_path(rc == NXT_UNIT_ERROR)) { + nxt_unit_req_alert(ws->req, "Failed to retain frame for suspension."); + + nxt_unit_websocket_done(frame); + + PyErr_SetString(PyExc_RuntimeError, + "Failed to retain frame for suspension."); + + return; + } + + p = nxt_unit_malloc(frame->req->ctx, sizeof(nxt_py_asgi_penging_frame_t)); + if (nxt_slow_path(p == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to allocate buffer to suspend frame."); + + nxt_unit_websocket_done(frame); + + PyErr_SetString(PyExc_RuntimeError, + "Failed to allocate buffer to suspend frame."); + + return; + } + + p->frame = frame; + nxt_queue_insert_tail(&ws->pending_frames, &p->link); + + ws->pending_payload_len += frame->payload_len; + ws->pending_fins += frame->header->fin; + + if (frame->header->fin) { + ws->pending_frame_len = 0; + + } else { + if (frame->header->opcode == NXT_WEBSOCKET_OP_CONT) { + ws->pending_frame_len += frame->payload_len; + + } else { + ws->pending_frame_len = frame->payload_len; + } + } +} + + +static PyObject * +nxt_py_asgi_websocket_pop_msg(nxt_py_asgi_websocket_t *ws, + nxt_unit_websocket_frame_t *frame) +{ + int fin; + char *buf; + uint8_t code_buf[2], opcode; + uint16_t code; + PyObject *msg, *data, *type, *data_key; + uint64_t payload_len; + nxt_unit_websocket_frame_t *fin_frame; + + nxt_unit_req_debug(ws->req, "asgi_websocket_pop_msg"); + + fin_frame = NULL; + + if (nxt_queue_is_empty(&ws->pending_frames) + || (frame != NULL + && frame->header->opcode == NXT_WEBSOCKET_OP_CLOSE)) + { + payload_len = frame->payload_len; + + } else { + if (frame != NULL) { + payload_len = ws->pending_payload_len + frame->payload_len; + fin_frame = frame; + + } else { + payload_len = nxt_py_asgi_websocket_pending_len(ws); + } + + frame = nxt_py_asgi_websocket_pop_frame(ws); + } + + opcode = frame->header->opcode; + + if (nxt_slow_path(opcode == NXT_WEBSOCKET_OP_CONT)) { + nxt_unit_req_alert(ws->req, + "Invalid state: attempt to process CONT frame."); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_AssertionError, + "Invalid state: attempt to process CONT frame."); + } + + type = nxt_py_websocket_receive_str; + + switch (opcode) { + case NXT_WEBSOCKET_OP_TEXT: + buf = nxt_unit_malloc(frame->req->ctx, payload_len); + if (nxt_slow_path(buf == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to allocate buffer for payload (%d).", + (int) payload_len); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to allocate buffer for payload (%d).", + (int) payload_len); + } + + data = NULL; + data_key = nxt_py_text_str; + + break; + + case NXT_WEBSOCKET_OP_BINARY: + data = PyBytes_FromStringAndSize(NULL, payload_len); + if (nxt_slow_path(data == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to create Bytes for payload (%d).", + (int) payload_len); + nxt_python_print_exception(); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to create Bytes for payload."); + } + + buf = (char *) PyBytes_AS_STRING(data); + data_key = nxt_py_bytes_str; + + break; + + case NXT_WEBSOCKET_OP_CLOSE: + if (frame->payload_len >= 2) { + nxt_unit_websocket_read(frame, code_buf, 2); + code = ((uint16_t) code_buf[0]) << 8 | code_buf[1]; + + } else { + code = NXT_WEBSOCKET_CR_NORMAL; + } + + nxt_unit_websocket_done(frame); + + data = PyLong_FromLong(code); + if (nxt_slow_path(data == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to create Long from code %d.", + (int) code); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to create Long from code %d.", + (int) code); + } + + buf = NULL; + type = nxt_py_websocket_disconnect_str; + data_key = nxt_py_code_str; + + break; + + default: + nxt_unit_req_alert(ws->req, "Unexpected opcode %d", opcode); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_AssertionError, "Unexpected opcode %d", + opcode); + } + + if (buf != NULL) { + fin = frame->header->fin; + buf += nxt_unit_websocket_read(frame, buf, frame->payload_len); + + nxt_unit_websocket_done(frame); + + if (!fin) { + while (!nxt_queue_is_empty(&ws->pending_frames)) { + frame = nxt_py_asgi_websocket_pop_frame(ws); + fin = frame->header->fin; + + buf += nxt_unit_websocket_read(frame, buf, frame->payload_len); + + nxt_unit_websocket_done(frame); + + if (fin) { + break; + } + } + + if (fin_frame != NULL) { + buf += nxt_unit_websocket_read(fin_frame, buf, + fin_frame->payload_len); + nxt_unit_websocket_done(fin_frame); + } + } + + if (opcode == NXT_WEBSOCKET_OP_TEXT) { + buf -= payload_len; + + data = PyUnicode_DecodeUTF8(buf, payload_len, NULL); + + nxt_unit_free(ws->req->ctx, buf); + + if (nxt_slow_path(data == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to create Unicode for payload (%d).", + (int) payload_len); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to create Unicode."); + } + } + } + + msg = nxt_py_asgi_new_msg(ws->req, type); + if (nxt_slow_path(msg == NULL)) { + Py_DECREF(data); + return NULL; + } + + if (nxt_slow_path(PyDict_SetItem(msg, data_key, data) == -1)) { + nxt_unit_req_alert(ws->req, "Python failed to set 'msg.data' item"); + + Py_DECREF(msg); + Py_DECREF(data); + + return PyErr_Format(PyExc_RuntimeError, + "Python failed to set 'msg.data' item"); + } + + Py_DECREF(data); + + return msg; +} + + +static uint64_t +nxt_py_asgi_websocket_pending_len(nxt_py_asgi_websocket_t *ws) +{ + uint64_t res; + nxt_py_asgi_penging_frame_t *p; + + res = 0; + + nxt_queue_each(p, &ws->pending_frames, nxt_py_asgi_penging_frame_t, link) { + res += p->frame->payload_len; + + if (p->frame->header->fin) { + nxt_unit_req_debug(ws->req, "asgi_websocket_pending_len: %d", + (int) res); + return res; + } + } nxt_queue_loop; + + nxt_unit_req_debug(ws->req, "asgi_websocket_pending_len: %d (all)", + (int) res); + return res; +} + + +static nxt_unit_websocket_frame_t * +nxt_py_asgi_websocket_pop_frame(nxt_py_asgi_websocket_t *ws) +{ + nxt_queue_link_t *lnk; + nxt_unit_websocket_frame_t *frame; + nxt_py_asgi_penging_frame_t *p; + + lnk = nxt_queue_first(&ws->pending_frames); + nxt_queue_remove(lnk); + + p = nxt_queue_link_data(lnk, nxt_py_asgi_penging_frame_t, link); + + frame = p->frame; + ws->pending_payload_len -= frame->payload_len; + ws->pending_fins -= frame->header->fin; + + nxt_unit_free(frame->req->ctx, p); + + nxt_unit_req_debug(frame->req, "asgi_websocket_pop_frame: " + "%d, %"PRIu64", %d", + frame->header->opcode, frame->payload_len, + frame->header->fin); + + return frame; +} + + +void +nxt_py_asgi_websocket_close_handler(nxt_unit_request_info_t *req) +{ + PyObject *msg, *exc; + nxt_py_asgi_websocket_t *ws; + + ws = req->data; + + nxt_unit_req_debug(req, "asgi_websocket_close_handler"); + + if (ws->receive_future == NULL) { + ws->state = NXT_WS_DISCONNECTED; + + return; + } + + msg = nxt_py_asgi_websocket_disconnect_msg(ws); + if (nxt_slow_path(msg == NULL)) { + exc = PyErr_Occurred(); + Py_INCREF(exc); + + nxt_py_asgi_websocket_receive_fail(ws, exc); + + } else { + nxt_py_asgi_websocket_receive_done(ws, msg); + } +} + + +static PyObject * +nxt_py_asgi_websocket_disconnect_msg(nxt_py_asgi_websocket_t *ws) +{ + PyObject *msg, *code; + + msg = nxt_py_asgi_new_msg(ws->req, nxt_py_websocket_disconnect_str); + if (nxt_slow_path(msg == NULL)) { + return NULL; + } + + code = PyLong_FromLong(NXT_WEBSOCKET_CR_GOING_AWAY); + if (nxt_slow_path(code == NULL)) { + nxt_unit_req_alert(ws->req, "Python failed to create long"); + nxt_python_print_exception(); + + Py_DECREF(msg); + + return PyErr_Format(PyExc_RuntimeError, "failed to create long"); + } + + if (nxt_slow_path(PyDict_SetItem(msg, nxt_py_code_str, code) == -1)) { + nxt_unit_req_alert(ws->req, "Python failed to set 'msg.code' item"); + + Py_DECREF(msg); + Py_DECREF(code); + + return PyErr_Format(PyExc_RuntimeError, + "Python failed to set 'msg.code' item"); + } + + Py_DECREF(code); + + return msg; +} + + +static PyObject * +nxt_py_asgi_websocket_done(PyObject *self, PyObject *future) +{ + int rc; + uint16_t status_code; + PyObject *res; + nxt_py_asgi_websocket_t *ws; + + ws = (nxt_py_asgi_websocket_t *) self; + + nxt_unit_req_debug(ws->req, "asgi_websocket_done: %p", self); + + /* + * Get Future.result() and it raises an exception, if coroutine exited + * with exception. + */ + res = PyObject_CallMethodObjArgs(future, nxt_py_result_str, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(ws->req, + "Python failed to call 'future.result()'"); + nxt_python_print_exception(); + + rc = NXT_UNIT_ERROR; + + } else { + Py_DECREF(res); + + rc = NXT_UNIT_OK; + } + + if (ws->state == NXT_WS_ACCEPTED) { + status_code = (rc == NXT_UNIT_OK) + ? htons(NXT_WEBSOCKET_CR_NORMAL) + : htons(NXT_WEBSOCKET_CR_INTERNAL_SERVER_ERROR); + + rc = nxt_unit_websocket_send(ws->req, NXT_WEBSOCKET_OP_CLOSE, + 1, &status_code, 2); + } + + while (!nxt_queue_is_empty(&ws->pending_frames)) { + nxt_unit_websocket_done(nxt_py_asgi_websocket_pop_frame(ws)); + } + + nxt_unit_request_done(ws->req, rc); + + Py_RETURN_NONE; +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_wsgi.c b/src/python/nxt_python_wsgi.c index 3a5842f1..97030cd3 100644 --- a/src/python/nxt_python_wsgi.c +++ b/src/python/nxt_python_wsgi.c @@ -38,24 +38,6 @@ */ -#if PY_MAJOR_VERSION == 3 -#define NXT_PYTHON_BYTES_TYPE "bytestring" - -#define PyString_FromStringAndSize(str, size) \ - PyUnicode_DecodeLatin1((str), (size), "strict") -#define PyString_AS_STRING PyUnicode_DATA - -#else -#define NXT_PYTHON_BYTES_TYPE "string" - -#define PyBytes_FromStringAndSize PyString_FromStringAndSize -#define PyBytes_Check PyString_Check -#define PyBytes_GET_SIZE PyString_GET_SIZE -#define PyBytes_AS_STRING PyString_AS_STRING -#define PyUnicode_InternInPlace PyString_InternInPlace -#define PyUnicode_AsUTF8 PyString_AS_STRING -#endif - typedef struct nxt_python_run_ctx_s nxt_python_run_ctx_t; typedef struct { -- cgit From d97e3a3296db77f6a33ce010a66d2a0b2d4bac49 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 1 Oct 2020 23:55:35 +0300 Subject: Tests: added ASGI WebSocket. --- test/python/websockets/mirror/asgi.py | 18 + test/python/websockets/subprotocol/asgi.py | 25 + test/test_asgi_websockets.py | 1432 ++++++++++++++++++++++++++++ test/unit/applications/websockets.py | 2 +- 4 files changed, 1476 insertions(+), 1 deletion(-) create mode 100644 test/python/websockets/mirror/asgi.py create mode 100644 test/python/websockets/subprotocol/asgi.py create mode 100644 test/test_asgi_websockets.py diff --git a/test/python/websockets/mirror/asgi.py b/test/python/websockets/mirror/asgi.py new file mode 100644 index 00000000..0f1d9953 --- /dev/null +++ b/test/python/websockets/mirror/asgi.py @@ -0,0 +1,18 @@ +async def application(scope, receive, send): + if scope['type'] == 'websocket': + while True: + m = await receive() + if m['type'] == 'websocket.connect': + 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), + }) + + if m['type'] == 'websocket.disconnect': + break; diff --git a/test/python/websockets/subprotocol/asgi.py b/test/python/websockets/subprotocol/asgi.py new file mode 100644 index 00000000..92263dd7 --- /dev/null +++ b/test/python/websockets/subprotocol/asgi.py @@ -0,0 +1,25 @@ +async def application(scope, receive, send): + assert scope['type'] == 'websocket' + + while True: + m = await receive() + if m['type'] == 'websocket.connect': + subprotocols = scope['subprotocols'] + + 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), + }) + + if m['type'] == 'websocket.disconnect': + break; diff --git a/test/test_asgi_websockets.py b/test/test_asgi_websockets.py new file mode 100644 index 00000000..249f630f --- /dev/null +++ b/test/test_asgi_websockets.py @@ -0,0 +1,1432 @@ +import pytest +import struct +import time +from distutils.version import LooseVersion + +from unit.applications.lang.python import TestApplicationPython +from unit.applications.websockets import TestApplicationWebsocket +from conftest import option, skip_alert + + +class TestASGIWebsockets(TestApplicationPython): + prerequisites = {'modules': {'python': + lambda v: LooseVersion(v) >= LooseVersion('3.5')}} + load_module = 'asgi' + + ws = TestApplicationWebsocket() + + def setup_method(self): + super().setup_method() + + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' + ), 'clear keepalive_interval' + + skip_alert(r'socket close\(\d+\) failed') + + def close_connection(self, sock): + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + + self.check_close(sock) + + def check_close(self, sock, code=1000, no_close=False): + frame = self.ws.frame_read(sock) + + assert frame['fin'] == True, 'close fin' + assert frame['opcode'] == self.ws.OP_CLOSE, 'close opcode' + assert frame['code'] == code, 'close code' + + if not no_close: + sock.close() + + def check_frame(self, frame, fin, opcode, payload, decode=True): + if opcode == self.ws.OP_BINARY or not decode: + data = frame['data'] + else: + data = frame['data'].decode('utf-8') + + assert frame['fin'] == fin, 'fin' + assert frame['opcode'] == opcode, 'opcode' + assert data == payload, 'payload' + + def test_asgi_websockets_handshake(self): + self.load('websockets/mirror') + + resp, sock, key = self.ws.upgrade() + sock.close() + + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' + + def test_asgi_websockets_subprotocol(self): + self.load('websockets/subprotocol') + + resp, sock, key = self.ws.upgrade() + sock.close() + + assert resp['status'] == 101, 'status' + assert resp['headers']['x-subprotocols'] == "('chat', 'phone', 'video')", 'subprotocols' + assert resp['headers']['sec-websocket-protocol'] == 'chat', 'key' + + def test_asgi_websockets_mirror(self): + self.load('websockets/mirror') + + 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() + + def test_asgi_websockets_no_mask(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message, mask=False) + + frame = self.ws.frame_read(sock) + + assert frame['opcode'] == self.ws.OP_CLOSE, 'no mask opcode' + assert frame['code'] == 1002, 'no mask close code' + + sock.close() + + def test_asgi_websockets_fragmentation(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message, fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, ' ', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, message) + + frame = self.ws.frame_read(sock) + + assert message + ' ' + message == frame['data'].decode( + 'utf-8' + ), 'mirror framing' + + sock.close() + + def test_asgi_websockets_frame_fragmentation_invalid(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PING, message, fin=False) + + frame = self.ws.frame_read(sock) + + frame.pop('data') + assert frame == { + 'fin': True, + 'rsv1': False, + 'rsv2': False, + 'rsv3': False, + 'opcode': self.ws.OP_CLOSE, + 'mask': 0, + 'code': 1002, + 'reason': 'Fragmented control frame', + }, 'close frame' + + sock.close() + + def test_asgi_websockets_large(self): + self.load('websockets/mirror') + + message = '0123456789' * 300 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + + frame = self.ws.frame_read(sock) + data = frame['data'].decode('utf-8') + + frame = self.ws.frame_read(sock) + data += frame['data'].decode('utf-8') + + assert message == data, 'large' + + sock.close() + + def test_asgi_websockets_two_clients(self): + self.load('websockets/mirror') + + message1 = 'blah1' + message2 = 'blah2' + + _, sock1, _ = self.ws.upgrade() + _, sock2, _ = self.ws.upgrade() + + self.ws.frame_write(sock1, self.ws.OP_TEXT, message1) + self.ws.frame_write(sock2, self.ws.OP_TEXT, message2) + + frame1 = self.ws.frame_read(sock1) + frame2 = self.ws.frame_read(sock2) + + assert message1 == frame1['data'].decode('utf-8'), 'client 1' + assert message2 == frame2['data'].decode('utf-8'), 'client 2' + + sock1.close() + sock2.close() + + @pytest.mark.skip('not yet') + def test_asgi_websockets_handshake_upgrade_absent( + self + ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'upgrade absent' + + def test_asgi_websockets_handshake_case_insensitive(self): + self.load('websockets/mirror') + + resp, sock, _ = self.ws.upgrade( + headers={ + 'Host': 'localhost', + 'Upgrade': 'WEBSOCKET', + 'Connection': 'UPGRADE', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + } + ) + sock.close() + + assert resp['status'] == 101, 'status' + + @pytest.mark.skip('not yet') + def test_asgi_websockets_handshake_connection_absent(self): # FAIL + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_handshake_version_absent(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + }, + ) + + assert resp['status'] == 426, 'status' + + @pytest.mark.skip('not yet') + def test_asgi_websockets_handshake_key_invalid(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': '!', + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'key length' + + key = self.ws.key() + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': [key, key], + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + 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') + + resp = self.post( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_handshake_http_10(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + http_10=True, + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_handshake_uri_invalid(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + url='!', + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_protocol_absent(self): + self.load('websockets/mirror') + + key = self.ws.key() + resp, sock, _ = self.ws.upgrade( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': key, + 'Sec-WebSocket-Version': 13, + } + ) + sock.close() + + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' + + # autobahn-testsuite + # + # Some following tests fail because of Unit does not support UTF-8 + # validation for websocket frames. It should be implemented + # by application, if necessary. + + def test_asgi_websockets_1_1_1__1_1_8(self): + self.load('websockets/mirror') + + opcode = self.ws.OP_TEXT + + _, sock, _ = self.ws.upgrade() + + def check_length(length, chopsize=None): + payload = '*' * length + + self.ws.frame_write(sock, opcode, payload, chopsize=chopsize) + + 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 + + self.close_connection(sock) + + def test_asgi_websockets_1_2_1__1_2_8(self): + self.load('websockets/mirror') + + opcode = self.ws.OP_BINARY + + _, sock, _ = self.ws.upgrade() + + def check_length(length, chopsize=None): + payload = b'\xfe' * length + + self.ws.frame_write(sock, opcode, payload, chopsize=chopsize) + frame = self.ws.frame_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 + + self.close_connection(sock) + + def test_asgi_websockets_2_1__2_6(self): + self.load('websockets/mirror') + + op_ping = self.ws.OP_PING + op_pong = self.ws.OP_PONG + + _, sock, _ = self.ws.upgrade() + + def check_ping(payload, chopsize=None, decode=True): + self.ws.frame_write(sock, op_ping, payload, chopsize=chopsize) + frame = self.ws.frame_read(sock) + + self.check_frame(frame, True, op_pong, payload, decode=decode) + + 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 + + self.close_connection(sock) + + # 2_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PING, b'\xfe' * 126) + self.check_close(sock, 1002) + + def test_asgi_websockets_2_7__2_9(self): + self.load('websockets/mirror') + + # 2_7 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PONG, '') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_7' + + # 2_8 + + self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_8' + + # 2_9 + + payload = 'ping payload' + + self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') + self.ws.frame_write(sock, self.ws.OP_PING, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, payload) + + self.close_connection(sock) + + def test_asgi_websockets_2_10__2_11(self): + self.load('websockets/mirror') + + # 2_10 + + _, sock, _ = self.ws.upgrade() + + for i in range(0, 10): + self.ws.frame_write(sock, self.ws.OP_PING, 'payload-%d' % i) + + for i in range(0, 10): + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'payload-%d' % i) + + # 2_11 + + for i in range(0, 10): + opcode = self.ws.OP_PING + self.ws.frame_write(sock, opcode, 'payload-%d' % i, chopsize=1) + + for i in range(0, 10): + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'payload-%d' % i) + + self.close_connection(sock) + + @pytest.mark.skip('not yet') + def test_asgi_websockets_3_1__3_7(self): + self.load('websockets/mirror') + + payload = 'Hello, world!' + + # 3_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, rsv1=True) + self.check_close(sock, 1002) + + # 3_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, rsv2=True) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.check_close(sock, 1002, no_close=True) + + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_2' + sock.close() + + # 3_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write( + sock, self.ws.OP_TEXT, payload, rsv1=True, rsv2=True + ) + + self.check_close(sock, 1002, no_close=True) + + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_3' + sock.close() + + # 3_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1) + self.ws.frame_write( + sock, self.ws.OP_TEXT, payload, rsv3=True, chopsize=1 + ) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.check_close(sock, 1002, no_close=True) + + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_4' + sock.close() + + # 3_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, + self.ws.OP_BINARY, + b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', + rsv1=True, + rsv3=True, + ) + + self.check_close(sock, 1002) + + # 3_6 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_PING, payload, rsv2=True, rsv3=True + ) + + self.check_close(sock, 1002) + + # 3_7 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CLOSE, payload, rsv1=True, rsv2=True, rsv3=True + ) + + self.check_close(sock, 1002) + + def test_asgi_websockets_4_1_1__4_2_5(self): + self.load('websockets/mirror') + + payload = 'Hello, world!' + + # 4_1_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x03, '') + self.check_close(sock, 1002) + + # 4_1_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x04, 'reserved opcode payload') + self.check_close(sock, 1002) + + # 4_1_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x05, '') + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_1_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x06, payload) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_1_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x07, payload, chopsize=1) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_2_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x0B, '') + self.check_close(sock, 1002) + + # 4_2_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x0C, 'reserved opcode payload') + self.check_close(sock, 1002) + + # 4_2_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x0D, '') + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_2_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x0E, payload) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_2_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x0F, payload, chopsize=1) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + def test_asgi_websockets_5_1__5_20(self): + self.load('websockets/mirror') + + # 5_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PING, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + self.check_close(sock, 1002) + + # 5_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PONG, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + self.check_close(sock, 1002) + + # 5_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_4 + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + assert self.recvall(sock, read_timeout=0.1) == b'', '5_4' + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_5 + + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1 + ) + self.ws.frame_write( + sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1 + ) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_6 + + ping_payload = 'ping payload' + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, ping_payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_7 + + ping_payload = 'ping payload' + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + assert self.recvall(sock, read_timeout=0.1) == b'', '5_7' + + self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, ping_payload) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_8 + + ping_payload = 'ping payload' + + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1 + ) + self.ws.frame_write(sock, self.ws.OP_PING, ping_payload, chopsize=1) + self.ws.frame_write( + sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1 + ) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, ping_payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_9 + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=True + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_10 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=True + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_11 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, + self.ws.OP_CONT, + 'non-continuation payload', + fin=True, + chopsize=1, + ) + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1 + ) + self.check_close(sock, 1002) + + # 5_12 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=False + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_13 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=False + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_14 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, + self.ws.OP_CONT, + 'non-continuation payload', + fin=False, + chopsize=1, + ) + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1 + ) + self.check_close(sock, 1002) + + # 5_15 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment4', fin=True) + self.check_close(sock, 1002) + + # 5_16 + + _, sock, _ = self.ws.upgrade() + + for i in range(0, 2): + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=True) + self.check_close(sock, 1002) + + # 5_17 + + _, sock, _ = self.ws.upgrade() + + for i in range(0, 2): + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment1', fin=True) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=True) + self.check_close(sock, 1002) + + # 5_18 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2') + self.check_close(sock, 1002) + + # 5_19 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 1!') + + time.sleep(1) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment4', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 2!') + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 1!') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') + + self.check_frame( + self.ws.frame_read(sock), + True, + self.ws.OP_TEXT, + 'fragment1fragment2fragment3fragment4fragment5', + ) + + # 5_20 + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 1!') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 1!') + + time.sleep(1) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment4', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 2!') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') + + assert self.recvall(sock, read_timeout=0.1) == b'', '5_20' + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') + + self.check_frame( + self.ws.frame_read(sock), + True, + self.ws.OP_TEXT, + 'fragment1fragment2fragment3fragment4fragment5', + ) + + self.close_connection(sock) + + def test_asgi_websockets_6_1_1__6_4_4(self): + self.load('websockets/mirror') + + # 6_1_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, '') + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, '') + + # 6_1_2 + + self.ws.frame_write(sock, self.ws.OP_TEXT, '', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, '', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, '') + + # 6_1_3 + + payload = 'middle frame payload' + + self.ws.frame_write(sock, self.ws.OP_TEXT, '', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, payload, fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_1 + + payload = 'Hello-µ@ßöäüàá-UTF-8!!' + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_2 + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload[:12], fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, payload[12:]) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_3 + + self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_4 + + payload = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' + + self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + 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 + + def test_asgi_websockets_7_1_1__7_5_1(self): + self.load('websockets/mirror') + + # 7_1_1 + + _, sock, _ = self.ws.upgrade() + + payload = "Hello World!" + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.close_connection(sock) + + # 7_1_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + + self.check_close(sock) + + # 7_1_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock, no_close=True) + + self.ws.frame_write(sock, self.ws.OP_PING, '') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_1_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock, no_close=True) + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_1_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock, no_close=True) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_1_6 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'BAsd7&jh23' * 26 * 2 ** 10) + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + + self.recvall(sock, read_timeout=1) + + self.ws.frame_write(sock, self.ws.OP_PING, '') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_3_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, '') + self.check_close(sock) + + # 7_3_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, 'a') + self.check_close(sock, 1002) + + # 7_3_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock) + + # 7_3_4 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(reason='Hello World!') + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock) + + # 7_3_5 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(reason='*' * 123) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock) + + # 7_3_6 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(reason='*' * 124) + + 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) + + def test_asgi_websockets_7_7_X__7_9_X(self): + self.load('websockets/mirror') + + valid_codes = [ + 1000, + 1001, + 1002, + 1003, + 1007, + 1008, + 1009, + 1010, + 1011, + 3000, + 3999, + 4000, + 4999, + ] + + invalid_codes = [0, 999, 1004, 1005, 1006, 1016, 1100, 2000, 2999] + + for code in valid_codes: + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(code=code) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock) + + for code in invalid_codes: + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(code=code) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock, 1002) + + def test_asgi_websockets_7_13_1__7_13_2(self): + self.load('websockets/mirror') + + # 7_13_1 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(code=5000) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock, 1002) + + # 7_13_2 + + _, sock, _ = self.ws.upgrade() + + payload = struct.pack('!I', 65536) + ''.encode('utf-8') + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock, 1002) + + def test_asgi_websockets_9_1_1__9_6_6(self, is_unsafe): + if not is_unsafe: + pytest.skip('unsafe, long run') + + self.load('websockets/mirror') + + assert 'success' in self.conf( + { + 'http': { + 'websocket': { + 'max_frame_size': 33554432, + 'keepalive_interval': 0, + } + } + }, + 'settings', + ), 'increase max_frame_size and keepalive_interval' + + _, sock, _ = self.ws.upgrade() + + op_text = self.ws.OP_TEXT + op_binary = self.ws.OP_BINARY + + def check_payload(opcode, length, chopsize=None): + if opcode == self.ws.OP_TEXT: + payload = '*' * length + else: + payload = b'*' * length + + self.ws.frame_write(sock, opcode, payload, chopsize=chopsize) + frame = self.ws.frame_read(sock, read_timeout=5) + self.check_frame(frame, True, opcode, payload) + + def check_message(opcode, f_size): + if opcode == self.ws.OP_TEXT: + payload = '*' * 4 * 2 ** 20 + else: + payload = b'*' * 4 * 2 ** 20 + + self.ws.message(sock, opcode, payload, fragmention_size=f_size) + 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_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_payload(op_binary, 2 ** 20, chopsize=1024) # 9_6_5 + check_payload(op_binary, 2 ** 20, chopsize=2048) # 9_6_6 + + self.close_connection(sock) + + def test_asgi_websockets_10_1_1(self): + self.load('websockets/mirror') + + _, sock, _ = self.ws.upgrade() + + payload = '*' * 65536 + + self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1300) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.close_connection(sock) + + # settings + + def test_asgi_websockets_max_frame_size(self): + self.load('websockets/mirror') + + assert 'success' in self.conf( + {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' + ), 'configure max_frame_size' + + _, sock, _ = self.ws.upgrade() + + payload = '*' * 94 + opcode = self.ws.OP_TEXT + + self.ws.frame_write(sock, opcode, payload) # frame length is 100 + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, opcode, payload) + + payload = '*' * 95 + + self.ws.frame_write(sock, opcode, payload) # frame length is 101 + self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE + + def test_asgi_websockets_read_timeout(self): + self.load('websockets/mirror') + + assert 'success' in self.conf( + {'http': {'websocket': {'read_timeout': 5}}}, 'settings' + ), 'configure read_timeout' + + _, sock, _ = self.ws.upgrade() + + frame = self.ws.frame_to_send(self.ws.OP_TEXT, 'blah') + sock.sendall(frame[:2]) + + time.sleep(2) + + self.check_close(sock, 1001) # 1001 - CLOSE_GOING_AWAY + + def test_asgi_websockets_keepalive_interval(self): + self.load('websockets/mirror') + + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' + ), 'configure keepalive_interval' + + _, sock, _ = self.ws.upgrade() + + frame = self.ws.frame_to_send(self.ws.OP_TEXT, 'blah') + sock.sendall(frame[:2]) + + time.sleep(2) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PING, '') # PING frame + + sock.close() diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index cb9f40aa..bae40620 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -40,7 +40,7 @@ class TestApplicationWebsocket(TestApplicationProto): 'Upgrade': 'websocket', 'Connection': 'Upgrade', 'Sec-WebSocket-Key': key, - 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Protocol': 'chat, phone, video', 'Sec-WebSocket-Version': 13, } -- cgit From 12f225a43acc6b5086b08c3d7df6f6ac2322efa1 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 1 Oct 2020 23:55:43 +0300 Subject: Tests: added ASGI HTTP applications. --- test/python/204_no_content/asgi.py | 8 + test/python/delayed/asgi.py | 51 +++++ test/python/empty/asgi.py | 10 + test/python/mirror/asgi.py | 22 ++ test/python/query_string/asgi.py | 11 + test/python/server_port/asgi.py | 11 + test/python/threading/asgi.py | 42 ++++ test/python/variables/asgi.py | 40 ++++ test/test_asgi_application.py | 403 +++++++++++++++++++++++++++++++++++++ 9 files changed, 598 insertions(+) create mode 100644 test/python/204_no_content/asgi.py create mode 100644 test/python/delayed/asgi.py create mode 100644 test/python/empty/asgi.py create mode 100644 test/python/mirror/asgi.py create mode 100644 test/python/query_string/asgi.py create mode 100644 test/python/server_port/asgi.py create mode 100644 test/python/threading/asgi.py create mode 100644 test/python/variables/asgi.py create mode 100644 test/test_asgi_application.py diff --git a/test/python/204_no_content/asgi.py b/test/python/204_no_content/asgi.py new file mode 100644 index 00000000..634facc2 --- /dev/null +++ b/test/python/204_no_content/asgi.py @@ -0,0 +1,8 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + await send({ + 'type': 'http.response.start', + 'status': 204, + 'headers': [], + }) diff --git a/test/python/delayed/asgi.py b/test/python/delayed/asgi.py new file mode 100644 index 00000000..d5cad929 --- /dev/null +++ b/test/python/delayed/asgi.py @@ -0,0 +1,51 @@ +import asyncio + +async def application(scope, receive, send): + assert scope['type'] == 'http' + + body = b'' + while True: + m = await receive() + body += m.get('body', b'') + if not m.get('more_body', False): + break + + headers = scope.get('headers', []) + + def get_header(n, v=None): + for h in headers: + if h[0] == n: + return h[1] + return v + + parts = int(get_header(b'x-parts', 1)) + delay = int(get_header(b'x-delay', 0)) + + loop = asyncio.get_event_loop() + + async def sleep(n): + future = loop.create_future() + 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()), + ] + }) + + if not body: + await sleep(delay) + return + + 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 sleep(delay) diff --git a/test/python/empty/asgi.py b/test/python/empty/asgi.py new file mode 100644 index 00000000..58b7c1f2 --- /dev/null +++ b/test/python/empty/asgi.py @@ -0,0 +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'), + ] + }) diff --git a/test/python/mirror/asgi.py b/test/python/mirror/asgi.py new file mode 100644 index 00000000..7088e893 --- /dev/null +++ b/test/python/mirror/asgi.py @@ -0,0 +1,22 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + body = b'' + while True: + m = await receive() + body += m.get('body', b'') + 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.body', + 'body': body, + }) diff --git a/test/python/query_string/asgi.py b/test/python/query_string/asgi.py new file mode 100644 index 00000000..28f4d107 --- /dev/null +++ b/test/python/query_string/asgi.py @@ -0,0 +1,11 @@ +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']), + ] + }) diff --git a/test/python/server_port/asgi.py b/test/python/server_port/asgi.py new file mode 100644 index 00000000..e79ced00 --- /dev/null +++ b/test/python/server_port/asgi.py @@ -0,0 +1,11 @@ +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()), + ] + }) diff --git a/test/python/threading/asgi.py b/test/python/threading/asgi.py new file mode 100644 index 00000000..3c978e50 --- /dev/null +++ b/test/python/threading/asgi.py @@ -0,0 +1,42 @@ +import asyncio +import sys +import time +import threading + + +class Foo(threading.Thread): + num = 10 + + def __init__(self, x): + self.__x = x + threading.Thread.__init__(self) + + def log_index(self, index): + sys.stderr.write( + "(" + str(index) + ") Thread: " + str(self.__x) + "\n" + ) + sys.stderr.flush() + + def run(self): + i = 0 + for _ in range(3): + self.log_index(i) + i += 1 + time.sleep(1) + self.log_index(i) + i += 1 + + +async def application(scope, receive, send): + assert scope['type'] == 'http' + + Foo(Foo.num).start() + Foo.num += 10 + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + ] + }) diff --git a/test/python/variables/asgi.py b/test/python/variables/asgi.py new file mode 100644 index 00000000..dd1cca72 --- /dev/null +++ b/test/python/variables/asgi.py @@ -0,0 +1,40 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + body = b'' + while True: + m = await receive() + body += m.get('body', b'') + if not m.get('more_body', False): + break + + headers = scope.get('headers', []) + + def get_header(n): + res = [] + for h in headers: + if h[0] == n: + 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.body', + 'body': body, + }) diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py new file mode 100644 index 00000000..7816caec --- /dev/null +++ b/test/test_asgi_application.py @@ -0,0 +1,403 @@ +import grp +import pytest +import pwd +import re +import time +from distutils.version import LooseVersion + +from unit.applications.lang.python import TestApplicationPython +from conftest import skip_alert + + +class TestASGIApplication(TestApplicationPython): + prerequisites = {'modules': {'python': + lambda v: LooseVersion(v) >= LooseVersion('3.5')}} + load_module = 'asgi' + + def findall(self, pattern): + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: + return re.findall(pattern, f.read()) + + def test_asgi_application__variables(self): + self.load('variables') + + body = 'Test body string.' + + resp = self.http( + b"""POST / HTTP/1.1 +Host: localhost +Content-Length: %d +Custom-Header: blah +Custom-hEader: Blah +Content-Type: text/html +Connection: close +custom-header: BLAH + +%s""" % (len(body), body.encode()), + raw=True, + ) + + assert resp['status'] == 200, 'status' + headers = resp['headers'] + header_server = headers.pop('Server') + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + + date = headers.pop('Date') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Connection': 'close', + 'content-length': str(len(body)), + 'content-type': 'text/html', + 'request-method': 'POST', + 'request-uri': '/', + 'http-host': 'localhost', + 'http-version': '1.1', + 'custom-header': 'blah, Blah, BLAH', + 'asgi-version': '3.0', + 'asgi-spec-version': '2.1', + 'scheme': 'http', + }, 'headers' + assert resp['body'] == body, 'body' + + def test_asgi_application__query_string(self): + self.load('query_string') + + resp = self.get(url='/?var1=val1&var2=val2') + + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string header' + + def test_asgi_application__query_string_space(self): + self.load('query_string') + + resp = self.get(url='/ ?var1=val1&var2=val2') + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string space' + + resp = self.get(url='/ %20?var1=val1&var2=val2') + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string space 2' + + resp = self.get(url='/ %20 ?var1=val1&var2=val2') + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string space 3' + + resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2') + assert ( + resp['headers']['query-string'] == ' var1= val1 & var2=val2' + ), 'query-string space 4' + + def test_asgi_application__query_string_empty(self): + self.load('query_string') + + resp = self.get(url='/?') + + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['query-string'] == '', 'query string empty' + + def test_asgi_application__query_string_absent(self): + self.load('query_string') + + resp = self.get() + + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['query-string'] == '', 'query string absent' + + @pytest.mark.skip('not yet') + def test_asgi_application__server_port(self): + self.load('server_port') + + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' + + @pytest.mark.skip('not yet') + def test_asgi_application__working_directory_invalid(self): + self.load('empty') + + assert 'success' in self.conf( + '"/blah"', 'applications/empty/working_directory' + ), 'configure invalid working_directory' + + assert self.get()['status'] == 500, 'status' + + def test_asgi_application__204_transfer_encoding(self): + self.load('204_no_content') + + assert ( + 'Transfer-Encoding' not in self.get()['headers'] + ), '204 header transfer encoding' + + def test_asgi_application__shm_ack_handle(self): + self.load('mirror') + + # Minimum possible limit + shm_limit = 10 * 1024 * 1024 + + assert ( + 'success' in self.conf('{"shm": ' + str(shm_limit) + '}', + 'applications/mirror/limits') + ) + + # Should exceed shm_limit + max_body_size = 12 * 1024 * 1024 + + assert ( + 'success' in self.conf('{"http":{"max_body_size": ' + + str(max_body_size) + ' }}', + 'settings') + ) + + assert self.get()['status'] == 200, 'init' + + body = '0123456789AB' * 1024 * 1024 # 12 Mb + resp = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + body=body, + read_buffer_size=1024 * 1024, + ) + + assert resp['body'] == body, 'keep-alive 1' + + def test_asgi_keepalive_body(self): + self.load('mirror') + + assert self.get()['status'] == 200, 'init' + + body = '0123456789' * 500 + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'keep-alive 1' + + body = '0123456789' + resp = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + sock=sock, + body=body, + ) + + assert resp['body'] == body, 'keep-alive 2' + + def test_asgi_keepalive_reconfigure(self): + skip_alert( + r'pthread_mutex.+failed', + r'failed to apply', + r'process \d+ exited on signal', + ) + self.load('mirror') + + assert self.get()['status'] == 200, 'init' + + body = '0123456789' + conns = 3 + socks = [] + + for i in range(conns): + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'keep-alive open' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure' + + socks.append(sock) + + for i in range(conns): + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + sock=socks[i], + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'keep-alive request' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 2' + + for i in range(conns): + resp = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + sock=socks[i], + body=body, + ) + + assert resp['body'] == body, 'keep-alive close' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 3' + + def test_asgi_keepalive_reconfigure_2(self): + self.load('mirror') + + assert self.get()['status'] == 200, 'init' + + body = '0123456789' + + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'reconfigure 2 keep-alive 1' + + self.load('empty') + + assert self.get()['status'] == 200, 'init' + + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + start=True, + sock=sock, + body=body, + ) + + assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' + assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' + + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 2 clear configuration' + + resp = self.get(sock=sock) + + assert resp == {}, 'reconfigure 2 keep-alive 3' + + def test_asgi_keepalive_reconfigure_3(self): + self.load('empty') + + assert self.get()['status'] == 200, 'init' + + (_, sock) = self.http( + b"""GET / HTTP/1.1 +""", + start=True, + raw=True, + no_recv=True, + ) + + assert self.get()['status'] == 200 + + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 3 clear configuration' + + resp = self.http( + b"""Host: localhost +Connection: close + +""", + sock=sock, + raw=True, + ) + + assert resp['status'] == 200, 'reconfigure 3' + + def test_asgi_process_switch(self): + self.load('delayed') + + assert 'success' in self.conf( + '2', 'applications/delayed/processes' + ), 'configure 2 processes' + + self.get( + headers={ + 'Host': 'localhost', + 'Content-Length': '0', + 'X-Delay': '5', + 'Connection': 'close', + }, + no_recv=True, + ) + + headers_delay_1 = { + 'Connection': 'close', + 'Host': 'localhost', + 'Content-Length': '0', + 'X-Delay': '1', + } + + self.get(headers=headers_delay_1, no_recv=True) + + time.sleep(0.5) + + for _ in range(10): + self.get(headers=headers_delay_1, no_recv=True) + + self.get(headers=headers_delay_1) + + def test_asgi_application__loading_error(self): + skip_alert(r'Python failed to import module "blah"') + + self.load('empty') + + assert 'success' in self.conf('"blah"', 'applications/empty/module') + + assert self.get()['status'] == 503, 'loading error' + + def test_asgi_application__threading(self): + """wait_for_record() timeouts after 5s while every thread works at + least 3s. So without releasing GIL test should fail. + """ + + self.load('threading') + + for _ in range(10): + self.get(no_recv=True) + + assert ( + self.wait_for_record(r'\(5\) Thread: 100') is not None + ), 'last thread finished' -- cgit From 9d8d2c17585c149601855a09eba575640d156eae Mon Sep 17 00:00:00 2001 From: Igor Sysoev Date: Fri, 2 Oct 2020 15:16:09 +0300 Subject: Fixed comment. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to 洪志道 (Hong Zhi Dao). --- src/nxt_http_chunk_parse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nxt_http_chunk_parse.c b/src/nxt_http_chunk_parse.c index 2164524b..be3a2023 100644 --- a/src/nxt_http_chunk_parse.c +++ b/src/nxt_http_chunk_parse.c @@ -74,7 +74,7 @@ nxt_http_chunk_parse(nxt_task_t *task, nxt_http_chunk_parse_t *hcp, goto next; } - /* ret == NXT_HTTP_CHUNK_END_ON_BORDER */ + /* ret == NXT_HTTP_CHUNK_END */ } ch = *hcp->pos++; -- cgit From 152ad526f491e70905e2fec3b7d33a89cf23813b Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 5 Oct 2020 11:05:00 +0100 Subject: Tests: added ASGI Lifespan. --- test/python/lifespan/empty/asgi.py | 27 +++++++++++++ test/python/lifespan/error/asgi.py | 3 ++ test/python/lifespan/error_auto/asgi.py | 2 + test/python/lifespan/failed/asgi.py | 11 +++++ test/test_asgi_lifespan.py | 71 +++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 test/python/lifespan/empty/asgi.py create mode 100644 test/python/lifespan/error/asgi.py create mode 100644 test/python/lifespan/error_auto/asgi.py create mode 100644 test/python/lifespan/failed/asgi.py create mode 100644 test/test_asgi_lifespan.py diff --git a/test/python/lifespan/empty/asgi.py b/test/python/lifespan/empty/asgi.py new file mode 100644 index 00000000..ea43af13 --- /dev/null +++ b/test/python/lifespan/empty/asgi.py @@ -0,0 +1,27 @@ +import os + + +async def application(scope, receive, send): + if scope['type'] == 'lifespan': + with open('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') + await send({'type': 'lifespan.startup.complete'}) + elif message['type'] == 'lifespan.shutdown': + os.remove('shutdown') + await send({'type': 'lifespan.shutdown.complete'}) + return + + if scope['type'] == 'http': + await send( + { + 'type': 'http.response.start', + 'status': 204, + 'headers': [(b'content-length', b'0'),], + } + ) diff --git a/test/python/lifespan/error/asgi.py b/test/python/lifespan/error/asgi.py new file mode 100644 index 00000000..509cb3ee --- /dev/null +++ b/test/python/lifespan/error/asgi.py @@ -0,0 +1,3 @@ +async def application(scope, receive, send): + if scope['type'] != 'http': + raise Exception('Exception blah') diff --git a/test/python/lifespan/error_auto/asgi.py b/test/python/lifespan/error_auto/asgi.py new file mode 100644 index 00000000..90d506a1 --- /dev/null +++ b/test/python/lifespan/error_auto/asgi.py @@ -0,0 +1,2 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' diff --git a/test/python/lifespan/failed/asgi.py b/test/python/lifespan/failed/asgi.py new file mode 100644 index 00000000..8f315f70 --- /dev/null +++ b/test/python/lifespan/failed/asgi.py @@ -0,0 +1,11 @@ +async def application(scope, receive, send): + if scope['type'] == 'lifespan': + while True: + message = await receive() + if message['type'] == 'lifespan.startup': + await send({"type": "lifespan.startup.failed"}) + raise Exception('Exception blah') + + elif message['type'] == 'lifespan.shutdown': + await send({'type': 'lifespan.shutdown.complete'}) + return diff --git a/test/test_asgi_lifespan.py b/test/test_asgi_lifespan.py new file mode 100644 index 00000000..2fbc68f8 --- /dev/null +++ b/test/test_asgi_lifespan.py @@ -0,0 +1,71 @@ +import os +import pytest +from distutils.version import LooseVersion + +from unit.applications.lang.python import TestApplicationPython +from conftest import option + + +class TestASGILifespan(TestApplicationPython): + prerequisites = { + 'modules': {'python': lambda v: LooseVersion(v) >= LooseVersion('3.5')} + } + load_module = 'asgi' + + def test_asgi_lifespan(self): + self.load('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' + + open(startup_path, 'a').close() + open(shutdown_path, 'a').close() + open(version_path, 'a').close() + + assert self.get()['status'] == 204 + + self.stop() + + is_startup = os.path.isfile(startup_path) + is_shutdown = os.path.isfile(shutdown_path) + + if is_startup: + os.remove(startup_path) + + if is_shutdown: + os.remove(shutdown_path) + + with open(version_path, 'r') as f: + version = f.read() + + os.remove(version_path) + + assert not is_startup, 'startup' + assert not is_shutdown, 'shutdown' + assert version == '3.0 2.0', 'version' + + def test_asgi_lifespan_failed(self): + self.load('lifespan/failed') + + assert self.get()['status'] == 503 + + assert ( + self.wait_for_record(r'\[error\].*Application startup failed') + is not None + ), 'error message' + assert self.wait_for_record(r'Exception blah') is not None, 'exception' + + def test_asgi_lifespan_error(self): + self.load('lifespan/error') + + self.get() + + assert self.wait_for_record(r'Exception blah') is not None, 'exception' + + def test_asgi_lifespan_error_auto(self): + self.load('lifespan/error_auto') + + self.get() + + assert self.wait_for_record(r'AssertionError') is not None, 'assertion' -- cgit From 58cc73994f071fc5db3347a3956f8089cb8e4dd0 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 5 Oct 2020 11:05:19 +0100 Subject: Tests: added websocket test with long length. --- test/test_asgi_websockets.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_asgi_websockets.py b/test/test_asgi_websockets.py index 249f630f..cbd77109 100644 --- a/test/test_asgi_websockets.py +++ b/test/test_asgi_websockets.py @@ -128,6 +128,18 @@ class TestASGIWebsockets(TestApplicationPython): sock.close() + def test_asgi_websockets_length_long(self): + self.load('websockets/mirror') + + _, sock, _ = self.ws.upgrade() + + 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 + ) + + self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE + def test_asgi_websockets_frame_fragmentation_invalid(self): self.load('websockets/mirror') -- cgit From 481e950b8696740397eeda0d3c01a2dda2239143 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 5 Oct 2020 13:26:35 +0300 Subject: Tests: pretty versions output for multi-version tests. --- test/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 8e9009b6..0bc5cdf9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -74,11 +74,11 @@ def pytest_generate_tests(metafunc): def generate_tests(versions): metafunc.fixturenames.append('tmp_ct') - metafunc.parametrize('tmp_ct', range(len(versions))) + metafunc.parametrize('tmp_ct', versions) - for i, version in enumerate(versions): + for version in versions: option.generated_tests[ - metafunc.function.__name__ + '[{}]'.format(i) + metafunc.function.__name__ + '[{}]'.format(version) ] = (type + ' ' + version) # take available module from option and generate tests for each version -- cgit From 703d79042b8dd161e7c53d485b90af2b926148e6 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Tue, 6 Oct 2020 19:06:33 +0300 Subject: Removing a meaningless warning message. Data in the queue and the socket are transmitted independently; special READ_QUEUE and READ_SOCKET message types are used for synchronization. The warning was accidentally committed with changeset 1d84b9e4b459. --- src/nxt_unit.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/nxt_unit.c b/src/nxt_unit.c index 4e468124..f75d61bc 100644 --- a/src/nxt_unit.c +++ b/src/nxt_unit.c @@ -5764,10 +5764,6 @@ retry: nxt_unit_debug(ctx, "port{%d,%d} recv %d read_queue", (int) port->id.pid, (int) port->id.id, (int) rbuf->size); - if (port_impl->from_socket) { - nxt_unit_warn(ctx, "port protocol warning: READ_QUEUE after READ_SOCKET"); - } - goto retry; } -- cgit From 37390d2a3be3646ad5a4c52d46ce93fc8f8a416b Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 6 Oct 2020 20:30:51 +0100 Subject: Tests: fixed tests to run as root. --- test/test_asgi_lifespan.py | 4 +++- test/test_ruby_isolation.py | 8 +------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/test_asgi_lifespan.py b/test/test_asgi_lifespan.py index 2fbc68f8..3d6fdcce 100644 --- a/test/test_asgi_lifespan.py +++ b/test/test_asgi_lifespan.py @@ -3,7 +3,7 @@ import pytest from distutils.version import LooseVersion from unit.applications.lang.python import TestApplicationPython -from conftest import option +from conftest import option, public_dir class TestASGILifespan(TestApplicationPython): @@ -23,6 +23,8 @@ class TestASGILifespan(TestApplicationPython): open(shutdown_path, 'a').close() open(version_path, 'a').close() + public_dir(option.test_dir + '/python/lifespan/empty') + assert self.get()['status'] == 204 self.stop() diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index be20300e..e88c60b2 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -33,15 +33,9 @@ class TestRubyIsolation(TestApplicationRuby): if not 'unprivileged_userns_clone' in isolation_features: pytest.skip('requires unprivileged userns or root') - os.mkdir(self.temp_dir + '/ruby') - - shutil.copytree( - option.test_dir + '/ruby/status_int', - self.temp_dir + '/ruby/status_int', - ) isolation = { 'namespaces': {'credential': not is_su, 'mount': True}, - 'rootfs': self.temp_dir, + 'rootfs': option.test_dir, } self.load('status_int', isolation=isolation) -- cgit From 3f513f434fbe44810ea2352d4ffc7d4d702b3e12 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Wed, 7 Oct 2020 20:06:30 +0300 Subject: Router: fixed "not empty" pattern matching. The "!" pattern should be opposite to "", i.e. match only non-empty values. But after 3c00af54b937 it was equal to "!*", which is wrong. --- src/nxt_http_route.c | 4 ---- test/test_routing.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/nxt_http_route.c b/src/nxt_http_route.c index 36e003ae..ae91076a 100644 --- a/src/nxt_http_route.c +++ b/src/nxt_http_route.c @@ -1085,10 +1085,6 @@ nxt_http_route_pattern_create(nxt_task_t *task, nxt_mp_t *mp, pattern->negative = 1; pattern->any = 0; - - if (test.length == 0) { - return NXT_OK; - } } if (test.length == 0) { diff --git a/test/test_routing.py b/test/test_routing.py index 734825ef..32a7fbc8 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -118,6 +118,9 @@ class TestRouting(TestApplicationProto): def test_routes_match_negative(self): self.route_match({"uri": "!"}) + assert self.get()['status'] == 200 + + self.route_match({"uri": "!*"}) assert self.get()['status'] == 404 self.route_match({"uri": "!/"}) @@ -1187,6 +1190,18 @@ class TestRouting(TestApplicationProto): assert self.get(url='/?foo=barxx&x%=%')['status'] == 404 def test_routes_match_arguments_negative(self): + self.route_match({"arguments": {"foo": "!"}}) + assert self.get(url='/?bar')['status'] == 404 + assert self.get(url='/?foo')['status'] == 404 + assert self.get(url='/?foo=')['status'] == 404 + assert self.get(url='/?foo=%25')['status'] == 200 + + self.route_match({"arguments": {"foo": "!*"}}) + assert self.get(url='/?bar')['status'] == 404 + assert self.get(url='/?foo')['status'] == 404 + assert self.get(url='/?foo=')['status'] == 404 + assert self.get(url='/?foo=blah')['status'] == 404 + self.route_match({"arguments": {"foo": "!%25"}}) assert self.get(url='/?foo=blah')['status'] == 200 assert self.get(url='/?foo=%')['status'] == 404 -- cgit From 645683f432d742f42f2769f589fe769585fb7aa0 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Wed, 7 Oct 2020 22:04:54 +0300 Subject: Tests: fixed loading selected module version. Previously, for PHP, Ruby, and Perl the latest version was always loaded in multi-version tests. --- test/unit/applications/lang/perl.py | 6 +++++- test/unit/applications/lang/php.py | 6 +++++- test/unit/applications/lang/ruby.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/test/unit/applications/lang/perl.py b/test/unit/applications/lang/perl.py index 92939d88..d686cc8a 100644 --- a/test/unit/applications/lang/perl.py +++ b/test/unit/applications/lang/perl.py @@ -7,13 +7,17 @@ class TestApplicationPerl(TestApplicationProto): def load(self, script, name='psgi.pl', **kwargs): script_path = option.test_dir + '/perl/' + script + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "working_directory": script_path, "script": script_path + '/' + name, diff --git a/test/unit/applications/lang/php.py b/test/unit/applications/lang/php.py index 350eb29b..6ac3d710 100644 --- a/test/unit/applications/lang/php.py +++ b/test/unit/applications/lang/php.py @@ -7,13 +7,17 @@ class TestApplicationPHP(TestApplicationProto): def load(self, script, index='index.php', **kwargs): script_path = option.test_dir + '/php/' + script + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "root": script_path, "working_directory": script_path, diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index 534227bd..a6c39b2a 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -7,13 +7,17 @@ class TestApplicationRuby(TestApplicationProto): def load(self, script, name='config.ru', **kwargs): script_path = option.test_dir + '/ruby/' + script + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "working_directory": script_path, "script": script_path + '/' + name, -- cgit From 6ec0ff35964c7805712d978625949f72ff5a63bc Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Wed, 7 Oct 2020 23:18:43 +0100 Subject: Tests: minor fixes. --- test/conftest.py | 5 +++-- test/test_access_log.py | 3 ++- test/test_asgi_application.py | 29 ++++++++++++++--------------- test/test_asgi_lifespan.py | 15 +++++++++++---- test/test_asgi_websockets.py | 6 ++++-- test/test_configuration.py | 8 ++++---- test/test_go_application.py | 3 ++- test/test_go_isolation.py | 1 + test/test_go_isolation_rootfs.py | 1 + test/test_http_header.py | 4 ++-- test/test_java_application.py | 4 +++- test/test_java_isolation_rootfs.py | 3 ++- test/test_java_websockets.py | 6 ++++-- test/test_node_application.py | 9 +++++---- test/test_node_websockets.py | 6 ++++-- test/test_perl_application.py | 5 +++-- test/test_php_application.py | 5 +++-- test/test_php_isolation.py | 2 +- test/test_php_targets.py | 2 +- test/test_proxy.py | 8 +++++--- test/test_proxy_chunked.py | 2 +- test/test_python_application.py | 5 +++-- test/test_python_isolation.py | 1 - test/test_python_procman.py | 3 ++- test/test_respawn.py | 2 +- test/test_routing.py | 3 ++- test/test_ruby_application.py | 5 +++-- test/test_ruby_isolation.py | 5 ++--- test/test_settings.py | 5 +++-- test/test_share_fallback.py | 2 +- test/test_static.py | 5 +++-- test/test_tls.py | 7 ++++--- test/test_upstreams_rr.py | 2 +- test/test_usr1.py | 2 +- test/unit/applications/lang/go.py | 2 +- test/unit/applications/lang/java.py | 4 ++-- test/unit/applications/lang/node.py | 4 ++-- test/unit/applications/lang/perl.py | 2 +- test/unit/applications/lang/php.py | 2 +- test/unit/applications/lang/python.py | 6 +++--- test/unit/applications/lang/ruby.py | 2 +- test/unit/applications/proto.py | 2 +- test/unit/applications/tls.py | 3 +-- test/unit/applications/websockets.py | 3 +-- test/unit/http.py | 4 ++-- test/unit/main.py | 12 +++++++----- 46 files changed, 125 insertions(+), 95 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 0bc5cdf9..b62264ca 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,16 +1,17 @@ import fcntl import os import platform -import pytest +import re import shutil import signal import stat import subprocess import sys -import re import tempfile import time +import pytest + from unit.check.go import check_go from unit.check.node import check_node from unit.check.tls import check_openssl diff --git a/test/test_access_log.py b/test/test_access_log.py index 010c56c2..eaba82ab 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -1,6 +1,7 @@ -import pytest import time +import pytest + from unit.applications.lang.python import TestApplicationPython diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py index 7816caec..948d9823 100644 --- a/test/test_asgi_application.py +++ b/test/test_asgi_application.py @@ -1,12 +1,11 @@ -import grp -import pytest -import pwd import re import time from distutils.version import LooseVersion -from unit.applications.lang.python import TestApplicationPython +import pytest + from conftest import skip_alert +from unit.applications.lang.python import TestApplicationPython class TestASGIApplication(TestApplicationPython): @@ -18,7 +17,7 @@ class TestASGIApplication(TestApplicationPython): with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) - def test_asgi_application__variables(self): + def test_asgi_application_variables(self): self.load('variables') body = 'Test body string.' @@ -63,7 +62,7 @@ custom-header: BLAH }, 'headers' assert resp['body'] == body, 'body' - def test_asgi_application__query_string(self): + def test_asgi_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') @@ -72,7 +71,7 @@ custom-header: BLAH resp['headers']['query-string'] == 'var1=val1&var2=val2' ), 'query-string header' - def test_asgi_application__query_string_space(self): + def test_asgi_application_query_string_space(self): self.load('query_string') resp = self.get(url='/ ?var1=val1&var2=val2') @@ -95,7 +94,7 @@ custom-header: BLAH resp['headers']['query-string'] == ' var1= val1 & var2=val2' ), 'query-string space 4' - def test_asgi_application__query_string_empty(self): + def test_asgi_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') @@ -103,7 +102,7 @@ custom-header: BLAH assert resp['status'] == 200, 'query string empty status' assert resp['headers']['query-string'] == '', 'query string empty' - def test_asgi_application__query_string_absent(self): + def test_asgi_application_query_string_absent(self): self.load('query_string') resp = self.get() @@ -112,7 +111,7 @@ custom-header: BLAH assert resp['headers']['query-string'] == '', 'query string absent' @pytest.mark.skip('not yet') - def test_asgi_application__server_port(self): + def test_asgi_application_server_port(self): self.load('server_port') assert ( @@ -120,7 +119,7 @@ custom-header: BLAH ), 'Server-Port header' @pytest.mark.skip('not yet') - def test_asgi_application__working_directory_invalid(self): + def test_asgi_application_working_directory_invalid(self): self.load('empty') assert 'success' in self.conf( @@ -129,14 +128,14 @@ custom-header: BLAH assert self.get()['status'] == 500, 'status' - def test_asgi_application__204_transfer_encoding(self): + def test_asgi_application_204_transfer_encoding(self): self.load('204_no_content') assert ( 'Transfer-Encoding' not in self.get()['headers'] ), '204 header transfer encoding' - def test_asgi_application__shm_ack_handle(self): + def test_asgi_application_shm_ack_handle(self): self.load('mirror') # Minimum possible limit @@ -379,7 +378,7 @@ Connection: close self.get(headers=headers_delay_1) - def test_asgi_application__loading_error(self): + def test_asgi_application_loading_error(self): skip_alert(r'Python failed to import module "blah"') self.load('empty') @@ -388,7 +387,7 @@ Connection: close assert self.get()['status'] == 503, 'loading error' - def test_asgi_application__threading(self): + def test_asgi_application_threading(self): """wait_for_record() timeouts after 5s while every thread works at least 3s. So without releasing GIL test should fail. """ diff --git a/test/test_asgi_lifespan.py b/test/test_asgi_lifespan.py index 3d6fdcce..c37a1aae 100644 --- a/test/test_asgi_lifespan.py +++ b/test/test_asgi_lifespan.py @@ -1,9 +1,11 @@ import os -import pytest from distutils.version import LooseVersion +import pytest + +from conftest import option +from conftest import public_dir from unit.applications.lang.python import TestApplicationPython -from conftest import option, public_dir class TestASGILifespan(TestApplicationPython): @@ -19,11 +21,16 @@ class TestASGILifespan(TestApplicationPython): shutdown_path = option.test_dir + '/python/lifespan/empty/shutdown' version_path = option.test_dir + '/python/lifespan/empty/version' + os.chmod(option.test_dir + '/python/lifespan/empty', 0o777) + open(startup_path, 'a').close() + os.chmod(startup_path, 0o777) + open(shutdown_path, 'a').close() - open(version_path, 'a').close() + os.chmod(shutdown_path, 0o777) - public_dir(option.test_dir + '/python/lifespan/empty') + open(version_path, 'a').close() + os.chmod(version_path, 0o777) assert self.get()['status'] == 204 diff --git a/test/test_asgi_websockets.py b/test/test_asgi_websockets.py index cbd77109..ab49b130 100644 --- a/test/test_asgi_websockets.py +++ b/test/test_asgi_websockets.py @@ -1,11 +1,13 @@ -import pytest import struct import time from distutils.version import LooseVersion +import pytest + +from conftest import option +from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython from unit.applications.websockets import TestApplicationWebsocket -from conftest import option, skip_alert class TestASGIWebsockets(TestApplicationPython): diff --git a/test/test_configuration.py b/test/test_configuration.py index 07b8d522..d1e6f000 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,7 +1,7 @@ import pytest -from unit.control import TestControl from conftest import skip_alert +from unit.control import TestControl class TestConfiguration(TestControl): @@ -15,7 +15,7 @@ class TestConfiguration(TestControl): def test_json_unicode(self): assert 'success' in self.conf( - b""" + u""" { "ap\u0070": { "type": "\u0070ython", @@ -53,8 +53,8 @@ class TestConfiguration(TestControl): assert 'приложение' in self.conf_get('applications'), 'unicode 2 get' def test_json_unicode_number(self): - assert 'error' in self.conf( - b""" + assert 'success' in self.conf( + u""" { "app": { "type": "python", diff --git a/test/test_go_application.py b/test/test_go_application.py index 828a8e6e..8c77dfc5 100644 --- a/test/test_go_application.py +++ b/test/test_go_application.py @@ -1,6 +1,7 @@ -from unit.applications.lang.go import TestApplicationGo import re +from unit.applications.lang.go import TestApplicationGo + class TestGoApplication(TestApplicationGo): prerequisites = {'modules': {'go': 'all'}} diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py index bcfdd015..1e7243f6 100644 --- a/test/test_go_isolation.py +++ b/test/test_go_isolation.py @@ -1,6 +1,7 @@ import grp import os import pwd + import pytest from unit.applications.lang.go import TestApplicationGo diff --git a/test/test_go_isolation_rootfs.py b/test/test_go_isolation_rootfs.py index 68891cd6..d8e177b1 100644 --- a/test/test_go_isolation_rootfs.py +++ b/test/test_go_isolation_rootfs.py @@ -1,4 +1,5 @@ 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 8c1e211b..8381a0d9 100644 --- a/test/test_http_header.py +++ b/test/test_http_header.py @@ -108,7 +108,7 @@ class TestHTTPHeader(TestApplicationPython): resp = self.get( headers={ 'Host': 'localhost', - 'Custom-Header': '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~', + 'Custom-Header': r'(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~', 'Connection': 'close', } ) @@ -116,7 +116,7 @@ class TestHTTPHeader(TestApplicationPython): assert resp['status'] == 200, 'value chars status' assert ( resp['headers']['Custom-Header'] - == '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' + == r'(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' ), 'value chars custom header' def test_http_header_value_chars_edge(self): diff --git a/test/test_java_application.py b/test/test_java_application.py index a2bd3d44..afcdf651 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -3,8 +3,10 @@ import os import re import time +from conftest import option +from conftest import public_dir +from conftest import skip_alert from unit.applications.lang.java import TestApplicationJava -from conftest import option, public_dir, skip_alert class TestJavaApplication(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py index df3ccdfa..f0f04df1 100644 --- a/test/test_java_isolation_rootfs.py +++ b/test/test_java_isolation_rootfs.py @@ -1,9 +1,10 @@ import os import subprocess + import pytest -from unit.applications.lang.java import TestApplicationJava from conftest import option +from unit.applications.lang.java import TestApplicationJava class TestJavaIsolationRootfs(TestApplicationJava): diff --git a/test/test_java_websockets.py b/test/test_java_websockets.py index 1bbefa1e..7e6d82e8 100644 --- a/test/test_java_websockets.py +++ b/test/test_java_websockets.py @@ -1,10 +1,12 @@ -import pytest import struct import time +import pytest + +from conftest import option +from conftest import skip_alert from unit.applications.lang.java import TestApplicationJava from unit.applications.websockets import TestApplicationWebsocket -from conftest import option, skip_alert class TestJavaWebsockets(TestApplicationJava): diff --git a/test/test_node_application.py b/test/test_node_application.py index c2b0ed69..a0b882f3 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -1,8 +1,9 @@ -import pytest import re -from unit.applications.lang.node import TestApplicationNode +import pytest + from conftest import waitforfiles +from unit.applications.lang.node import TestApplicationNode class TestNodeApplication(TestApplicationNode): @@ -50,8 +51,8 @@ class TestNodeApplication(TestApplicationNode): raw_headers = headers.pop('Request-Raw-Headers') assert re.search( r'^(?:Host|localhost|Content-Type|' - 'text\/html|Custom-Header|blah|Content-Length|17|Connection|' - 'close|,)+$', + r'text\/html|Custom-Header|blah|Content-Length|17|Connection|' + r'close|,)+$', raw_headers, ), 'raw headers' diff --git a/test/test_node_websockets.py b/test/test_node_websockets.py index 5af2f6f3..6a6b7f2d 100644 --- a/test/test_node_websockets.py +++ b/test/test_node_websockets.py @@ -1,10 +1,12 @@ -import pytest import struct import time +import pytest + +from conftest import option +from conftest import skip_alert from unit.applications.lang.node import TestApplicationNode from unit.applications.websockets import TestApplicationWebsocket -from conftest import option, skip_alert class TestNodeWebsockets(TestApplicationNode): diff --git a/test/test_perl_application.py b/test/test_perl_application.py index bb63eceb..78e32a43 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -1,8 +1,9 @@ -import pytest import re -from unit.applications.lang.perl import TestApplicationPerl +import pytest + from conftest import skip_alert +from unit.applications.lang.perl import TestApplicationPerl class TestPerlApplication(TestApplicationPerl): diff --git a/test/test_php_application.py b/test/test_php_application.py index 18a5c085..48fab5ee 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -1,11 +1,12 @@ import os -import pytest import re import shutil import time -from unit.applications.lang.php import TestApplicationPHP +import pytest + from conftest import option +from unit.applications.lang.php import TestApplicationPHP class TestPHPApplication(TestApplicationPHP): prerequisites = {'modules': {'php': 'all'}} diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index 556bd387..8ab3419a 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -1,8 +1,8 @@ import pytest +from conftest import option from unit.applications.lang.php import TestApplicationPHP from unit.feature.isolation import TestFeatureIsolation -from conftest import option class TestPHPIsolation(TestApplicationPHP): diff --git a/test/test_php_targets.py b/test/test_php_targets.py index 2eadf071..e64cd6b6 100644 --- a/test/test_php_targets.py +++ b/test/test_php_targets.py @@ -1,5 +1,5 @@ -from unit.applications.lang.php import TestApplicationPHP from conftest import option +from unit.applications.lang.php import TestApplicationPHP class TestPHPTargets(TestApplicationPHP): diff --git a/test/test_proxy.py b/test/test_proxy.py index 1476670c..d02c96a7 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -1,10 +1,12 @@ -import pytest import re import socket import time +import pytest + +from conftest import option +from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython -from conftest import option, skip_alert class TestProxy(TestApplicationPython): @@ -215,7 +217,7 @@ Content-Length: 10 == header_value ), 'custom header' - header_value = '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' + header_value = r'(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} diff --git a/test/test_proxy_chunked.py b/test/test_proxy_chunked.py index 93746703..26023617 100644 --- a/test/test_proxy_chunked.py +++ b/test/test_proxy_chunked.py @@ -51,7 +51,7 @@ class TestProxyChunked(TestApplicationPython): for line in re.split('\r\n', body): add = '' - m1 = re.search('(.*)\sX\s(\d+)', line) + m1 = re.search(r'(.*)\sX\s(\d+)', line) if m1 is not None: add = m1.group(1) * int(m1.group(2)) diff --git a/test/test_python_application.py b/test/test_python_application.py index 434ceb19..3e27a24c 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -1,11 +1,12 @@ import grp -import pytest import pwd import re import time -from unit.applications.lang.python import TestApplicationPython +import pytest + from conftest import skip_alert +from unit.applications.lang.python import TestApplicationPython class TestPythonApplication(TestApplicationPython): diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 59ac670a..ac678103 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -94,4 +94,3 @@ class TestPythonIsolation(TestApplicationPython): self.load('empty', isolation=isolation) assert (self.get()['status'] == 200), 'enabled language_deps' - diff --git a/test/test_python_procman.py b/test/test_python_procman.py index 7e727fa8..8eccae3e 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -1,8 +1,9 @@ -import pytest import re import subprocess import time +import pytest + from unit.applications.lang.python import TestApplicationPython diff --git a/test/test_respawn.py b/test/test_respawn.py index d40e78a4..18b9d535 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -2,8 +2,8 @@ import re import subprocess import time -from unit.applications.lang.python import TestApplicationPython from conftest import skip_alert +from unit.applications.lang.python import TestApplicationPython class TestRespawn(TestApplicationPython): diff --git a/test/test_routing.py b/test/test_routing.py index 32a7fbc8..2b528435 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import pytest +from conftest import option +from conftest import skip_alert from unit.applications.proto import TestApplicationProto -from conftest import option, skip_alert class TestRouting(TestApplicationProto): diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index bdd1afb9..f84935f8 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -1,8 +1,9 @@ -import pytest import re -from unit.applications.lang.ruby import TestApplicationRuby +import pytest + from conftest import skip_alert +from unit.applications.lang.ruby import TestApplicationRuby class TestRubyApplication(TestApplicationRuby): diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index e88c60b2..13ca0e16 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -1,10 +1,9 @@ -import os + import pytest -import shutil +from conftest import option from unit.applications.lang.ruby import TestApplicationRuby from unit.feature.isolation import TestFeatureIsolation -from conftest import option class TestRubyIsolation(TestApplicationRuby): diff --git a/test/test_settings.py b/test/test_settings.py index 89f55703..b0af6b04 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -1,9 +1,10 @@ -import pytest +import re import socket import time +import pytest + from unit.applications.lang.python import TestApplicationPython -import re class TestSettings(TestApplicationPython): diff --git a/test/test_share_fallback.py b/test/test_share_fallback.py index 391066ec..391d0836 100644 --- a/test/test_share_fallback.py +++ b/test/test_share_fallback.py @@ -1,7 +1,7 @@ import os -from unit.applications.proto import TestApplicationProto from conftest import skip_alert +from unit.applications.proto import TestApplicationProto class TestStatic(TestApplicationProto): diff --git a/test/test_static.py b/test/test_static.py index 2290bc38..0b82b4e8 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -1,9 +1,10 @@ import os -import pytest import socket -from unit.applications.proto import TestApplicationProto +import pytest + from conftest import waitforfiles +from unit.applications.proto import TestApplicationProto class TestStatic(TestApplicationProto): diff --git a/test/test_tls.py b/test/test_tls.py index 9881e973..518a834c 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -1,11 +1,12 @@ import io -import pytest import re import ssl import subprocess -from unit.applications.tls import TestApplicationTLS +import pytest + from conftest import skip_alert +from unit.applications.tls import TestApplicationTLS class TestTLS(TestApplicationTLS): @@ -527,7 +528,7 @@ basicConstraints = critical,CA:TRUE""" self.wait_for_record( re.compile( - ' (?!' + app_id + '#)(\d+)#\d+ "mirror" application started' + r' (?!' + app_id + r'#)(\d+)#\d+ "mirror" application started' ) ) diff --git a/test/test_upstreams_rr.py b/test/test_upstreams_rr.py index ceab11c3..2ecf1d9a 100644 --- a/test/test_upstreams_rr.py +++ b/test/test_upstreams_rr.py @@ -1,8 +1,8 @@ import os import re -from unit.applications.lang.python import TestApplicationPython from conftest import option +from unit.applications.lang.python import TestApplicationPython class TestUpstreamsRR(TestApplicationPython): diff --git a/test/test_usr1.py b/test/test_usr1.py index 19081223..2e48c18f 100644 --- a/test/test_usr1.py +++ b/test/test_usr1.py @@ -1,8 +1,8 @@ import os from subprocess import call -from unit.applications.lang.python import TestApplicationPython from conftest import waitforfiles +from unit.applications.lang.python import TestApplicationPython class TestUSR1(TestApplicationPython): diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 946b5421..7715bd6c 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -1,8 +1,8 @@ import os import subprocess -from unit.applications.proto import TestApplicationProto from conftest import option +from unit.applications.proto import TestApplicationProto class TestApplicationGo(TestApplicationProto): diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index 93427709..01cbfa0b 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -1,11 +1,11 @@ import glob import os -import pytest import shutil import subprocess -from unit.applications.proto import TestApplicationProto +import pytest from conftest import option +from unit.applications.proto import TestApplicationProto class TestApplicationJava(TestApplicationProto): diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index dbb7036b..877fc461 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -1,9 +1,9 @@ -import os import shutil from urllib.parse import quote +from conftest import option +from conftest import public_dir from unit.applications.proto import TestApplicationProto -from conftest import option, public_dir class TestApplicationNode(TestApplicationProto): diff --git a/test/unit/applications/lang/perl.py b/test/unit/applications/lang/perl.py index d686cc8a..a27c7649 100644 --- a/test/unit/applications/lang/perl.py +++ b/test/unit/applications/lang/perl.py @@ -1,5 +1,5 @@ -from unit.applications.proto import TestApplicationProto from conftest import option +from unit.applications.proto import TestApplicationProto class TestApplicationPerl(TestApplicationProto): diff --git a/test/unit/applications/lang/php.py b/test/unit/applications/lang/php.py index 6ac3d710..2d50df2e 100644 --- a/test/unit/applications/lang/php.py +++ b/test/unit/applications/lang/php.py @@ -1,5 +1,5 @@ -from unit.applications.proto import TestApplicationProto from conftest import option +from unit.applications.proto import TestApplicationProto class TestApplicationPHP(TestApplicationProto): diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index 75d34722..47b95dac 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -1,10 +1,10 @@ import os import shutil -import pytest - -from unit.applications.proto import TestApplicationProto from urllib.parse import quote + +import pytest from conftest import option +from unit.applications.proto import TestApplicationProto class TestApplicationPython(TestApplicationProto): diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index a6c39b2a..bc3cefc6 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -1,5 +1,5 @@ -from unit.applications.proto import TestApplicationProto from conftest import option +from unit.applications.proto import TestApplicationProto class TestApplicationRuby(TestApplicationProto): diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py index 7bf7e244..2f748c21 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -2,8 +2,8 @@ import os import re import time -from unit.control import TestControl from conftest import option +from unit.control import TestControl class TestApplicationProto(TestControl): diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index 7c95f27b..fdf681ae 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -1,10 +1,9 @@ import os -import re import ssl import subprocess -from unit.applications.proto import TestApplicationProto from conftest import option +from unit.applications.proto import TestApplicationProto class TestApplicationTLS(TestApplicationProto): diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index bae40620..cc720a98 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -1,12 +1,11 @@ import base64 import hashlib import itertools -import pytest import random -import re import select import struct +import pytest from unit.applications.proto import TestApplicationProto GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" diff --git a/test/unit/http.py b/test/unit/http.py index e5231b3d..7845f9a8 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -2,14 +2,14 @@ import binascii import io import json import os -import pytest import re import select import socket import time -from unit.main import TestUnit +import pytest from conftest import option +from unit.main import TestUnit class TestHTTP(TestUnit): diff --git a/test/unit/main.py b/test/unit/main.py index 053ce145..d5940995 100644 --- a/test/unit/main.py +++ b/test/unit/main.py @@ -1,19 +1,21 @@ -import argparse import atexit import os -import platform -import pytest import re import shutil import signal import stat import subprocess -import sys import tempfile import time -from conftest import option, public_dir, waitforfiles, _check_alerts, _print_log from multiprocessing import Process +import pytest +from conftest import _check_alerts +from conftest import _print_log +from conftest import option +from conftest import public_dir +from conftest import waitforfiles + class TestUnit(): @classmethod -- cgit From 2821b3347c026ae171228146c5ccd0bd7408358f Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Tue, 6 Oct 2020 18:12:05 +0300 Subject: PHP: compatibility with 8.0.0 RC1. This closes #474 PR on GitHub. --- src/nxt_php_sapi.c | 30 ++++++++++++++++++++++++++++++ test/php/get_variables/index.php | 6 +++--- test/php/post_variables/index.php | 4 ++-- test/test_php_application.py | 10 +++++----- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/nxt_php_sapi.c b/src/nxt_php_sapi.c index de329ad7..234ceef8 100644 --- a/src/nxt_php_sapi.c +++ b/src/nxt_php_sapi.c @@ -88,6 +88,9 @@ static void nxt_php_set_options(nxt_task_t *task, nxt_conf_value_t *options, int type); static nxt_int_t nxt_php_alter_option(nxt_str_t *name, nxt_str_t *value, int type); +#ifdef NXT_PHP8 +static void nxt_php_disable_functions(nxt_str_t *str); +#endif static void nxt_php_disable(nxt_task_t *task, const char *type, nxt_str_t *value, char **ptr, nxt_php_disable_t disable); @@ -589,9 +592,13 @@ nxt_php_set_options(nxt_task_t *task, nxt_conf_value_t *options, int type) } if (nxt_str_eq(&name, "disable_functions", 17)) { +#ifdef NXT_PHP8 + nxt_php_disable_functions(&value); +#else nxt_php_disable(task, "function", &value, &PG(disable_functions), zend_disable_function); +#endif continue; } @@ -680,6 +687,29 @@ nxt_php_alter_option(nxt_str_t *name, nxt_str_t *value, int type) #endif +#ifdef NXT_PHP8 + +static void +nxt_php_disable_functions(nxt_str_t *str) +{ + char *p; + + p = nxt_malloc(str->length + 1); + if (nxt_slow_path(p == NULL)) { + return; + } + + nxt_memcpy(p, str->start, str->length); + p[str->length] = '\0'; + + zend_disable_functions(p); + + nxt_free(p); +} + +#endif + + static void nxt_php_disable(nxt_task_t *task, const char *type, nxt_str_t *value, char **ptr, nxt_php_disable_t disable) diff --git a/test/php/get_variables/index.php b/test/php/get_variables/index.php index dd7ef985..d6eb7d6b 100644 --- a/test/php/get_variables/index.php +++ b/test/php/get_variables/index.php @@ -1,7 +1,7 @@ diff --git a/test/php/post_variables/index.php b/test/php/post_variables/index.php index 5ea17324..8981d54d 100644 --- a/test/php/post_variables/index.php +++ b/test/php/post_variables/index.php @@ -1,6 +1,6 @@ diff --git a/test/test_php_application.py b/test/test_php_application.py index 48fab5ee..063d3e0c 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -198,9 +198,9 @@ class TestPHPApplication(TestApplicationPHP): resp = self.get(url='/?var1=val1&var2=&var3') assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' - assert resp['headers']['X-Var-2'] == '1', 'GET variables 2' - assert resp['headers']['X-Var-3'] == '1', 'GET variables 3' - assert resp['headers']['X-Var-4'] == '', 'GET variables 4' + assert resp['headers']['X-Var-2'] == '', 'GET variables 2' + assert resp['headers']['X-Var-3'] == '', 'GET variables 3' + assert resp['headers']['X-Var-4'] == 'not set', 'GET variables 4' def test_php_application_post_variables(self): self.load('post_variables') @@ -214,8 +214,8 @@ class TestPHPApplication(TestApplicationPHP): body='var1=val1&var2=', ) assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' - assert resp['headers']['X-Var-2'] == '1', 'POST variables 2' - assert resp['headers']['X-Var-3'] == '', 'POST variables 3' + assert resp['headers']['X-Var-2'] == '', 'POST variables 2' + assert resp['headers']['X-Var-3'] == 'not set', 'POST variables 3' def test_php_application_cookies(self): self.load('cookies') -- cgit From e0f66cbba095d4a4739b414a8878b567dc930b01 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov Date: Tue, 22 Sep 2020 15:55:28 +0300 Subject: Added jsc11 docker image. --- pkg/docker/Dockerfile.tmpl | 2 ++ pkg/docker/Makefile | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/docker/Dockerfile.tmpl b/pkg/docker/Dockerfile.tmpl index 3372ef6f..8b0e35e4 100644 --- a/pkg/docker/Dockerfile.tmpl +++ b/pkg/docker/Dockerfile.tmpl @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages=@@UNITPACKAGES@@ \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Makefile b/pkg/docker/Makefile index 7647e51b..d18e2b18 100644 --- a/pkg/docker/Makefile +++ b/pkg/docker/Makefile @@ -12,7 +12,7 @@ CODENAME := buster UNIT_VERSION = $(VERSION)-$(RELEASE)~$(CODENAME) MODULES = python2.7 python3.7 php7.3 go1.11-dev perl5.28 ruby2.5 \ - full minimal + jsc11 full minimal MODULE_php7.3="unit=$${UNIT_VERSION} unit-php=$${UNIT_VERSION}" @@ -26,7 +26,9 @@ MODULE_perl5.28="unit=$${UNIT_VERSION} unit-perl=$${UNIT_VERSION}" MODULE_ruby2.5="unit=$${UNIT_VERSION} unit-ruby=$${UNIT_VERSION}" -MODULE_full="unit=$${UNIT_VERSION} unit-php=$${UNIT_VERSION} unit-python2.7=$${UNIT_VERSION} unit-python3.7=$${UNIT_VERSION} unit-perl=$${UNIT_VERSION} unit-ruby=$${UNIT_VERSION}" +MODULE_jsc11="unit=$${UNIT_VERSION} unit-jsc11=$${UNIT_VERSION}" + +MODULE_full="unit=$${UNIT_VERSION} unit-php=$${UNIT_VERSION} unit-python2.7=$${UNIT_VERSION} unit-python3.7=$${UNIT_VERSION} unit-perl=$${UNIT_VERSION} unit-ruby=$${UNIT_VERSION} unit-jsc11=$${UNIT_VERSION}" MODULE_minimal="unit=$${UNIT_VERSION}" -- cgit From 5a8adce8c2be663ad2e6bb0a48ee8dc908ab4daa Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov Date: Tue, 22 Sep 2020 15:55:28 +0300 Subject: Ensure docker images are using latest versions of base OS packages. --- pkg/docker/Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/docker/Makefile b/pkg/docker/Makefile index d18e2b18..aed5b8f7 100644 --- a/pkg/docker/Makefile +++ b/pkg/docker/Makefile @@ -38,7 +38,7 @@ default: @echo "valid targets: all build dockerfiles push tag export clean" dockerfiles: $(addprefix Dockerfile., $(MODULES)) -build: $(addprefix build-,$(MODULES)) +build: refresh-base $(addprefix build-,$(MODULES)) tag: $(addprefix tag-,$(MODULES)) push: $(addprefix push-,$(MODULES)) latest export: $(addsuffix .tar.gz,$(addprefix $(EXPORT_DIR)/nginx-unit-$(VERSION)-,$(MODULES))) $(addsuffix .tar.gz.sha512, $(addprefix $(EXPORT_DIR)/nginx-unit-$(VERSION)-,$(MODULES))) @@ -51,7 +51,7 @@ Dockerfile.%: ../../version > $@ build-%: Dockerfile.% - docker build -t unit:$(VERSION)-$* -f Dockerfile.$* . + docker build --no-cache -t unit:$(VERSION)-$* -f Dockerfile.$* . tag-%: build-% docker tag unit:$(VERSION)-$* nginx/unit:$(VERSION)-$* @@ -63,6 +63,9 @@ latest: docker tag nginx/unit:$(VERSION)-full nginx/unit:latest docker push nginx/unit:latest +refresh-base: + docker pull $(shell head -n 1 Dockerfile.tmpl | cut -d' ' -f 2) + $(EXPORT_DIR): mkdir -p $@ @@ -78,4 +81,4 @@ clean: rm -f $(addprefix Dockerfile., $(MODULES)) rm -rf $(EXPORT_DIR) -.PHONY: default all build dockerfiles latest push tag export clean +.PHONY: default all build dockerfiles latest push tag export clean refresh-base -- cgit From bebc3746ca7f950341c4e495994c6f915c296393 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov Date: Tue, 22 Sep 2020 15:55:28 +0300 Subject: Regenerated dockerfiles. --- pkg/docker/Dockerfile.full | 4 +- pkg/docker/Dockerfile.go1.11-dev | 2 + pkg/docker/Dockerfile.jsc11 | 95 ++++++++++++++++++++++++++++++++++++++++ pkg/docker/Dockerfile.minimal | 2 + pkg/docker/Dockerfile.perl5.28 | 2 + pkg/docker/Dockerfile.php7.3 | 2 + pkg/docker/Dockerfile.python2.7 | 2 + pkg/docker/Dockerfile.python3.7 | 2 + pkg/docker/Dockerfile.ruby2.5 | 2 + 9 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 pkg/docker/Dockerfile.jsc11 diff --git a/pkg/docker/Dockerfile.full b/pkg/docker/Dockerfile.full index fc100635..27f9298d 100644 --- a/pkg/docker/Dockerfile.full +++ b/pkg/docker/Dockerfile.full @@ -21,8 +21,10 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ - && unitPackages="unit=${UNIT_VERSION} unit-php=${UNIT_VERSION} unit-python2.7=${UNIT_VERSION} unit-python3.7=${UNIT_VERSION} unit-perl=${UNIT_VERSION} unit-ruby=${UNIT_VERSION}" \ + && unitPackages="unit=${UNIT_VERSION} unit-php=${UNIT_VERSION} unit-python2.7=${UNIT_VERSION} unit-python3.7=${UNIT_VERSION} unit-perl=${UNIT_VERSION} unit-ruby=${UNIT_VERSION} unit-jsc11=${UNIT_VERSION}" \ && case "$dpkgArch" in \ amd64|i386) \ # arches officialy built by upstream diff --git a/pkg/docker/Dockerfile.go1.11-dev b/pkg/docker/Dockerfile.go1.11-dev index 7c3f234e..ed0e20eb 100644 --- a/pkg/docker/Dockerfile.go1.11-dev +++ b/pkg/docker/Dockerfile.go1.11-dev @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-go=${UNIT_VERSION} gcc" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.jsc11 b/pkg/docker/Dockerfile.jsc11 new file mode 100644 index 00000000..3e72afa8 --- /dev/null +++ b/pkg/docker/Dockerfile.jsc11 @@ -0,0 +1,95 @@ +FROM debian:buster-slim + +LABEL maintainer="NGINX Docker Maintainers " + +ENV UNIT_VERSION 1.19.0-1~buster + +RUN set -x \ + && apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 apt-transport-https ca-certificates \ + && \ + NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \ + found=''; \ + for server in \ + ha.pool.sks-keyservers.net \ + hkp://keyserver.ubuntu.com:80 \ + hkp://p80.pool.sks-keyservers.net:80 \ + pgp.mit.edu \ + ; do \ + echo "Fetching GPG key $NGINX_GPGKEY from $server"; \ + apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \ + done; \ + test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ + apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ + && dpkgArch="$(dpkg --print-architecture)" \ + && unitPackages="unit=${UNIT_VERSION} unit-jsc11=${UNIT_VERSION}" \ + && case "$dpkgArch" in \ + amd64|i386) \ +# arches officialy built by upstream + echo "deb https://packages.nginx.org/unit/debian/ buster unit" >> /etc/apt/sources.list.d/unit.list \ + && apt-get update \ + ;; \ + *) \ +# we're on an architecture upstream doesn't officially build for +# let's build binaries from the published source packages + echo "deb-src https://packages.nginx.org/unit/debian/ buster unit" >> /etc/apt/sources.list.d/unit.list \ + \ +# new directory for storing sources and .deb files + && tempDir="$(mktemp -d)" \ + && chmod 777 "$tempDir" \ +# (777 to ensure APT's "_apt" user can access it too) + \ +# save list of currently-installed packages so build dependencies can be cleanly removed later + && savedAptMark="$(apt-mark showmanual)" \ + \ +# build .deb files from upstream's source packages (which are verified by apt-get) + && apt-get update \ + && apt-get build-dep -y $unitPackages \ + && ( \ + cd "$tempDir" \ + && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \ + apt-get source --compile $unitPackages \ + ) \ +# we don't remove APT lists here because they get re-downloaded and removed later + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies +# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies) + && apt-mark showmanual | xargs apt-mark auto > /dev/null \ + && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \ + \ +# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be) + && ls -lAFh "$tempDir" \ + && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \ + && grep '^Package: ' "$tempDir/Packages" \ + && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \ +# work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes") +# Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied) +# ... +# E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied) + && apt-get -o Acquire::GzipIndexes=false update \ + ;; \ + esac \ + \ + && apt-get install --no-install-recommends --no-install-suggests -y \ + $unitPackages \ + curl \ + && apt-get remove --purge --auto-remove -y apt-transport-https && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/unit.list \ + \ +# if we have leftovers from building, let's purge them (including extra, unnecessary build deps) + && if [ -n "$tempDir" ]; then \ + apt-get purge -y --auto-remove \ + && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \ + fi + +# forward log to docker log collector +RUN ln -sf /dev/stdout /var/log/unit.log + +STOPSIGNAL SIGTERM + +COPY docker-entrypoint.sh /usr/local/bin/ +RUN mkdir /docker-entrypoint.d/ +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] + +CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock"] diff --git a/pkg/docker/Dockerfile.minimal b/pkg/docker/Dockerfile.minimal index 48f1864c..2fb58964 100644 --- a/pkg/docker/Dockerfile.minimal +++ b/pkg/docker/Dockerfile.minimal @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.perl5.28 b/pkg/docker/Dockerfile.perl5.28 index bff0ba0c..024d65db 100644 --- a/pkg/docker/Dockerfile.perl5.28 +++ b/pkg/docker/Dockerfile.perl5.28 @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-perl=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.php7.3 b/pkg/docker/Dockerfile.php7.3 index 832baa5d..ffcc3d1a 100644 --- a/pkg/docker/Dockerfile.php7.3 +++ b/pkg/docker/Dockerfile.php7.3 @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-php=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.python2.7 b/pkg/docker/Dockerfile.python2.7 index 85f0add6..4711f3e3 100644 --- a/pkg/docker/Dockerfile.python2.7 +++ b/pkg/docker/Dockerfile.python2.7 @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-python2.7=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.python3.7 b/pkg/docker/Dockerfile.python3.7 index cefd15c1..9370642f 100644 --- a/pkg/docker/Dockerfile.python3.7 +++ b/pkg/docker/Dockerfile.python3.7 @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-python3.7=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.ruby2.5 b/pkg/docker/Dockerfile.ruby2.5 index 36f9594f..4ff042ce 100644 --- a/pkg/docker/Dockerfile.ruby2.5 +++ b/pkg/docker/Dockerfile.ruby2.5 @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-ruby=${UNIT_VERSION}" \ && case "$dpkgArch" in \ -- cgit From f221ddbe66131948fb677dea0a58d1f1238e3661 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Thu, 8 Oct 2020 18:32:36 +0300 Subject: Added version 1.20.0 CHANGES. --- CHANGES | 45 +++++++++++++++++++++ docs/changes.xml | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/CHANGES b/CHANGES index 236371e1..040c6989 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,49 @@ +Changes with Unit 1.20.0 08 Oct 2020 + + *) Change: the PHP module is now initialized before chrooting; this + enables loading all extensions from the host system. + + *) Change: AVIF and APNG image formats added to the default MIME type + list. + + *) Feature: the Python module now fully supports applications that use + the ASGI 3.0 server interface. + + *) Feature: the Python module now has a built-in WebSocket server + implementation for applications, compatible with the HTTP & WebSocket + ASGI Message Format 2.1 specification. + + *) Feature: automatic mounting of an isolated "/tmp" file system into + chrooted application environments. + + *) Feature: the $host variable contains a normalized "Host" request + value. + + *) Feature: the "callable" option sets Python application callable + names. + + *) Feature: compatibility with PHP 8 RC 1. Thanks to Remi Collet. + + *) Feature: the "automount" option in the "isolation" object allows to + turn off the automatic mounting of language module dependencies. + + *) Bugfix: "pass"-ing requests to upstreams from a route was broken; the + bug had appeared in 1.19.0. Thanks to 洪志道 (Hong Zhi Dao) for + discovering and fixing it. + + *) Bugfix: the router process could crash during reconfiguration. + + *) Bugfix: a memory leak occurring in the router process; the bug had + appeared in 1.18.0. + + *) Bugfix: the "!" (non-empty) pattern was matched incorrectly; the bug + had appeared in 1.19.0. + + *) Bugfix: fixed building on platforms without sendfile() support, + notably NetBSD; the bug had appeared in 1.16.0. + + Changes with Unit 1.19.0 13 Aug 2020 *) Feature: reworked IPC between the router process and the applications diff --git a/docs/changes.xml b/docs/changes.xml index b3b8a201..ee3879f1 100644 --- a/docs/changes.xml +++ b/docs/changes.xml @@ -5,6 +5,127 @@ + + + + +NGINX Unit updated to 1.20.0. + + + + + + + + + + +the PHP module is now initialized before chrooting; this enables loading all +extensions from the host system. + + + + + +AVIF and APNG image formats added to the default MIME type list. + + + + + +the Python module now fully supports applications that use the ASGI 3.0 server +interface. + + + + + +the Python module now has a built-in WebSocket server implementation for +applications, compatible with the HTTP & WebSocket ASGI Message Format 2.1 +specification. + + + + + +automatic mounting of an isolated "/tmp" file system into chrooted application +environments. + + + + + +the $host variable contains a normalized "Host" request value. + + + + + +the "callable" option sets Python application callable names. + + + + + +compatibility with PHP 8 RC 1. Thanks to Remi Collet. + + + + + +the "automount" option in the "isolation" object allows to turn off the +automatic mounting of language module dependencies. + + + + + +"pass"-ing requests to upstreams from a route was broken; the bug had appeared +in 1.19.0. Thanks to 洪志道 (Hong Zhi Dao) for discovering and fixing it. + + + + + +the router process could crash during reconfiguration. + + + + + +a memory leak occurring in the router process; the bug had appeared in 1.18.0. + + + + + +the "!" (non-empty) pattern was matched incorrectly; +the bug had appeared in 1.19.0. + + + + + +fixed building on platforms without sendfile() support, notably NetBSD; +the bug had appeared in 1.16.0. + + + + + + " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.go1.11-dev b/pkg/docker/Dockerfile.go1.11-dev index ed0e20eb..17b1d2e9 100644 --- a/pkg/docker/Dockerfile.go1.11-dev +++ b/pkg/docker/Dockerfile.go1.11-dev @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.jsc11 b/pkg/docker/Dockerfile.jsc11 index 3e72afa8..65b8ce0f 100644 --- a/pkg/docker/Dockerfile.jsc11 +++ b/pkg/docker/Dockerfile.jsc11 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.minimal b/pkg/docker/Dockerfile.minimal index 2fb58964..5a10620a 100644 --- a/pkg/docker/Dockerfile.minimal +++ b/pkg/docker/Dockerfile.minimal @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.perl5.28 b/pkg/docker/Dockerfile.perl5.28 index 024d65db..868527e5 100644 --- a/pkg/docker/Dockerfile.perl5.28 +++ b/pkg/docker/Dockerfile.perl5.28 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.php7.3 b/pkg/docker/Dockerfile.php7.3 index ffcc3d1a..b5789dec 100644 --- a/pkg/docker/Dockerfile.php7.3 +++ b/pkg/docker/Dockerfile.php7.3 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.python2.7 b/pkg/docker/Dockerfile.python2.7 index 4711f3e3..9bb83f7b 100644 --- a/pkg/docker/Dockerfile.python2.7 +++ b/pkg/docker/Dockerfile.python2.7 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.python3.7 b/pkg/docker/Dockerfile.python3.7 index 9370642f..99324844 100644 --- a/pkg/docker/Dockerfile.python3.7 +++ b/pkg/docker/Dockerfile.python3.7 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ diff --git a/pkg/docker/Dockerfile.ruby2.5 b/pkg/docker/Dockerfile.ruby2.5 index 4ff042ce..fef96867 100644 --- a/pkg/docker/Dockerfile.ruby2.5 +++ b/pkg/docker/Dockerfile.ruby2.5 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ -- cgit From 10468ed22be1a1b3bf2b1c1eaa4ccb0c9ab266dc Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Thu, 8 Oct 2020 18:55:50 +0300 Subject: Added tag 1.20.0 for changeset f7e9cf490512 --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index a75d1a97..658a3235 100644 --- a/.hgtags +++ b/.hgtags @@ -26,3 +26,4 @@ b391df5f0102aa6afe660cfc863729c1b1111c9e 1.12.0 4b13438632bc37ca599113be90af64f6e2f09d83 1.17.0 9e14c63773be52613dd47dea9fd113037f15a3eb 1.18.0 86cdf66f82745d8db35345368dcdb38c79a4f03a 1.19.0 +f7e9cf490512549df03471223a93fd9dd14eff6b 1.20.0 -- cgit From 7d77d50568b347ec71a9db1f53b77d1976535294 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Thu, 8 Oct 2020 19:04:40 +0300 Subject: Updated 1.20.0 CHANGES to include pytest migration. --- CHANGES | 2 ++ docs/changes.xml | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index 040c6989..0373bb6f 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,8 @@ Changes with Unit 1.20.0 08 Oct 2020 *) Change: AVIF and APNG image formats added to the default MIME type list. + *) Change: functional tests migrated to the pytest framework. + *) Feature: the Python module now fully supports applications that use the ASGI 3.0 server interface. diff --git a/docs/changes.xml b/docs/changes.xml index ee3879f1..aa413e2a 100644 --- a/docs/changes.xml +++ b/docs/changes.xml @@ -43,6 +43,12 @@ AVIF and APNG image formats added to the default MIME type list. + + +functional tests migrated to the pytest framework. + + + the Python module now fully supports applications that use the ASGI 3.0 server -- cgit From ad516735a65fe109773b60e26214a071411f1734 Mon Sep 17 00:00:00 2001 From: Valentin Bartenev Date: Thu, 8 Oct 2020 19:09:16 +0300 Subject: Adjusted tag 1.20.0 to include 0e985b300673. --- .hgtags | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hgtags b/.hgtags index 658a3235..3d3b905b 100644 --- a/.hgtags +++ b/.hgtags @@ -26,4 +26,4 @@ b391df5f0102aa6afe660cfc863729c1b1111c9e 1.12.0 4b13438632bc37ca599113be90af64f6e2f09d83 1.17.0 9e14c63773be52613dd47dea9fd113037f15a3eb 1.18.0 86cdf66f82745d8db35345368dcdb38c79a4f03a 1.19.0 -f7e9cf490512549df03471223a93fd9dd14eff6b 1.20.0 +0e985b30067380782125f1c479eda4ef909418df 1.20.0 -- cgit