├── compat ├── ctype.h ├── limits.h ├── stdint.h ├── stddef.h ├── string.h └── assert.h ├── .gitignore ├── scripts ├── test.sh ├── install-git-hooks ├── pre-push.hook ├── aspell-pws ├── pre-commit.hook └── commit-msg.hook ├── http_server.h ├── .clang-format ├── README.md ├── LICENSE ├── Makefile ├── main.c ├── http_server.c └── htstress.c /compat/ctype.h: -------------------------------------------------------------------------------- 1 | /* Dummy */ 2 | -------------------------------------------------------------------------------- /compat/limits.h: -------------------------------------------------------------------------------- 1 | /* Dummy */ 2 | -------------------------------------------------------------------------------- /compat/stdint.h: -------------------------------------------------------------------------------- 1 | /* Dummy */ 2 | -------------------------------------------------------------------------------- /compat/stddef.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | -------------------------------------------------------------------------------- /compat/string.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | -------------------------------------------------------------------------------- /compat/assert.h: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_ASSERT_H 2 | #define COMPAT_ASSERT_H 3 | 4 | static inline void assert(int x){}; 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # kernel module compile output 2 | *.ko.cmd 3 | *.o.cmd 4 | .tmp_versions 5 | *.symvers 6 | *.symvers.cmd 7 | *.mod* 8 | *.order 9 | *.o 10 | *.ko 11 | 12 | # Other 13 | http_parser.c 14 | http_parser.h 15 | .cache.mk 16 | htstress 17 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | KHTTPD_MOD=khttpd.ko 4 | 5 | if [ "$EUID" -eq 0 ] 6 | then echo "Don't run this script as root" 7 | exit 8 | fi 9 | 10 | # load kHTTPd 11 | sudo rmmod -f khttpd 2>/dev/null 12 | sleep 1 13 | sudo insmod $KHTTPD_MOD 14 | 15 | # run HTTP benchmarking 16 | ./htstress -n 100000 -c 1 -t 4 http://localhost:8081/ 17 | 18 | # epilogue 19 | sudo rmmod khttpd 20 | echo "Complete" 21 | -------------------------------------------------------------------------------- /http_server.h: -------------------------------------------------------------------------------- 1 | #ifndef KHTTPD_HTTP_SERVER_H 2 | #define KHTTPD_HTTP_SERVER_H 3 | 4 | #include 5 | 6 | #define RECV_BUFFER_SIZE 4096 7 | extern mempool_t *http_buf_pool; 8 | 9 | struct http_server_param { 10 | struct socket *listen_socket; 11 | }; 12 | 13 | extern int http_server_daemon(void *arg); 14 | 15 | static inline void *http_buf_alloc(gfp_t gfp_mask, void *pool_data) 16 | { 17 | return kzalloc(RECV_BUFFER_SIZE, gfp_mask); 18 | } 19 | 20 | static inline void http_buf_free(void *element, void *pool_data) 21 | { 22 | kfree(element); 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /scripts/install-git-hooks: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! test -d .git; then 4 | echo "Execute scripts/install-git-hooks in the top-level directory." 5 | exit 1 6 | fi 7 | 8 | ln -sf ../../scripts/pre-commit.hook .git/hooks/pre-commit || exit 1 9 | chmod +x .git/hooks/pre-commit 10 | 11 | ln -sf ../../scripts/commit-msg.hook .git/hooks/commit-msg || exit 1 12 | chmod +x .git/hooks/commit-msg 13 | 14 | ln -sf ../../scripts/pre-push.hook .git/hooks/pre-push || exit 1 15 | chmod +x .git/hooks/pre-push 16 | 17 | touch .git/hooks/applied || exit 1 18 | 19 | echo 20 | echo "Git hooks are installed successfully." 21 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Chromium 2 | Language: Cpp 3 | MaxEmptyLinesToKeep: 3 4 | IndentCaseLabels: false 5 | AllowShortIfStatementsOnASingleLine: false 6 | AllowShortCaseLabelsOnASingleLine: false 7 | AllowShortLoopsOnASingleLine: false 8 | DerivePointerAlignment: false 9 | PointerAlignment: Right 10 | SpaceAfterCStyleCast: true 11 | TabWidth: 4 12 | UseTab: Never 13 | IndentWidth: 4 14 | BreakBeforeBraces: Linux 15 | AccessModifierOffset: -4 16 | ForEachMacros: 17 | - foreach 18 | - Q_FOREACH 19 | - BOOST_FOREACH 20 | - list_for_each 21 | - list_for_each_safe 22 | - list_for_each_entry 23 | - list_for_each_entry_safe 24 | - hlist_for_each_entry 25 | - rb_list_foreach 26 | - rb_list_foreach_safe 27 | - vec_foreach 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # khttpd 2 | 3 | `khttpd` is an experimental HTTP server implemented as Linux kernel module. 4 | The server defaults to port 8081, but this can be easily configured using 5 | command line argument `port=?` when you are about to load the kernel module. 6 | 7 | ## TODO 8 | * Release resources when HTTP connection is about to be closed. 9 | * Introduce CMWQ. 10 | * Improve memory management. 11 | * Request queue and/or cache 12 | 13 | ## License 14 | 15 | `khttpd` is released under the MIT License. Use of this source code is governed by 16 | a MIT License that can be found in the LICENSE file. 17 | 18 | External source code: 19 | * `http_parser.[ch]`: taken from [nodejs/http-parser](https://github.com/nodejs/http-parser) 20 | - Copyrighted by Joyent, Inc. and other Node contributors. 21 | - MIT License 22 | * `htstress.c`: derived from [htstress](https://github.com/arut/htstress) 23 | - Copyrighted by Roman Arutyunyan 24 | - 2-clause BSD license 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2020-2022 National Cheng Kung University, Taiwan. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/pre-push.hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | protected_branch='master' 4 | current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,') 5 | RED='\033[0;31m' 6 | GREEN='\033[1;32m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | # Show hints 11 | echo -e "${YELLOW}Hint${NC}: You might want to know why Git is always ${GREEN}asking for my password${NC}." 12 | echo -e " https://help.github.com/en/github/using-git/why-is-git-always-asking-for-my-password" 13 | echo "" 14 | 15 | # only run this if you are pushing to master 16 | if [[ $current_branch = $protected_branch ]] ; then 17 | echo -e "${YELLOW}Running pre push to master check...${NC}" 18 | 19 | echo -e "${YELLOW}Trying to build tests project...${NC}" 20 | 21 | # build the project 22 | make 23 | 24 | # $? is a shell variable which stores the return code from what we just ran 25 | rc=$? 26 | if [[ $rc != 0 ]] ; then 27 | echo -e "${RED}Failed to build the project, please fix this and push again${NC}" 28 | echo "" 29 | exit $rc 30 | fi 31 | 32 | # Everything went OK so we can exit with a zero 33 | echo -e "${GREEN}Pre-push check passed!${NC}" 34 | echo "" 35 | fi 36 | 37 | exit 0 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | KDIR=/lib/modules/$(shell uname -r)/build 2 | 3 | CFLAGS_user = -std=gnu11 -Wall -Wextra -Werror 4 | LDFLAGS_user = -lpthread 5 | 6 | obj-m += khttpd.o 7 | khttpd-objs := \ 8 | http_parser.o \ 9 | http_server.o \ 10 | main.o 11 | 12 | GIT_HOOKS := .git/hooks/applied 13 | all: $(GIT_HOOKS) http_parser.c htstress 14 | make -C $(KDIR) M=$(PWD) modules 15 | 16 | $(GIT_HOOKS): 17 | @scripts/install-git-hooks 18 | @echo 19 | 20 | htstress: htstress.c 21 | $(CC) $(CFLAGS_user) -o $@ $< $(LDFLAGS_user) 22 | 23 | check: all 24 | @scripts/test.sh 25 | 26 | clean: 27 | make -C $(KDIR) M=$(PWD) clean 28 | $(RM) htstress 29 | 30 | # Download http_parser.[ch] from nodejs/http-parser repository 31 | # the inclusion of standard header files such as will be replaced 32 | # with "compat/string.h", which is just a wrapper to Linux kernel headers. 33 | # TODO: rewrite with elegant scripts 34 | http_parser.c: 35 | wget -q https://raw.githubusercontent.com/nodejs/http-parser/main/http_parser.c 36 | @sed -i 's/#include /#include "compat\/assert.h"/' $@ 37 | @sed -i 's/#include /#include "compat\/stddef.h"/' $@ 38 | @sed -i 's/#include /#include "compat\/ctype.h"/' $@ 39 | @sed -i 's/#include /#include "compat\/string.h"/' $@ 40 | @sed -i 's/#include /#include "compat\/limits.h"/' $@ 41 | @sed -i -E 's@/\*[[:space:]]*fall[[:space:]-]*through[[:space:]]*\*/@fallthrough;@Ig' $@ 42 | @echo "File $@ was patched." 43 | wget -q https://raw.githubusercontent.com/nodejs/http-parser/main/http_parser.h 44 | @sed -i 's/#include /#include "compat\/stddef.h"/' http_parser.h 45 | @sed -i 's/#include /#include "compat\/stdint.h"/' http_parser.h 46 | @echo "File http_parser.h was patched." 47 | 48 | distclean: clean 49 | $(RM) http_parser.c http_parser.h 50 | -------------------------------------------------------------------------------- /scripts/aspell-pws: -------------------------------------------------------------------------------- 1 | personal_ws-1.1 en 500 2 | usr 3 | lib 4 | sbin 5 | env 6 | bash 7 | etc 8 | var 9 | dudect 10 | runtime 11 | todo 12 | fixme 13 | hotfix 14 | qtest 15 | vscode 16 | sanitizer 17 | unix 18 | linux 19 | valgrind 20 | ubuntu 21 | gdb 22 | sdk 23 | aspell 24 | cppcheck 25 | glibc 26 | git 27 | pre 28 | gcc 29 | clang 30 | enqueue 31 | dequeue 32 | fifo 33 | lifo 34 | stdin 35 | stdout 36 | stderr 37 | strdup 38 | strcmp 39 | strcasecmp 40 | snprintf 41 | sprintf 42 | strcat 43 | strchr 44 | strcmp 45 | strcoll 46 | strcpy 47 | strcspn 48 | strerror 49 | strlen 50 | strncasecmp 51 | strncat 52 | strncmp 53 | strncpy 54 | strpbrk 55 | strrchr 56 | strspn 57 | strstr 58 | strtod 59 | strtof 60 | strtok 61 | strtol 62 | strtold 63 | strtoul 64 | atexit 65 | atof 66 | atoi 67 | atol 68 | bsearch 69 | calloc 70 | fclose 71 | fdopen 72 | feof 73 | ferror 74 | fflush 75 | fgetc 76 | fgetpos 77 | fgets 78 | fileno 79 | fopen 80 | fprintf 81 | fputc 82 | fputs 83 | fread 84 | freopen 85 | fscanf 86 | fseek 87 | fsetpos 88 | ftell 89 | fwrite 90 | getc 91 | getchar 92 | getenv 93 | gets 94 | isalnum 95 | isalpha 96 | isascii 97 | iscntrl 98 | isdigit 99 | isgraph 100 | islower 101 | isprint 102 | ispunct 103 | isspace 104 | isupper 105 | longjmp 106 | memchr 107 | memcmp 108 | memcpy 109 | memmove 110 | memset 111 | printf 112 | putc 113 | putchar 114 | putenv 115 | puts 116 | qsort 117 | rand 118 | realloc 119 | regcomp 120 | regerror 121 | regexec 122 | regfree 123 | rewind 124 | scanf 125 | setbuf 126 | setjmp 127 | signal 128 | srand 129 | sscanf 130 | macOS 131 | Fibonacci 132 | fib 133 | pow 134 | Binet 135 | Vorobev 136 | GMP 137 | MPFR 138 | mutex 139 | trylock 140 | unlock 141 | lseek 142 | llseek 143 | cdev 144 | inode 145 | sysfs 146 | printk 147 | clz 148 | ctz 149 | popcount 150 | fops 151 | init 152 | alloc 153 | ktime 154 | getres 155 | gettime 156 | settime 157 | ns 158 | timespec 159 | timeval 160 | NaN 161 | livepatch 162 | MathEx 163 | vec 164 | expr 165 | httpd 166 | daemon 167 | bench 168 | benchmark 169 | htstress 170 | workqueue 171 | percpu 172 | cmwq 173 | wq 174 | epoll 175 | kthread 176 | sock 177 | tcp 178 | udp 179 | msg 180 | http 181 | -------------------------------------------------------------------------------- /scripts/pre-commit.hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CPPCHECK_unmatched= 4 | for f in *.c *.h; do 5 | CPPCHECK_unmatched="$CPPCHECK_unmatched --suppress=unmatchedSuppression:$f" 6 | done 7 | # http-parser was taken from Node.js, and we don't tend to validate it. 8 | CPPCHECK_suppresses="--suppress=missingIncludeSystem \ 9 | --suppress=unusedFunction:http_parser.c \ 10 | --suppress=duplicateBreak:http_parser.c \ 11 | --suppress=shadowVariable:http_parser.c \ 12 | --suppress=unknownMacro:http_server.c \ 13 | --suppress=unusedStructMember:http_parser.h \ 14 | --suppress=unusedStructMember:http_server.h" 15 | CPPCHECK_OPTS="-I. --enable=all --error-exitcode=1 --force $CPPCHECK_suppresses $CPPCHECK_unmatched" 16 | 17 | RETURN=0 18 | CLANG_FORMAT=$(which clang-format) 19 | if [ $? -ne 0 ]; then 20 | echo "[!] clang-format not installed. Unable to check source file format policy." >&2 21 | exit 1 22 | fi 23 | 24 | CPPCHECK=$(which cppcheck) 25 | if [ $? -ne 0 ]; then 26 | echo "[!] cppcheck not installed. Unable to perform static analysis." >&2 27 | exit 1 28 | fi 29 | 30 | ASPELL=$(which aspell) 31 | if [ $? -ne 0 ]; then 32 | echo "[!] aspell not installed. Unable to do spelling check." >&2 33 | exit 1 34 | fi 35 | 36 | DIFF=$(which colordiff) 37 | if [ $? -ne 0 ]; then 38 | DIFF=diff 39 | fi 40 | 41 | FILES=`git diff --cached --name-only --diff-filter=ACMR | grep -E "\.(c|cpp|h)$"` 42 | for FILE in $FILES; do 43 | nf=`git checkout-index --temp $FILE | cut -f 1` 44 | tempdir=`mktemp -d` || exit 1 45 | newfile=`mktemp ${tempdir}/${nf}.XXXXXX` || exit 1 46 | basename=`basename $FILE` 47 | 48 | source="${tempdir}/${basename}" 49 | mv $nf $source 50 | cp .clang-format $tempdir 51 | $CLANG_FORMAT $source > $newfile 2>> /dev/null 52 | $DIFF -u -p -B --label="modified $FILE" --label="expected coding style" \ 53 | "${source}" "${newfile}" 54 | r=$? 55 | rm -rf "${tempdir}" 56 | if [ $r != 0 ] ; then 57 | echo "[!] $FILE does not follow the consistent coding style." >&2 58 | RETURN=1 59 | fi 60 | if [ $RETURN -eq 1 ]; then 61 | echo "" >&2 62 | echo "Make sure you indent as the following:" >&2 63 | echo " clang-format -i $FILE" >&2 64 | echo 65 | fi 66 | done 67 | 68 | # Prevent unsafe functions 69 | root=$(git rev-parse --show-toplevel) 70 | banned="([^f]gets\()|(sprintf\()|(strcpy\()" 71 | status=0 72 | for file in $(git diff --staged --name-only | grep -E "\.(c|cc|cpp|h|hh|hpp)\$") 73 | do 74 | filepath="${root}/${file}" 75 | output=$(grep -nrE "${banned}" "${filepath}") 76 | if [ ! -z "${output}" ]; then 77 | echo "Dangerous function detected in ${filepath}" 78 | echo "${output}" 79 | echo 80 | echo "Read 'Common vulnerabilities guide for C programmers' carefully." 81 | echo " https://security.web.cern.ch/security/recommendations/en/codetools/c.shtml" 82 | RETURN=1 83 | fi 84 | done 85 | 86 | # static analysis 87 | git ls-tree --full-tree -r --name-only HEAD | \ 88 | grep -v compat | grep -E "\.(c|cc|cpp|h|hh|hpp)\$" | \ 89 | xargs $CPPCHECK $CPPCHECK_OPTS >/dev/null 90 | if [ $? -ne 0 ]; then 91 | RETURN=1 92 | echo "" >&2 93 | echo "Fail to pass static analysis." >&2 94 | echo 95 | fi 96 | 97 | exit $RETURN 98 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "http_server.h" 12 | 13 | #define DEFAULT_PORT 8081 14 | #define DEFAULT_BACKLOG 100 15 | #define POOL_MIN_NR 4 16 | 17 | mempool_t *http_buf_pool; 18 | 19 | static ushort port = DEFAULT_PORT; 20 | module_param(port, ushort, S_IRUGO); 21 | static ushort backlog = DEFAULT_BACKLOG; 22 | module_param(backlog, ushort, S_IRUGO); 23 | 24 | static struct socket *listen_socket; 25 | static struct http_server_param param; 26 | static struct task_struct *http_server; 27 | 28 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0) 29 | static int set_sock_opt(struct socket *sock, 30 | int level, 31 | int optname, 32 | char *optval, 33 | unsigned int optlen) 34 | { 35 | int ret = 0; 36 | 37 | if (optlen < sizeof(int)) 38 | return -EINVAL; 39 | 40 | switch (optname) { 41 | case SO_REUSEADDR: 42 | sock_set_reuseaddr(sock->sk); 43 | break; 44 | case SO_RCVBUF: 45 | sock_set_rcvbuf(sock->sk, *(int *) optval); 46 | break; 47 | } 48 | 49 | return ret; 50 | } 51 | 52 | static int set_tcp_opt(struct socket *sock, 53 | int level, 54 | int optname, 55 | char *optval, 56 | unsigned int optlen) 57 | { 58 | int ret = 0; 59 | 60 | if (optlen < sizeof(int)) 61 | return -EINVAL; 62 | 63 | switch (optname) { 64 | case TCP_NODELAY: 65 | tcp_sock_set_nodelay(sock->sk); 66 | break; 67 | case TCP_CORK: 68 | tcp_sock_set_cork(sock->sk, *(bool *) optval); 69 | break; 70 | } 71 | 72 | return ret; 73 | } 74 | 75 | static int kernel_setsockopt(struct socket *sock, 76 | int level, 77 | int optname, 78 | char *optval, 79 | unsigned int optlen) 80 | { 81 | if (level == SOL_SOCKET) 82 | return set_sock_opt(sock, level, optname, optval, optlen); 83 | else if (level == SOL_TCP) 84 | return set_tcp_opt(sock, level, optname, optval, optlen); 85 | return -EINVAL; 86 | } 87 | #endif 88 | 89 | static inline int setsockopt(struct socket *sock, 90 | int level, 91 | int optname, 92 | int optval) 93 | { 94 | int opt = optval; 95 | return kernel_setsockopt(sock, level, optname, (char *) &opt, sizeof(opt)); 96 | } 97 | 98 | static int open_listen_socket(ushort port, ushort backlog, struct socket **res) 99 | { 100 | struct socket *sock; 101 | struct sockaddr_in s; 102 | 103 | int err = sock_create(PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock); 104 | if (err < 0) { 105 | pr_err("sock_create() failure, err=%d\n", err); 106 | return err; 107 | } 108 | 109 | err = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 1); 110 | if (err < 0) 111 | goto bail_setsockopt; 112 | 113 | err = setsockopt(sock, SOL_TCP, TCP_NODELAY, 1); 114 | if (err < 0) 115 | goto bail_setsockopt; 116 | 117 | err = setsockopt(sock, SOL_TCP, TCP_CORK, 0); 118 | if (err < 0) 119 | goto bail_setsockopt; 120 | 121 | err = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, 1024 * 1024); 122 | if (err < 0) 123 | goto bail_setsockopt; 124 | 125 | err = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, 1024 * 1024); 126 | if (err < 0) 127 | goto bail_setsockopt; 128 | 129 | memset(&s, 0, sizeof(s)); 130 | s.sin_family = AF_INET; 131 | s.sin_addr.s_addr = htonl(INADDR_ANY); 132 | s.sin_port = htons(port); 133 | err = kernel_bind(sock, (struct sockaddr *) &s, sizeof(s)); 134 | if (err < 0) { 135 | pr_err("kernel_bind() failure, err=%d\n", err); 136 | goto bail_sock; 137 | } 138 | 139 | err = kernel_listen(sock, backlog); 140 | if (err < 0) { 141 | pr_err("kernel_listen() failure, err=%d\n", err); 142 | goto bail_sock; 143 | } 144 | *res = sock; 145 | return 0; 146 | 147 | bail_setsockopt: 148 | pr_err("kernel_setsockopt() failure, err=%d\n", err); 149 | bail_sock: 150 | sock_release(sock); 151 | return err; 152 | } 153 | 154 | static void close_listen_socket(struct socket *socket) 155 | { 156 | kernel_sock_shutdown(socket, SHUT_RDWR); 157 | sock_release(socket); 158 | } 159 | 160 | static int __init khttpd_init(void) 161 | { 162 | if (!(http_buf_pool = mempool_create(POOL_MIN_NR, http_buf_alloc, 163 | http_buf_free, NULL))) { 164 | pr_err("failed to create mempool\n"); 165 | return -ENOMEM; 166 | } 167 | int err = open_listen_socket(port, backlog, &listen_socket); 168 | if (err < 0) { 169 | pr_err("can't open listen socket\n"); 170 | return err; 171 | } 172 | param.listen_socket = listen_socket; 173 | http_server = kthread_run(http_server_daemon, ¶m, KBUILD_MODNAME); 174 | if (IS_ERR(http_server)) { 175 | pr_err("can't start http server daemon\n"); 176 | close_listen_socket(listen_socket); 177 | return PTR_ERR(http_server); 178 | } 179 | return 0; 180 | } 181 | 182 | static void __exit khttpd_exit(void) 183 | { 184 | send_sig(SIGTERM, http_server, 1); 185 | kthread_stop(http_server); 186 | close_listen_socket(listen_socket); 187 | pr_info("module unloaded\n"); 188 | } 189 | 190 | module_init(khttpd_init); 191 | module_exit(khttpd_exit); 192 | 193 | MODULE_LICENSE("Dual MIT/GPL"); 194 | MODULE_AUTHOR("National Cheng Kung University, Taiwan"); 195 | MODULE_DESCRIPTION("in-kernel HTTP daemon"); 196 | MODULE_VERSION("0.1"); 197 | -------------------------------------------------------------------------------- /http_server.c: -------------------------------------------------------------------------------- 1 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "http_parser.h" 8 | #include "http_server.h" 9 | 10 | #define CRLF "\r\n" 11 | 12 | #define HTTP_RESPONSE_200_DUMMY \ 13 | "" \ 14 | "HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \ 15 | "Content-Type: text/plain" CRLF "Content-Length: 12" CRLF \ 16 | "Connection: Close" CRLF CRLF "Hello World!" CRLF 17 | #define HTTP_RESPONSE_200_KEEPALIVE_DUMMY \ 18 | "" \ 19 | "HTTP/1.1 200 OK" CRLF "Server: " KBUILD_MODNAME CRLF \ 20 | "Content-Type: text/plain" CRLF "Content-Length: 12" CRLF \ 21 | "Connection: Keep-Alive" CRLF CRLF "Hello World!" CRLF 22 | #define HTTP_RESPONSE_501 \ 23 | "" \ 24 | "HTTP/1.1 501 Not Implemented" CRLF "Server: " KBUILD_MODNAME CRLF \ 25 | "Content-Type: text/plain" CRLF "Content-Length: 21" CRLF \ 26 | "Connection: Close" CRLF CRLF "501 Not Implemented" CRLF 27 | #define HTTP_RESPONSE_501_KEEPALIVE \ 28 | "" \ 29 | "HTTP/1.1 501 Not Implemented" CRLF "Server: " KBUILD_MODNAME CRLF \ 30 | "Content-Type: text/plain" CRLF "Content-Length: 21" CRLF \ 31 | "Connection: KeepAlive" CRLF CRLF "501 Not Implemented" CRLF 32 | 33 | 34 | struct http_request { 35 | struct socket *socket; 36 | enum http_method method; 37 | char request_url[128]; 38 | int complete; 39 | }; 40 | 41 | static int http_server_recv(struct socket *sock, char *buf, size_t size) 42 | { 43 | struct kvec iov = {.iov_base = (void *) buf, .iov_len = size}; 44 | struct msghdr msg = { 45 | .msg_name = 0, 46 | .msg_namelen = 0, 47 | .msg_control = NULL, 48 | .msg_controllen = 0, 49 | .msg_flags = 0, 50 | }; 51 | return kernel_recvmsg(sock, &msg, &iov, 1, size, msg.msg_flags); 52 | } 53 | 54 | static int http_server_send(struct socket *sock, const char *buf, size_t size) 55 | { 56 | struct msghdr msg = { 57 | .msg_name = NULL, 58 | .msg_namelen = 0, 59 | .msg_control = NULL, 60 | .msg_controllen = 0, 61 | .msg_flags = 0, 62 | }; 63 | int done = 0; 64 | while (done < size) { 65 | struct kvec iov = { 66 | .iov_base = (void *) ((char *) buf + done), 67 | .iov_len = size - done, 68 | }; 69 | int length = kernel_sendmsg(sock, &msg, &iov, 1, iov.iov_len); 70 | if (length < 0) { 71 | pr_err("write error: %d\n", length); 72 | break; 73 | } 74 | done += length; 75 | } 76 | return done; 77 | } 78 | 79 | static int http_server_response(struct http_request *request, int keep_alive) 80 | { 81 | char *response; 82 | 83 | pr_info("requested_url = %s\n", request->request_url); 84 | if (request->method != HTTP_GET) 85 | response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501; 86 | else 87 | response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY 88 | : HTTP_RESPONSE_200_DUMMY; 89 | http_server_send(request->socket, response, strlen(response)); 90 | return 0; 91 | } 92 | 93 | static int http_parser_callback_message_begin(http_parser *parser) 94 | { 95 | struct http_request *request = parser->data; 96 | struct socket *socket = request->socket; 97 | memset(request, 0x00, sizeof(struct http_request)); 98 | request->socket = socket; 99 | return 0; 100 | } 101 | 102 | static int http_parser_callback_request_url(http_parser *parser, 103 | const char *p, 104 | size_t len) 105 | { 106 | struct http_request *request = parser->data; 107 | strncat(request->request_url, p, len); 108 | return 0; 109 | } 110 | 111 | static int http_parser_callback_header_field(http_parser *parser, 112 | const char *p, 113 | size_t len) 114 | { 115 | return 0; 116 | } 117 | 118 | static int http_parser_callback_header_value(http_parser *parser, 119 | const char *p, 120 | size_t len) 121 | { 122 | return 0; 123 | } 124 | 125 | static int http_parser_callback_headers_complete(http_parser *parser) 126 | { 127 | struct http_request *request = parser->data; 128 | request->method = parser->method; 129 | return 0; 130 | } 131 | 132 | static int http_parser_callback_body(http_parser *parser, 133 | const char *p, 134 | size_t len) 135 | { 136 | return 0; 137 | } 138 | 139 | static int http_parser_callback_message_complete(http_parser *parser) 140 | { 141 | struct http_request *request = parser->data; 142 | http_server_response(request, http_should_keep_alive(parser)); 143 | request->complete = 1; 144 | return 0; 145 | } 146 | 147 | static int http_server_worker(void *arg) 148 | { 149 | char *buf; 150 | struct http_parser parser; 151 | struct http_parser_settings setting = { 152 | .on_message_begin = http_parser_callback_message_begin, 153 | .on_url = http_parser_callback_request_url, 154 | .on_header_field = http_parser_callback_header_field, 155 | .on_header_value = http_parser_callback_header_value, 156 | .on_headers_complete = http_parser_callback_headers_complete, 157 | .on_body = http_parser_callback_body, 158 | .on_message_complete = http_parser_callback_message_complete, 159 | }; 160 | struct http_request request; 161 | struct socket *socket = (struct socket *) arg; 162 | int err = 0; 163 | 164 | allow_signal(SIGKILL); 165 | allow_signal(SIGTERM); 166 | 167 | buf = mempool_alloc(http_buf_pool, GFP_KERNEL); 168 | if (!buf) { 169 | pr_err("can't allocate memory!\n"); 170 | err = -ENOMEM; 171 | goto out; 172 | } 173 | 174 | request.socket = socket; 175 | http_parser_init(&parser, HTTP_REQUEST); 176 | parser.data = &request; 177 | while (!kthread_should_stop()) { 178 | int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1); 179 | if (ret <= 0) { 180 | if (ret) { 181 | pr_err("recv error: %d\n", ret); 182 | err = ret; 183 | } 184 | goto out_free_buf; 185 | } 186 | http_parser_execute(&parser, &setting, buf, ret); 187 | if (request.complete && !http_should_keep_alive(&parser)) 188 | goto out_free_buf; 189 | memset(buf, 0, RECV_BUFFER_SIZE); 190 | } 191 | out_free_buf: 192 | mempool_free(buf, http_buf_pool); 193 | out: 194 | kernel_sock_shutdown(socket, SHUT_RDWR); 195 | sock_release(socket); 196 | return err; 197 | } 198 | 199 | int http_server_daemon(void *arg) 200 | { 201 | struct socket *socket; 202 | struct task_struct *worker; 203 | struct http_server_param *param = (struct http_server_param *) arg; 204 | 205 | allow_signal(SIGKILL); 206 | allow_signal(SIGTERM); 207 | 208 | while (!kthread_should_stop()) { 209 | int err = kernel_accept(param->listen_socket, &socket, 0); 210 | if (err < 0) { 211 | if (signal_pending(current)) 212 | break; 213 | pr_err("kernel_accept() error: %d\n", err); 214 | continue; 215 | } 216 | worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); 217 | if (IS_ERR(worker)) { 218 | pr_err("can't create more worker process\n"); 219 | kernel_sock_shutdown(socket, SHUT_RDWR); 220 | sock_release(socket); 221 | continue; 222 | } 223 | } 224 | return 0; 225 | } 226 | -------------------------------------------------------------------------------- /scripts/commit-msg.hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # git-good-commit(1) - Git hook to help you write good commit messages. 4 | # Released under the MIT License. 5 | # 6 | # https://github.com/tommarshall/git-good-commit 7 | 8 | COMMIT_MSG_FILE="$1" 9 | COMMIT_MSG_LINES= 10 | HOOK_EDITOR= 11 | SKIP_DISPLAY_WARNINGS=0 12 | WARNINGS= 13 | 14 | RED= 15 | YELLOW= 16 | BLUE= 17 | WHITE= 18 | CYAN= 19 | NC= 20 | 21 | # 22 | # Set colour variables if the output should be coloured. 23 | # 24 | 25 | set_colors() { 26 | local default_color=$(git config --get hooks.goodcommit.color || git config --get color.ui || echo 'auto') 27 | if [[ $default_color == 'always' ]] || [[ $default_color == 'auto' && -t 1 ]]; then 28 | RED='\033[1;31m' 29 | YELLOW='\033[1;33m' 30 | BLUE='\033[1;34m' 31 | WHITE='\033[1;37m' 32 | CYAN='\033[1;36m' 33 | NC='\033[0m' # No Color 34 | fi 35 | } 36 | 37 | # 38 | # Set the hook editor, using the same approach as git. 39 | # 40 | 41 | set_editor() { 42 | # $GIT_EDITOR appears to always be set to `:` when the hook is executed by Git? 43 | # ref: http://stackoverflow.com/q/41468839/885540 44 | # ref: https://github.com/tommarshall/git-good-commit/issues/11 45 | # HOOK_EDITOR=$GIT_EDITOR 46 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR=$(git config --get core.editor) 47 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR=$VISUAL 48 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR=$EDITOR 49 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR='vi' 50 | } 51 | 52 | # 53 | # Output prompt help information. 54 | # 55 | 56 | prompt_help() { 57 | echo -e "${RED}$(cat <<-EOF 58 | e - edit commit message 59 | n - abort commit 60 | ? - print help 61 | EOF 62 | )${NC}" 63 | } 64 | 65 | # 66 | # Add a warning with and . 67 | # 68 | 69 | add_warning() { 70 | local line_number=$1 71 | local warning=$2 72 | WARNINGS[$line_number]="${WARNINGS[$line_number]}$warning;" 73 | } 74 | 75 | # 76 | # Output warnings. 77 | # 78 | 79 | display_warnings() { 80 | if [ $SKIP_DISPLAY_WARNINGS -eq 1 ]; then 81 | # if the warnings were skipped then they should be displayed next time 82 | SKIP_DISPLAY_WARNINGS=0 83 | return 84 | fi 85 | 86 | for i in "${!WARNINGS[@]}"; do 87 | printf "%-74s ${WHITE}%s${NC}\n" "${COMMIT_MSG_LINES[$(($i-1))]}" "[line ${i}]" 88 | IFS=';' read -ra WARNINGS_ARRAY <<< "${WARNINGS[$i]}" 89 | for ERROR in "${WARNINGS_ARRAY[@]}"; do 90 | echo -e " ${YELLOW}- ${ERROR}${NC}" 91 | done 92 | done 93 | 94 | echo 95 | echo -e "${RED}$(cat <<-EOF 96 | How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/ 97 | EOF 98 | )${NC}" 99 | } 100 | 101 | # 102 | # Read the contents of the commit msg into an array of lines. 103 | # 104 | 105 | read_commit_message() { 106 | # reset commit_msg_lines 107 | COMMIT_MSG_LINES=() 108 | 109 | # read commit message into lines array 110 | while IFS= read -r; do 111 | 112 | # trim trailing spaces from commit lines 113 | shopt -s extglob 114 | REPLY="${REPLY%%*( )}" 115 | shopt -u extglob 116 | 117 | # ignore comments 118 | [[ $REPLY =~ ^# ]] 119 | test $? -eq 0 || COMMIT_MSG_LINES+=("$REPLY") 120 | 121 | done < $COMMIT_MSG_FILE 122 | } 123 | 124 | # 125 | # Validate the contents of the commmit msg agains the good commit guidelines. 126 | # 127 | 128 | validate_commit_message() { 129 | # reset warnings 130 | WARNINGS=() 131 | 132 | # capture the subject, and remove the 'squash! ' prefix if present 133 | COMMIT_SUBJECT=${COMMIT_MSG_LINES[0]/#squash! /} 134 | 135 | # if the commit is empty there's nothing to validate, we can return here 136 | COMMIT_MSG_STR="${COMMIT_MSG_LINES[*]}" 137 | test -z "${COMMIT_MSG_STR[*]// }" && return; 138 | 139 | # if the commit subject starts with 'fixup! ' there's nothing to validate, we can return here 140 | [[ $COMMIT_SUBJECT == 'fixup! '* ]] && return; 141 | 142 | # skip first token in subject (e.g. issue ID from bugtracker which is validated otherwise) 143 | skipfirsttokeninsubject=$(git config --get hooks.goodcommit.subjectskipfirsttoken || echo 'false') 144 | if [ "$skipfirsttokeninsubject" == "true" ]; then 145 | COMMIT_SUBJECT_TO_PROCESS=${COMMIT_SUBJECT#* } 146 | else 147 | COMMIT_SUBJECT_TO_PROCESS=$COMMIT_SUBJECT 148 | fi 149 | 150 | # 0. Check spelling 151 | # ------------------------------------------------------------------------------ 152 | ASPELL=$(which aspell) 153 | if [ $? -ne 0 ]; then 154 | echo "Aspell not installed - unable to check spelling" 155 | else 156 | LINE_NUMBER=1 157 | MISSPELLED_WORDS=`echo "$COMMIT_MSG_LINES[LINE_NUMBER]" | $ASPELL --lang=en --list --home-dir=scripts --personal=aspell-pws` 158 | if [ -n "$MISSPELLED_WORDS" ]; then 159 | add_warning LINE_NUMBER "Possible misspelled word(s): $MISSPELLED_WORDS" 160 | fi 161 | fi 162 | 163 | # 1. Separate subject from body with a blank line 164 | # ------------------------------------------------------------------------------ 165 | 166 | test ${#COMMIT_MSG_LINES[@]} -lt 1 || test -z "${COMMIT_MSG_LINES[1]}" 167 | test $? -eq 0 || add_warning 2 "Separate subject from body with a blank line" 168 | 169 | # 2. Limit the subject line to configured number of characters 170 | # ------------------------------------------------------------------------------ 171 | 172 | subject_max_length=$(git config --get hooks.goodcommit.subjectmaxlength || echo '60') 173 | test "${#COMMIT_SUBJECT}" -le $subject_max_length 174 | test $? -eq 0 || add_warning 1 "Limit the subject line to $subject_max_length characters (${#COMMIT_SUBJECT} chars)" 175 | 176 | # 3. Capitalize the subject line 177 | # ------------------------------------------------------------------------------ 178 | 179 | [[ ${COMMIT_SUBJECT_TO_PROCESS} =~ ^[[:blank:]]*([[:upper:]]{1}[[:lower:]]*|[[:digit:]]+)([[:blank:]]|[[:punct:]]|$) ]] 180 | test $? -eq 0 || add_warning 1 "Capitalize the subject line" 181 | 182 | # 4. Do not end the subject line with a period 183 | # ------------------------------------------------------------------------------ 184 | 185 | [[ ${COMMIT_SUBJECT} =~ [^\.]$ ]] 186 | test $? -eq 0 || add_warning 1 "Do not end the subject line with a period" 187 | 188 | # 5. Use the imperative mood in the subject line 189 | # ------------------------------------------------------------------------------ 190 | 191 | IMPERATIVE_MOOD_BLACKLIST=( 192 | added adds adding 193 | adjusted adjusts adjusting 194 | amended amends amending 195 | avoided avoids avoiding 196 | bumped bumps bumping 197 | changed changes changing 198 | checked checks checking 199 | committed commits committing 200 | copied copies copying 201 | corrected corrects correcting 202 | created creates creating 203 | decreased decreases decreasing 204 | deleted deletes deleting 205 | disabled disables disabling 206 | dropped drops dropping 207 | duplicated duplicates duplicating 208 | enabled enables enabling 209 | excluded excludes excluding 210 | fixed fixes fixing 211 | handled handles handling 212 | implemented implements implementing 213 | improved improves improving 214 | included includes including 215 | increased increases increasing 216 | installed installs installing 217 | introduced introduces introducing 218 | merged merges merging 219 | moved moves moving 220 | pruned prunes pruning 221 | refactored refactors refactoring 222 | released releases releasing 223 | removed removes removing 224 | renamed renames renaming 225 | replaced replaces replacing 226 | resolved resolves resolving 227 | reverted reverts reverting 228 | showed shows showing 229 | tested tests testing 230 | tidied tidies tidying 231 | updated updates updating 232 | used uses using 233 | ) 234 | 235 | # enable case insensitive match 236 | shopt -s nocasematch 237 | 238 | for BLACKLISTED_WORD in "${IMPERATIVE_MOOD_BLACKLIST[@]}"; do 239 | [[ ${COMMIT_SUBJECT_TO_PROCESS} =~ ^[[:blank:]]*$BLACKLISTED_WORD ]] 240 | test $? -eq 0 && add_warning 1 "Use the imperative mood in the subject line, e.g 'fix' not 'fixes'" && break 241 | done 242 | 243 | # disable case insensitive match 244 | shopt -u nocasematch 245 | 246 | # 6. Wrap the body at 72 characters 247 | # ------------------------------------------------------------------------------ 248 | 249 | URL_REGEX='^[[:blank:]]*(https?|ftp|file)://[-A-Za-z0-9\+&@#/%?=~_|!:,.;]*[-A-Za-z0-9\+&@#/%=~_|]' 250 | 251 | for i in "${!COMMIT_MSG_LINES[@]}"; do 252 | LINE_NUMBER=$((i+1)) 253 | test "${#COMMIT_MSG_LINES[$i]}" -le 72 || [[ ${COMMIT_MSG_LINES[$i]} =~ $URL_REGEX ]] 254 | test $? -eq 0 || add_warning $LINE_NUMBER "Wrap the body at 72 characters (${#COMMIT_MSG_LINES[$i]} chars)" 255 | done 256 | 257 | # 7. Use the body to explain what and why vs. how 258 | # ------------------------------------------------------------------------------ 259 | 260 | # ? 261 | 262 | # 8. Do no write single worded commits 263 | # ------------------------------------------------------------------------------ 264 | 265 | COMMIT_SUBJECT_WORDS=(${COMMIT_SUBJECT_TO_PROCESS}) 266 | test "${#COMMIT_SUBJECT_WORDS[@]}" -gt 1 267 | test $? -eq 0 || add_warning 1 "Do no write single worded commits" 268 | 269 | # 9. Do not start the subject line with whitespace 270 | # ------------------------------------------------------------------------------ 271 | 272 | [[ ${COMMIT_SUBJECT_TO_PROCESS} =~ ^[[:blank:]]+ ]] 273 | test $? -eq 1 || add_warning 1 "Do not start the subject line with whitespace" 274 | } 275 | 276 | # 277 | # It's showtime. 278 | # 279 | 280 | set_colors 281 | 282 | set_editor 283 | 284 | if tty >/dev/null 2>&1; then 285 | TTY=$(tty) 286 | else 287 | TTY=/dev/tty 288 | fi 289 | 290 | while true; do 291 | 292 | read_commit_message 293 | 294 | validate_commit_message 295 | 296 | # if there are no WARNINGS are empty then we're good to break out of here 297 | test ${#WARNINGS[@]} -eq 0 && exit 0; 298 | 299 | display_warnings 300 | 301 | # Ask the question (not using "read -p" as it uses stderr not stdout) 302 | echo -en "${CYAN}Proceed with commit? [e/n/?] ${NC}" 303 | 304 | # Read the answer 305 | read REPLY < "$TTY" 306 | 307 | # Check if the reply is valid 308 | case "$REPLY" in 309 | E*|e*) $HOOK_EDITOR "$COMMIT_MSG_FILE" < $TTY; continue ;; 310 | N*|n*) exit 1 ;; 311 | *) SKIP_DISPLAY_WARNINGS=1; prompt_help; continue ;; 312 | esac 313 | 314 | done 315 | -------------------------------------------------------------------------------- /htstress.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020, 2022 National Cheng Kung University, Taiwan. 3 | * Copyright (C) 2011-2012 Roman Arutyunyan (arut@qip.ru) 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 17 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 | * EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 22 | * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 24 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 25 | * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | /* 29 | * htstress - Fast HTTP Benchmarking tool 30 | */ 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | 53 | typedef void (*sighandler_t)(int); 54 | 55 | #define HTTP_REQUEST_PREFIX "http://" 56 | 57 | #define HTTP_REQUEST_FMT "GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" 58 | 59 | #define HTTP_REQUEST_DEBUG 0x01 60 | #define HTTP_RESPONSE_DEBUG 0x02 61 | 62 | #define INBUFSIZE 1024 63 | 64 | #define BAD_REQUEST 0x1 65 | 66 | #define MAX_EVENTS 256 67 | 68 | /** 69 | * struct econn - represent one connection to server from htstress. 70 | * @fd: client fd 71 | * @offs: the buffer offset represents data already read from or sent to 72 | * buffer. 73 | * @flags: flags represents the http response status from server, only used with 74 | * BAD_REQUEST. 75 | */ 76 | struct econn { 77 | int fd; 78 | size_t offs; 79 | int flags; 80 | }; 81 | 82 | static char *outbuf; 83 | static size_t outbufsize; 84 | 85 | static struct sockaddr_storage sss; 86 | static socklen_t sssln = 0; 87 | 88 | static int concurrency = 1; 89 | static int num_threads = 1; 90 | 91 | static char *udaddr = ""; 92 | 93 | static volatile _Atomic uint64_t num_requests = 0; 94 | static volatile uint64_t max_requests = 0; 95 | static volatile _Atomic uint64_t good_requests = 0; 96 | static volatile _Atomic uint64_t bad_requests = 0; 97 | static volatile _Atomic uint64_t socket_errors = 0; 98 | static volatile uint64_t in_bytes = 0; 99 | static volatile uint64_t out_bytes = 0; 100 | 101 | static uint64_t ticks; 102 | 103 | static int debug = 0; 104 | static int exit_i = 0; 105 | 106 | static struct timeval tv, tve; 107 | 108 | static const char short_options[] = "n:c:t:u:h:d46"; 109 | 110 | static const struct option long_options[] = { 111 | {"number", 1, NULL, 'n'}, {"concurrency", 1, NULL, 'c'}, 112 | {"threads", 0, NULL, 't'}, {"udaddr", 1, NULL, 'u'}, 113 | {"host", 1, NULL, 'h'}, {"debug", 0, NULL, 'd'}, 114 | {"help", 0, NULL, '%'}, {NULL, 0, NULL, 0}, 115 | }; 116 | 117 | static void sigint_handler(int arg) 118 | { 119 | (void) arg; 120 | max_requests = num_requests; 121 | } 122 | 123 | static void start_time() 124 | { 125 | if (gettimeofday(&tv, NULL)) { 126 | perror("gettimeofday"); 127 | exit(1); 128 | } 129 | } 130 | 131 | static void end_time() 132 | { 133 | if (gettimeofday(&tve, NULL)) { 134 | perror("gettimeofday"); 135 | exit(1); 136 | } 137 | } 138 | 139 | /* init_conn - initialize new econn or reset closed econn, then put into the 140 | * epoll fd. 141 | * @efd: epoll fd 142 | * @ec: empty or closed econn 143 | */ 144 | static void init_conn(int efd, struct econn *ec) 145 | { 146 | int ret; 147 | 148 | ec->fd = socket(sss.ss_family, SOCK_STREAM, 0); 149 | ec->offs = 0; 150 | ec->flags = 0; 151 | 152 | if (ec->fd == -1) { 153 | perror("socket() failed"); 154 | exit(1); 155 | } 156 | 157 | /* manipulate fd, F_SETFL: set file status flags */ 158 | fcntl(ec->fd, F_SETFL, O_NONBLOCK); 159 | 160 | do { 161 | /* not accept(server), so it's not conn fd(server side) but client fd. 162 | * 163 | * If the connection or binding succeeds, zero is returned. On 164 | * error, -1 is returned, and errno is set to indicate the error. 165 | */ 166 | ret = connect(ec->fd, (struct sockaddr *) &sss, sssln); 167 | /* set socket to O_NONBLOCK, connect function never blocks, so checking 168 | * EAGAIN is needed.*/ 169 | } while (ret && errno == EAGAIN); 170 | 171 | if (ret && errno != EINPROGRESS) { 172 | perror("connect() failed"); 173 | exit(1); 174 | } 175 | 176 | struct epoll_event evt = { 177 | .events = EPOLLOUT, 178 | .data.ptr = ec, 179 | }; 180 | 181 | /* add client fd into epoll fd */ 182 | if (epoll_ctl(efd, EPOLL_CTL_ADD, ec->fd, &evt)) { 183 | perror("epoll_ctl"); 184 | exit(1); 185 | } 186 | } 187 | 188 | /* worker - hold epoll logic to handle http request and response. 189 | * @arg: no use 190 | * 191 | * one worker manage concurrency number connections. 192 | * htstress creates thread per worker, so the num_threads also means how many 193 | * workers we have. 194 | */ 195 | static void *worker(void *arg) 196 | { 197 | int ret, nevts; 198 | struct epoll_event evts[MAX_EVENTS]; 199 | char inbuf[INBUFSIZE]; 200 | struct econn ecs[concurrency], *ec; 201 | 202 | (void) arg; 203 | 204 | int efd = epoll_create(concurrency); 205 | if (efd == -1) { 206 | perror("epoll"); 207 | exit(1); 208 | } 209 | 210 | /* each worker has concurrency number econn */ 211 | for (int n = 0; n < concurrency; ++n) 212 | init_conn(efd, ecs + n); 213 | 214 | for (;;) { 215 | do { 216 | nevts = epoll_wait(efd, evts, sizeof(evts) / sizeof(evts[0]), -1); 217 | } while (!exit_i && nevts < 0 && errno == EINTR); 218 | 219 | if (exit_i != 0) { 220 | exit(0); 221 | } 222 | 223 | if (nevts == -1) { 224 | perror("epoll_wait"); 225 | exit(1); 226 | } 227 | 228 | for (int n = 0; n < nevts; ++n) { 229 | ec = (struct econn *) evts[n].data.ptr; 230 | 231 | if (!ec) { 232 | fprintf(stderr, "fatal: NULL econn\n"); 233 | exit(1); 234 | } 235 | 236 | if (evts[n].events & EPOLLERR) { 237 | /* normally this should not happen */ 238 | static unsigned int number_of_errors_logged = 0; 239 | int error = 0; 240 | socklen_t errlen = sizeof(error); 241 | number_of_errors_logged += 1; 242 | if (getsockopt(efd, SOL_SOCKET, SO_ERROR, (void *) &error, 243 | &errlen) == 0) { 244 | fprintf(stderr, "error = %s\n", strerror(error)); 245 | } 246 | if (number_of_errors_logged % 100 == 0) { 247 | fprintf(stderr, "EPOLLERR caused by unknown error\n"); 248 | } 249 | atomic_fetch_add(&socket_errors, 1); 250 | close(ec->fd); 251 | if (num_requests > max_requests) 252 | continue; 253 | init_conn(efd, ec); 254 | continue; 255 | } 256 | 257 | if (evts[n].events & EPOLLHUP) { 258 | /* This can happen for HTTP/1.0 */ 259 | fprintf(stderr, "EPOLLHUP\n"); 260 | exit(1); 261 | } 262 | 263 | if (evts[n].events & EPOLLOUT) { 264 | ret = send(ec->fd, outbuf + ec->offs, outbufsize - ec->offs, 0); 265 | 266 | if (ret == -1 && errno != EAGAIN) { 267 | /* TODO: something better than this */ 268 | perror("send"); 269 | exit(1); 270 | } 271 | 272 | if (ret > 0) { 273 | if (debug & HTTP_REQUEST_DEBUG) 274 | write(STDERR_FILENO, outbuf + ec->offs, 275 | outbufsize - ec->offs); 276 | 277 | ec->offs += ret; 278 | 279 | /* write done? schedule read */ 280 | if (ec->offs == outbufsize) { 281 | evts[n].events = EPOLLIN; 282 | evts[n].data.ptr = ec; 283 | 284 | ec->offs = 0; 285 | 286 | if (epoll_ctl(efd, EPOLL_CTL_MOD, ec->fd, evts + n)) { 287 | perror("epoll_ctl"); 288 | exit(1); 289 | } 290 | } 291 | } 292 | 293 | } else if (evts[n].events & EPOLLIN) { 294 | for (;;) { 295 | ret = recv(ec->fd, inbuf, sizeof(inbuf), 0); 296 | 297 | if (ret == -1 && errno != EAGAIN) { 298 | perror("recv"); 299 | exit(1); 300 | } 301 | 302 | if (ret <= 0) 303 | break; 304 | /* check http response status code is 4XX or 5XX */ 305 | if (ec->offs <= 9 && ec->offs + ret > 10) { 306 | char c = inbuf[9 - ec->offs]; 307 | if (c == '4' || c == '5') 308 | ec->flags |= BAD_REQUEST; 309 | } 310 | 311 | if (debug & HTTP_RESPONSE_DEBUG) 312 | write(2, inbuf, ret); 313 | 314 | ec->offs += ret; 315 | } 316 | 317 | if (!ret) { 318 | close(ec->fd); 319 | 320 | int m = atomic_fetch_add(&num_requests, 1); 321 | 322 | if (max_requests && (m + 1 > (int) max_requests)) 323 | atomic_fetch_sub(&num_requests, 1); 324 | else if (ec->flags & BAD_REQUEST) 325 | atomic_fetch_add(&bad_requests, 1); 326 | else 327 | atomic_fetch_add(&good_requests, 1); 328 | 329 | if (max_requests && (m + 1 >= (int) max_requests)) { 330 | end_time(); 331 | return NULL; 332 | } 333 | 334 | if (ticks && m % ticks == 0) 335 | printf("%d requests\n", m); 336 | 337 | init_conn(efd, ec); 338 | } 339 | } 340 | } 341 | } 342 | } 343 | 344 | static void signal_exit(int signal) 345 | { 346 | (void) signal; 347 | exit_i++; 348 | } 349 | 350 | static void print_usage() 351 | { 352 | printf( 353 | "Usage: htstress [options] [http://]hostname[:port]/path\n" 354 | "Options:\n" 355 | " -n, --number total number of requests (0 for infinite, " 356 | "Ctrl-C to abort)\n" 357 | " -c, --concurrency number of concurrent connections\n" 358 | " -t, --threads number of threads (set this to the number of " 359 | "CPU cores)\n" 360 | " -u, --udaddr path to unix domain socket\n" 361 | " -h, --host host to use for http request\n" 362 | " -d, --debug debug HTTP response\n" 363 | " --help display this message\n"); 364 | exit(0); 365 | } 366 | 367 | int main(int argc, char *argv[]) 368 | { 369 | pthread_t useless_thread; 370 | const char *host = NULL; 371 | char *node = NULL; 372 | char *port = "http"; 373 | struct sockaddr_in *ssin = (struct sockaddr_in *) &sss; 374 | struct sockaddr_in6 *ssin6 = (struct sockaddr_in6 *) &sss; 375 | struct sockaddr_un *ssun = (struct sockaddr_un *) &sss; 376 | struct addrinfo *result, *rp; 377 | struct addrinfo hints; 378 | 379 | sighandler_t ret = signal(SIGINT, signal_exit); 380 | if (ret == SIG_ERR) { 381 | perror("signal(SIGINT, handler)"); 382 | exit(0); 383 | } 384 | 385 | ret = signal(SIGTERM, signal_exit); 386 | if (ret == SIG_ERR) { 387 | perror("signal(SIGTERM, handler)"); 388 | exit(0); 389 | } 390 | 391 | memset(&hints, 0, sizeof(struct addrinfo)); 392 | hints.ai_family = AF_UNSPEC; 393 | hints.ai_socktype = SOCK_STREAM; 394 | hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG; 395 | 396 | memset(&sss, 0, sizeof(struct sockaddr_storage)); 397 | 398 | if (argc == 1) 399 | print_usage(); 400 | 401 | int next_option; 402 | do { 403 | next_option = 404 | getopt_long(argc, argv, short_options, long_options, NULL); 405 | 406 | switch (next_option) { 407 | case 'n': 408 | max_requests = strtoull(optarg, 0, 10); 409 | break; 410 | case 'c': 411 | concurrency = atoi(optarg); 412 | break; 413 | case 't': 414 | num_threads = atoi(optarg); 415 | break; 416 | case 'u': 417 | udaddr = optarg; 418 | break; 419 | case 'd': 420 | debug = 0x03; 421 | break; 422 | case 'h': 423 | host = optarg; 424 | break; 425 | case '4': 426 | hints.ai_family = PF_INET; 427 | break; 428 | case '6': 429 | hints.ai_family = PF_INET6; 430 | break; 431 | case '%': 432 | print_usage(); 433 | case -1: 434 | break; 435 | default: 436 | printf("Unexpected argument: '%c'\n", next_option); 437 | return 1; 438 | } 439 | } while (next_option != -1); 440 | 441 | if (optind >= argc) { 442 | printf("Missing URL\n"); 443 | return 1; 444 | } 445 | 446 | /* parse URL */ 447 | char *s = argv[optind]; 448 | if (!strncmp(s, HTTP_REQUEST_PREFIX, sizeof(HTTP_REQUEST_PREFIX) - 1)) 449 | s += (sizeof(HTTP_REQUEST_PREFIX) - 1); 450 | node = s; 451 | 452 | char *rq = strpbrk(s, ":/"); 453 | if (!rq) 454 | rq = "/"; 455 | else if (*rq == '/') { 456 | node = strndup(s, rq - s); 457 | if (!node) { 458 | perror("node = strndup(s, rq - s)"); 459 | exit(EXIT_FAILURE); 460 | } 461 | } else if (*rq == ':') { 462 | *rq++ = 0; 463 | port = rq; 464 | rq = strchr(rq, '/'); 465 | if (rq) { 466 | if (*rq == '/') { 467 | port = strndup(port, rq - port); 468 | if (!port) { 469 | perror("port = strndup(rq, rq - port)"); 470 | exit(EXIT_FAILURE); 471 | } 472 | } else 473 | rq = "/"; 474 | } 475 | } 476 | 477 | if (strnlen(udaddr, sizeof(ssun->sun_path) - 1) == 0) { 478 | int j = getaddrinfo(node, port, &hints, &result); 479 | if (j) { 480 | fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(j)); 481 | exit(EXIT_FAILURE); 482 | } 483 | 484 | for (rp = result; rp; rp = rp->ai_next) { 485 | int testfd = 486 | socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); 487 | if (testfd == -1) 488 | continue; 489 | 490 | if (connect(testfd, rp->ai_addr, rp->ai_addrlen) == 0) { 491 | close(testfd); 492 | break; 493 | } 494 | 495 | close(testfd); 496 | } 497 | 498 | if (!rp) { /* No address succeeded */ 499 | fprintf(stderr, "getaddrinfo failed\n"); 500 | exit(EXIT_FAILURE); 501 | } 502 | 503 | if (rp->ai_addr->sa_family == PF_INET) { 504 | *ssin = *(struct sockaddr_in *) rp->ai_addr; 505 | } else if (rp->ai_addr->sa_family == PF_INET6) { 506 | *ssin6 = *(struct sockaddr_in6 *) rp->ai_addr; 507 | } else { 508 | fprintf(stderr, "invalid family %d from getaddrinfo\n", 509 | rp->ai_addr->sa_family); 510 | exit(EXIT_FAILURE); 511 | } 512 | sssln = rp->ai_addrlen; 513 | 514 | freeaddrinfo(result); 515 | } else { 516 | ssun->sun_family = PF_UNIX; 517 | strncpy(ssun->sun_path, udaddr, sizeof(ssun->sun_path) - 1); 518 | sssln = sizeof(struct sockaddr_un); 519 | } 520 | 521 | /* prepare request buffer */ 522 | if (!host) 523 | host = node; 524 | outbufsize = sizeof(HTTP_REQUEST_FMT) + strlen(host); 525 | outbufsize += rq ? strlen(rq) : 1; 526 | 527 | outbuf = malloc(outbufsize); 528 | outbufsize = 529 | snprintf(outbuf, outbufsize, HTTP_REQUEST_FMT, rq ? rq : "/", host); 530 | 531 | ticks = max_requests / 10; 532 | 533 | signal(SIGINT, &sigint_handler); 534 | 535 | if (!max_requests) { 536 | ticks = 1000; 537 | printf("[Press Ctrl-C to finish]\n"); 538 | } 539 | 540 | start_time(); 541 | 542 | /* run test */ 543 | for (int n = 0; n < num_threads - 1; ++n) 544 | pthread_create(&useless_thread, 0, &worker, 0); 545 | 546 | worker(0); 547 | 548 | /* output result */ 549 | double delta = 550 | tve.tv_sec - tv.tv_sec + ((double) (tve.tv_usec - tv.tv_usec)) / 1e6; 551 | 552 | printf( 553 | "\n" 554 | "requests: %" PRIu64 555 | "\n" 556 | "good requests: %" PRIu64 557 | " [%d%%]\n" 558 | "bad requests: %" PRIu64 559 | " [%d%%]\n" 560 | "socket errors: %" PRIu64 561 | " [%d%%]\n" 562 | "seconds: %.3f\n" 563 | "requests/sec: %.3f\n" 564 | "\n", 565 | num_requests, good_requests, 566 | (int) (num_requests ? good_requests * 100 / num_requests : 0), 567 | bad_requests, 568 | (int) (num_requests ? bad_requests * 100 / num_requests : 0), 569 | socket_errors, 570 | (int) (num_requests ? socket_errors * 100 / num_requests : 0), delta, 571 | delta > 0 ? max_requests / delta : 0); 572 | 573 | return 0; 574 | } 575 | --------------------------------------------------------------------------------