├── example ├── html │ ├── css │ │ └── main.css │ └── index.html ├── cookie.sh ├── test_cookie_get_post.sh ├── websocket │ └── test.sh └── example.sh ├── LICENSE ├── README.md ├── mime.types ├── bash-patches ├── accept.patch └── accept.c └── bash-server.sh /example/html/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: blue 3 | } 4 | -------------------------------------------------------------------------------- /example/cookie.sh: -------------------------------------------------------------------------------- 1 | runner(){ 2 | cookieSet "trii=lek; Max-Age=5000" 3 | sessionStart 4 | 5 | sessionGet "lelek" 6 | echo cookie 7 | } 8 | -------------------------------------------------------------------------------- /example/test_cookie_get_post.sh: -------------------------------------------------------------------------------- 1 | runner(){ 2 | declare -p POST 3 | declare -p GET 4 | declare -p COOKIE 5 | declare -p HTTP_HEADERS 6 | } 7 | -------------------------------------------------------------------------------- /example/websocket/test.sh: -------------------------------------------------------------------------------- 1 | runner(){ 2 | websocketStart websocketRunner 3 | } 4 | 5 | websocketRunner(){ 6 | printf '%s' "From WebSocket: hello world" 7 | 8 | } 9 | -------------------------------------------------------------------------------- /example/example.sh: -------------------------------------------------------------------------------- 1 | runner(){ 2 | if [[ $REQUEST_PATH == "/1" ]]; then 3 | echo "it's working" 4 | sleep 5 5 | echo "10" 6 | else 7 | echo cool 8 | fi 9 | } 10 | -------------------------------------------------------------------------------- /example/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Hello Bash Worl

8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 dzove855 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bash-web-server 2 | A purely bash web server, no socat, netcat, etc... 3 | 4 | # Requirement 5 | * bash 5.2 (The patch will be included in bash5.2 - Alpha release already contains the patch) 6 | * if bash version is under 5.2, patched loadable accept builtin (http://git.savannah.gnu.org/cgit/bash.git/tree/examples/loadables/accept.c) is needed, you have to apply accept.patch to your loadable accept.c file which is existed in bash source code, and than build and install the loadable accept builtin into BASH_LOADABLE_PATH specified in `bash-server.sh` 7 | 8 | # How to 9 | The port can be set by the env var: HTTP_PORT 10 | 11 | The path to accept (Directory) can be set by using: BASH_LOADABLES_PATH (see man bash) 12 | 13 | Server Methods: 14 | * serveHtml (needs DOCUMENT_ROOT envvars) - This will serve the static files 15 | * script file - The script need a file as first argument which will be source. The file will need a function named runner, which will be run on each request 16 | 17 | Basic authentication can be enabled by env var: BASIC_AUTH, accounts and passwords are stored in the file specified in $BASIC_AUTH_FILE 18 | 19 | 20 | # Usage 21 | Simple explication of various functions that could be used. 22 | 23 | ## Session Handling 24 | Variables: 25 | 26 | ``` 27 | SESSION_COOKIE 28 | The name of the cookie : default BASHSESSID 29 | ``` 30 | 31 | Functions: 32 | 33 | ``` 34 | sessionStart 35 | Start a session or reuse an existing session 36 | 37 | sessionSet $1 $2 38 | Set a session variable 39 | 40 | sessionGet $1 41 | Get the value of the given variable 42 | ``` 43 | 44 | ## Cookie Handling 45 | Functions: 46 | 47 | ``` 48 | cookieSet $1 49 | Send the cookie 50 | Example: cookieSet "BASHSESSID=12345; max-age=5000" 51 | ``` 52 | 53 | ## HTTP Handling 54 | Functions: 55 | 56 | ``` 57 | httpSendStatus $1 58 | Send the provided http status 59 | Example: httpSendStatus 200 60 | 61 | To set Headers, you should add an entry inside the assoc var HTTP_RESPONSE_HEADERS 62 | HTTP_RESPONSE_HEADERS["ExampleHeader"]="The value of the Header" 63 | ``` 64 | 65 | ## Websocket 66 | !!!NOTE: This is still expiremental and only works for sendin data, not receiving! 67 | 68 | Functions: 69 | ``` 70 | websocketStart FUNCTION 71 | Start the websocket server 72 | 73 | websocketStop 74 | Stop the websocket server inside the function 75 | ``` 76 | -------------------------------------------------------------------------------- /mime.types: -------------------------------------------------------------------------------- 1 | text/html html htm shtml 2 | text/css css 3 | text/xml xml 4 | image/gif gif 5 | image/jpeg jpeg jpg 6 | application/javascript js 7 | application/atom+xml atom 8 | application/rss+xml rss 9 | text/mathml mml 10 | text/plain txt 11 | text/vnd.sun.j2me.app-descriptor jad 12 | text/vnd.wap.wml wml 13 | text/x-component htc 14 | image/avif avif 15 | image/png png 16 | image/svg+xml svg svgz 17 | image/tiff tif tiff 18 | image/vnd.wap.wbmp wbmp 19 | image/webp webp 20 | image/x-icon ico 21 | image/x-jng jng 22 | image/x-ms-bmp bmp 23 | font/woff woff 24 | font/woff2 woff2 25 | application/java-archive jar war ear 26 | application/json json 27 | application/mac-binhex40 hqx 28 | application/msword doc 29 | application/pdf pdf 30 | application/postscript ps eps ai 31 | application/rtf rtf 32 | application/vnd.apple.mpegurl m3u8 33 | application/vnd.google-earth.kml+xml kml 34 | application/vnd.google-earth.kmz kmz 35 | application/vnd.ms-excel xls 36 | application/vnd.ms-fontobject eot 37 | application/vnd.ms-powerpoint ppt 38 | application/vnd.oasis.opendocument.graphics odg 39 | application/vnd.oasis.opendocument.presentation odp 40 | application/vnd.oasis.opendocument.spreadsheet ods 41 | application/vnd.oasis.opendocument.text odt 42 | application/vnd.openxmlformats-officedocument.presentationml.presentation pptx 43 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx 44 | application/vnd.openxmlformats-officedocument.wordprocessingml.document docx 45 | application/vnd.wap.wmlc wmlc 46 | application/wasm wasm 47 | application/x-7z-compressed 7z 48 | application/x-cocoa cco 49 | application/x-java-archive-diff jardiff 50 | application/x-java-jnlp-file jnlp 51 | application/x-makeself run 52 | application/x-perl pl pm 53 | application/x-pilot prc pdb 54 | application/x-rar-compressed rar 55 | application/x-redhat-package-manager rpm 56 | application/x-sea sea 57 | application/x-shockwave-flash swf 58 | application/x-stuffit sit 59 | application/x-tcl tcl tk 60 | application/x-x509-ca-cert der pem crt 61 | application/x-xpinstall xpi 62 | application/xhtml+xml xhtml 63 | application/xspf+xml xspf 64 | application/zip zip 65 | application/octet-stream bin exe dll 66 | application/octet-stream deb 67 | application/octet-stream dmg 68 | application/octet-stream iso img 69 | application/octet-stream msi msp msm 70 | audio/midi mid midi kar 71 | audio/mpeg mp3 72 | audio/ogg ogg 73 | audio/x-m4a m4a 74 | audio/x-realaudio ra 75 | video/3gpp 3gpp 3gp 76 | video/mp2t ts 77 | video/mp4 mp4 78 | video/mpeg mpeg mpg 79 | video/quicktime mov 80 | video/webm webm 81 | video/x-flv flv 82 | video/x-m4v m4v 83 | video/x-mng mng 84 | video/x-ms-asf asx asf 85 | video/x-ms-wmv wmv 86 | video/x-msvideo avi 87 | 88 | -------------------------------------------------------------------------------- /bash-patches/accept.patch: -------------------------------------------------------------------------------- 1 | --- bash/examples/loadables/accept.c 2022-01-07 11:54:11.147298900 +0100 2 | +++ /home/dzove855/bash/examples/loadables/accept.c 2022-01-07 11:29:17.837298900 +0100 3 | @@ -48,7 +48,7 @@ 4 | SHELL_VAR *v; 5 | intmax_t iport; 6 | int opt; 7 | - char *tmoutarg, *fdvar, *rhostvar, *rhost; 8 | + char *tmoutarg, *fdvar, *rhostvar, *rhost, *bindaddress; 9 | unsigned short uport; 10 | int servsock, clisock; 11 | struct sockaddr_in server, client; 12 | @@ -56,27 +56,30 @@ 13 | struct timeval timeval; 14 | struct linger linger = { 0, 0 }; 15 | 16 | - rhostvar = tmoutarg = fdvar = rhost = (char *)NULL; 17 | + rhostvar = tmoutarg = fdvar = rhost = bindaddress = (char *)NULL; 18 | 19 | reset_internal_getopt (); 20 | - while ((opt = internal_getopt (list, "r:t:v:")) != -1) 21 | + while ((opt = internal_getopt (list, "b:r:t:v:")) != -1) 22 | { 23 | switch (opt) 24 | - { 25 | - case 'r': 26 | - rhostvar = list_optarg; 27 | - break; 28 | - case 't': 29 | - tmoutarg = list_optarg; 30 | - break; 31 | - case 'v': 32 | - fdvar = list_optarg; 33 | - break; 34 | - CASE_HELPOPT; 35 | - default: 36 | - builtin_usage (); 37 | - return (EX_USAGE); 38 | - } 39 | + { 40 | + case 'b': 41 | + bindaddress = list_optarg; 42 | + break; 43 | + case 'r': 44 | + rhostvar = list_optarg; 45 | + break; 46 | + case 't': 47 | + tmoutarg = list_optarg; 48 | + break; 49 | + case 'v': 50 | + fdvar = list_optarg; 51 | + break; 52 | + CASE_HELPOPT; 53 | + default: 54 | + builtin_usage (); 55 | + return (EX_USAGE); 56 | + } 57 | } 58 | 59 | list = loptend; 60 | @@ -87,10 +90,10 @@ 61 | long ival, uval; 62 | opt = uconvert (tmoutarg, &ival, &uval, (char **)0); 63 | if (opt == 0 || ival < 0 || uval < 0) 64 | - { 65 | - builtin_error ("%s: invalid timeout specification", tmoutarg); 66 | - return (EXECUTION_FAILURE); 67 | - } 68 | + { 69 | + builtin_error ("%s: invalid timeout specification", tmoutarg); 70 | + return (EXECUTION_FAILURE); 71 | + } 72 | timeval.tv_sec = ival; 73 | timeval.tv_usec = uval; 74 | /* XXX - should we warn if ival == uval == 0 ? */ 75 | @@ -125,7 +128,16 @@ 76 | memset ((char *)&server, 0, sizeof (server)); 77 | server.sin_family = AF_INET; 78 | server.sin_port = htons(uport); 79 | - server.sin_addr.s_addr = htonl(INADDR_ANY); 80 | + if (bindaddress) { 81 | + server.sin_addr.s_addr = inet_addr(bindaddress); 82 | + } else { 83 | + server.sin_addr.s_addr = htonl(INADDR_ANY); 84 | + } 85 | + 86 | + opt = 1; 87 | + setsockopt (servsock, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof (opt)); 88 | +/* setsockopt (servsock, SOL_SOCKET, SO_REUSEPORT, (void *)&opt, sizeof(opt)); 89 | + setsockopt (servsock, SOL_SOCKET, SO_LINGER, (void *)&linger, sizeof (linger));*/ 90 | 91 | if (bind (servsock, (struct sockaddr *)&server, sizeof (server)) < 0) 92 | { 93 | @@ -134,10 +146,6 @@ 94 | return (EXECUTION_FAILURE); 95 | } 96 | 97 | - opt = 1; 98 | - setsockopt (servsock, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof (opt)); 99 | - setsockopt (servsock, SOL_SOCKET, SO_LINGER, (void *)&linger, sizeof (linger)); 100 | - 101 | if (listen (servsock, 1) < 0) 102 | { 103 | builtin_error ("listen failure: %s", strerror (errno)); 104 | @@ -178,9 +186,10 @@ 105 | rhost = inet_ntoa (client.sin_addr); 106 | v = builtin_bind_variable (rhostvar, rhost, 0); 107 | if (v == 0 || readonly_p (v) || noassign_p (v)) 108 | - builtin_error ("%s: cannot set variable", rhostvar); 109 | + builtin_error ("%s: cannot set variable", rhostvar); 110 | } 111 | 112 | + 113 | return (EXECUTION_SUCCESS); 114 | } 115 | 116 | @@ -200,35 +209,36 @@ 117 | } 118 | 119 | char *accept_doc[] = { 120 | - "Accept a network connection on a specified port.", 121 | - "" 122 | - "This builtin allows a bash script to act as a TCP/IP server.", 123 | - "", 124 | - "Options, if supplied, have the following meanings:", 125 | - " -t timeout wait TIMEOUT seconds for a connection. TIMEOUT may", 126 | - " be a decimal number including a fractional portion", 127 | - " -v varname store the numeric file descriptor of the connected", 128 | - " socket into VARNAME. The default VARNAME is ACCEPT_FD", 129 | - " -r rhost store the IP address of the remote host into the shell", 130 | - " variable RHOST, in dotted-decimal notation", 131 | - "", 132 | - "If successful, the shell variable ACCEPT_FD, or the variable named by the", 133 | - "-v option, will be set to the fd of the connected socket, suitable for", 134 | - "use as 'read -u$ACCEPT_FD'. RHOST, if supplied, will hold the IP address", 135 | - "of the remote client. The return status is 0.", 136 | - "", 137 | - "On failure, the return status is 1 and ACCEPT_FD (or VARNAME) and RHOST,", 138 | - "if supplied, will be unset.", 139 | - "", 140 | - "The server socket fd will be closed before accept returns.", 141 | - (char *) NULL 142 | + "Accept a network connection on a specified port.", 143 | + "" 144 | + "This builtin allows a bash script to act as a TCP/IP server.", 145 | + "", 146 | + "Options, if supplied, have the following meanings:", 147 | + " -b bindadress set the ip on which we should liste, default is any", 148 | + " -t timeout wait TIMEOUT seconds for a connection. TIMEOUT may", 149 | + " be a decimal number including a fractional portion", 150 | + " -v varname store the numeric file descriptor of the connected", 151 | + " socket into VARNAME. The default VARNAME is ACCEPT_FD", 152 | + " -r rhost store the IP address of the remote host into the shell", 153 | + " variable RHOST, in dotted-decimal notation", 154 | + "", 155 | + "If successful, the shell variable ACCEPT_FD, or the variable named by the", 156 | + "-v option, will be set to the fd of the connected socket, suitable for", 157 | + "use as 'read -u$ACCEPT_FD'. RHOST, if supplied, will hold the IP address", 158 | + "of the remote client. The return status is 0.", 159 | + "", 160 | + "On failure, the return status is 1 and ACCEPT_FD (or VARNAME) and RHOST,", 161 | + "if supplied, will be unset.", 162 | + "", 163 | + "The server socket fd will be closed before accept returns.", 164 | + (char *) NULL 165 | }; 166 | 167 | struct builtin accept_struct = { 168 | - "accept", /* builtin name */ 169 | - accept_builtin, /* function implementing the builtin */ 170 | - BUILTIN_ENABLED, /* initial flags for builtin */ 171 | - accept_doc, /* array of long documentation strings. */ 172 | - "accept [-t timeout] [-v varname] [-r addrvar ] port", /* usage synopsis; becomes short_doc */ 173 | - 0 /* reserved for internal use */ 174 | + "accept", /* builtin name */ 175 | + accept_builtin, /* function implementing the builtin */ 176 | + BUILTIN_ENABLED, /* initial flags for builtin */ 177 | + accept_doc, /* array of long documentation strings. */ 178 | + "accept [-b bindaddress] [-t timeout] [-v varname] [-r addrvar ] port", /* usage synopsis; becomes short_doc */ 179 | + 0 /* reserved for internal use */ 180 | }; 181 | -------------------------------------------------------------------------------- /bash-patches/accept.c: -------------------------------------------------------------------------------- 1 | /* accept - listen for and accept a remote network connection on a given port */ 2 | 3 | /* 4 | Copyright (C) 2020 Free Software Foundation, Inc. 5 | 6 | This file is part of GNU Bash. 7 | Bash is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | Bash is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with Bash. If not, see . 19 | */ 20 | 21 | #include 22 | 23 | #if defined (HAVE_UNISTD_H) 24 | # include 25 | #endif 26 | 27 | #include 28 | #include 29 | #include 30 | #include "bashtypes.h" 31 | #include 32 | #include 33 | #include "typemax.h" 34 | 35 | #include 36 | #include 37 | #include 38 | 39 | #include "loadables.h" 40 | 41 | static int accept_bind_variable (char *, int); 42 | 43 | int 44 | accept_builtin (list) 45 | WORD_LIST *list; 46 | { 47 | WORD_LIST *l; 48 | SHELL_VAR *v; 49 | intmax_t iport; 50 | int opt; 51 | char *tmoutarg, *fdvar, *rhostvar, *rhost, *bindaddress; 52 | unsigned short uport; 53 | int servsock, clisock; 54 | struct sockaddr_in server, client; 55 | socklen_t clientlen; 56 | struct timeval timeval; 57 | struct linger linger = { 0, 0 }; 58 | 59 | rhostvar = tmoutarg = fdvar = rhost = bindaddress = (char *)NULL; 60 | 61 | reset_internal_getopt (); 62 | while ((opt = internal_getopt (list, "b:r:t:v:")) != -1) 63 | { 64 | switch (opt) 65 | { 66 | case 'b': 67 | bindaddress = list_optarg; 68 | break; 69 | case 'r': 70 | rhostvar = list_optarg; 71 | break; 72 | case 't': 73 | tmoutarg = list_optarg; 74 | break; 75 | case 'v': 76 | fdvar = list_optarg; 77 | break; 78 | CASE_HELPOPT; 79 | default: 80 | builtin_usage (); 81 | return (EX_USAGE); 82 | } 83 | } 84 | 85 | list = loptend; 86 | 87 | /* Validate input and variables */ 88 | if (tmoutarg) 89 | { 90 | long ival, uval; 91 | opt = uconvert (tmoutarg, &ival, &uval, (char **)0); 92 | if (opt == 0 || ival < 0 || uval < 0) 93 | { 94 | builtin_error ("%s: invalid timeout specification", tmoutarg); 95 | return (EXECUTION_FAILURE); 96 | } 97 | timeval.tv_sec = ival; 98 | timeval.tv_usec = uval; 99 | /* XXX - should we warn if ival == uval == 0 ? */ 100 | } 101 | 102 | if (list == 0) 103 | { 104 | builtin_usage (); 105 | return (EX_USAGE); 106 | } 107 | 108 | if (legal_number (list->word->word, &iport) == 0 || iport < 0 || iport > TYPE_MAXIMUM (unsigned short)) 109 | { 110 | builtin_error ("%s: invalid port number", list->word->word); 111 | return (EXECUTION_FAILURE); 112 | } 113 | uport = (unsigned short)iport; 114 | 115 | if (fdvar == 0) 116 | fdvar = "ACCEPT_FD"; 117 | 118 | unbind_variable (fdvar); 119 | if (rhostvar) 120 | unbind_variable (rhostvar); 121 | 122 | if ((servsock = socket (AF_INET, SOCK_STREAM, IPPROTO_IP)) < 0) 123 | { 124 | builtin_error ("cannot create socket: %s", strerror (errno)); 125 | return (EXECUTION_FAILURE); 126 | } 127 | 128 | memset ((char *)&server, 0, sizeof (server)); 129 | server.sin_family = AF_INET; 130 | server.sin_port = htons(uport); 131 | if (bindaddress) { 132 | server.sin_addr.s_addr = inet_addr(bindaddress); 133 | } else { 134 | server.sin_addr.s_addr = htonl(INADDR_ANY); 135 | } 136 | 137 | opt = 1; 138 | setsockopt (servsock, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof (opt)); 139 | 140 | /* No need of SO_LINGER 141 | setsockopt (servsock, SOL_SOCKET, SO_LINGER, (void *)&linger, sizeof (linger)); 142 | */ 143 | 144 | if (bind (servsock, (struct sockaddr *)&server, sizeof (server)) < 0) 145 | { 146 | builtin_error ("socket bind failure: %s", strerror (errno)); 147 | close (servsock); 148 | return (EXECUTION_FAILURE); 149 | } 150 | 151 | if (listen (servsock, 1) < 0) 152 | { 153 | builtin_error ("listen failure: %s", strerror (errno)); 154 | close (servsock); 155 | return (EXECUTION_FAILURE); 156 | } 157 | 158 | if (tmoutarg) 159 | { 160 | fd_set iofds; 161 | 162 | FD_ZERO(&iofds); 163 | FD_SET(servsock, &iofds); 164 | 165 | opt = select (servsock+1, &iofds, 0, 0, &timeval); 166 | if (opt < 0) 167 | builtin_error ("select failure: %s", strerror (errno)); 168 | if (opt <= 0) 169 | { 170 | close (servsock); 171 | return (EXECUTION_FAILURE); 172 | } 173 | } 174 | 175 | clientlen = sizeof (client); 176 | if ((clisock = accept (servsock, (struct sockaddr *)&client, &clientlen)) < 0) 177 | { 178 | builtin_error ("client accept failure: %s", strerror (errno)); 179 | close (servsock); 180 | return (EXECUTION_FAILURE); 181 | } 182 | 183 | close (servsock); 184 | 185 | accept_bind_variable (fdvar, clisock); 186 | if (rhostvar) 187 | { 188 | rhost = inet_ntoa (client.sin_addr); 189 | v = builtin_bind_variable (rhostvar, rhost, 0); 190 | if (v == 0 || readonly_p (v) || noassign_p (v)) 191 | builtin_error ("%s: cannot set variable", rhostvar); 192 | } 193 | 194 | 195 | return (EXECUTION_SUCCESS); 196 | } 197 | 198 | static int 199 | accept_bind_variable (varname, intval) 200 | char *varname; 201 | int intval; 202 | { 203 | SHELL_VAR *v; 204 | char ibuf[INT_STRLEN_BOUND (int) + 1], *p; 205 | 206 | p = fmtulong (intval, 10, ibuf, sizeof (ibuf), 0); 207 | v = builtin_bind_variable (varname, p, 0); 208 | if (v == 0 || readonly_p (v) || noassign_p (v)) 209 | builtin_error ("%s: cannot set variable", varname); 210 | return (v != 0); 211 | } 212 | 213 | char *accept_doc[] = { 214 | "Accept a network connection on a specified port.", 215 | "" 216 | "This builtin allows a bash script to act as a TCP/IP server.", 217 | "", 218 | "Options, if supplied, have the following meanings:", 219 | " -b bindadress set the ip on which we should liste, default is any", 220 | " -t timeout wait TIMEOUT seconds for a connection. TIMEOUT may", 221 | " be a decimal number including a fractional portion", 222 | " -v varname store the numeric file descriptor of the connected", 223 | " socket into VARNAME. The default VARNAME is ACCEPT_FD", 224 | " -r rhost store the IP address of the remote host into the shell", 225 | " variable RHOST, in dotted-decimal notation", 226 | "", 227 | "If successful, the shell variable ACCEPT_FD, or the variable named by the", 228 | "-v option, will be set to the fd of the connected socket, suitable for", 229 | "use as 'read -u$ACCEPT_FD'. RHOST, if supplied, will hold the IP address", 230 | "of the remote client. The return status is 0.", 231 | "", 232 | "On failure, the return status is 1 and ACCEPT_FD (or VARNAME) and RHOST,", 233 | "if supplied, will be unset.", 234 | "", 235 | "The server socket fd will be closed before accept returns.", 236 | (char *) NULL 237 | }; 238 | 239 | struct builtin accept_struct = { 240 | "accept", /* builtin name */ 241 | accept_builtin, /* function implementing the builtin */ 242 | BUILTIN_ENABLED, /* initial flags for builtin */ 243 | accept_doc, /* array of long documentation strings. */ 244 | "accept [-b bindaddress] [-t timeout] [-v varname] [-r addrvar ] port", /* usage synopsis; becomes short_doc */ 245 | 0 /* reserved for internal use */ 246 | }; 247 | -------------------------------------------------------------------------------- /bash-server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/bash 2 | 3 | # https://github.com/dylanaraps/pure-bash-bible#decode-a-percent-encoded-string 4 | urldecode() { 5 | : "${1//+/ }" 6 | printf '%b\n' "${_//%/\\x}" 7 | } 8 | 9 | # https://gist.github.com/markusfisch/6110640 10 | uuidgen() { 11 | local N B C='89ab' 12 | 13 | for (( N=0; N < 16; ++N )); do 14 | B="$(( RANDOM%256 ))" 15 | 16 | case $N in 17 | 6) 18 | printf '4%x' $(( B%16 )) 19 | ;; 20 | 8) 21 | printf '%c%x' ${C:$RANDOM%${#C}:1} $(( B%16 )) 22 | ;; 23 | 3 | 5 | 7 | 9) 24 | printf '%02x-' $B 25 | ;; 26 | *) 27 | printf '%02x' $B 28 | ;; 29 | esac 30 | done 31 | } 32 | 33 | send_encoded_frame() { 34 | local first_byte="0x81" 35 | local hex_string binary 36 | 37 | # TODO: Get this working! 38 | # printf -v 'hex_string' '%s0x%x' "$first_byte" "${#1}" 39 | # for ((i = 0; i < ${#hex_string}; i += 2)); do 40 | # binary+="\x${hex_string:i:2}" 41 | # done 42 | # _verbose 4 "$binary" 43 | 44 | printf '%s0x%x' $first_byte "${#1}" | xxd -r -p 45 | printf '%s' "$1" 46 | } 47 | 48 | parseHttpRequest(){ 49 | # Get information about the request 50 | read -r REQUEST_METHOD REQUEST_PATH HTTP_VERSION 51 | HTTP_VERSION="${HTTP_VERSION%%$'\r'}" 52 | } 53 | 54 | parseHttpHeaders(){ 55 | local line _h _v 56 | # Split headers and put it inside HTTP_HEADERS, so it can be reused 57 | while read -r line; do 58 | line="${line%%$'\r'}" 59 | _verbose 3 "$line" 60 | [[ -z "$line" ]] && return 61 | _h="${line%%:*}" 62 | HTTP_HEADERS["${_h,,}"]="${line#*: }" 63 | done 64 | } 65 | 66 | parseGetData(){ 67 | local entry 68 | # Split QUERY_STRING into an assoc, so it can be easy reused 69 | IFS='?' read -r REQUEST_PATH get <<<"$REQUEST_PATH" 70 | 71 | # Url decode get data 72 | get="$(urldecode "$get")" 73 | 74 | # Split html # 75 | IFS='#' read -r REQUEST_PATH _ <<<"$REQUEST_PATH" 76 | QUERY_STRING="$get" 77 | IFS='&' read -ra data <<<"$get" 78 | for entry in "${data[@]}"; do 79 | GET["${entry%%=*}"]="${entry#*:}" 80 | done 81 | } 82 | 83 | parsePostData(){ 84 | local entry 85 | # Split POst data into an assoc if is a form, if not create a key raw 86 | if [[ "${HTTP_HEADERS["Content-type"]}" == "application/x-www-form-urlencoded" ]]; then 87 | IFS='&' read -rN "${HTTP_HEADERS["Content-Length"]}" -a data 88 | for entry in "${data[@]}"; do 89 | entry="${entry%%$'\r'}" 90 | POST["${entry%%=*}"]="${entry#*:}" 91 | done 92 | else 93 | read -rN "${HTTP_HEADERS["Content-Length"]}" data 94 | POST["raw"]="${data%%$'\r'}" 95 | fi 96 | } 97 | 98 | parseCookieData(){ 99 | local -a cookie 100 | local entry key value 101 | IFS=';' read -ra cookie <<<"${HTTP_HEADERS["Cookie"]}" 102 | 103 | for entry in "${cookie[@]}"; do 104 | IFS='=' read -r key value <<<"$entry" 105 | COOKIE["${key# }"]="${value% }" 106 | done 107 | } 108 | 109 | httpSendStatus(){ 110 | local -A status_code=( 111 | [101]="101 Switching Protocols" 112 | [200]="200 OK" 113 | [201]="201 Created" 114 | [301]="301 Moved Permanently" 115 | [302]="302 Found" 116 | [400]="400 Bad Request" 117 | [401]="401 Unauthorized" 118 | [403]="403 Forbidden" 119 | [404]="404 Not Found" 120 | [405]="405 Method Not Allowed" 121 | [500]="500 Internal Server Error" 122 | ) 123 | 124 | HTTP_RESPONSE_HEADERS['status']="${status_code[${1:-200}]}" 125 | } 126 | 127 | buildHttpHeaders(){ 128 | # We will first send the status header and then all the other headers 129 | _verbose 2 "status ${HTTP_RESPONSE_HEADERS['status']}" 130 | printf '%s %s\n' "$HTTP_VERSION" "${HTTP_RESPONSE_HEADERS['status']}" 131 | unset 'HTTP_RESPONSE_HEADERS["status"]' 132 | 133 | _verbose 2 "${cookie_to_send}" 134 | 135 | for value in "${cookie_to_send[@]}"; do 136 | printf 'Set-Cookie: %s\n' "$value" 137 | done 138 | 139 | for key in "${!HTTP_RESPONSE_HEADERS[@]}"; do 140 | _verbose 2 "${key,,}: ${HTTP_RESPONSE_HEADERS[$key]}" 141 | printf '%s: %s\n' "${key,,}" "${HTTP_RESPONSE_HEADERS[$key]}" 142 | done 143 | } 144 | 145 | websocketStart(){ 146 | websocketStart=1 147 | websocketRunner="$1" 148 | } 149 | 150 | websocketStop(){ 151 | websocketStop=1 152 | } 153 | 154 | buildResponse(){ 155 | # Every output will first be saved in a file and then printed to the output 156 | # Like this we can build a clean output to the client 157 | 158 | local websocketStart websocketRunner websocketStop sha1 159 | websocketStart=0 160 | websocketStop=0 161 | 162 | # build a default header 163 | httpSendStatus "$1" 164 | 165 | [[ $1 == 401 ]] && \ 166 | { 167 | HTTP_RESPONSE_HEADERS['WWW-Authenticate']="Basic realm=WebServer" 168 | buildHttpHeaders 169 | return 170 | } 171 | 172 | # get mime type 173 | IFS=. read -r _ extension <<<"$REQUEST_PATH" 174 | [[ -z "${MIME_TYPES["${extension:-html}"]}" ]] || HTTP_RESPONSE_HEADERS["content-type"]="${MIME_TYPES["${extension:-html}"]}" 175 | 176 | "$run" >"$TMPDIR/output" 177 | 178 | # get content-legth 179 | PATH="" type -p "finfo" &>/dev/null && HTTP_RESPONSE_HEADERS["content-length"]="$(finfo -s "$TMPDIR/output")" 180 | 181 | if (( websocketStart )); then 182 | httpSendStatus 101 183 | HTTP_RESPONSE_HEADERS['upgrade']="${HTTP_HEADERS['upgrade']}" 184 | HTTP_RESPONSE_HEADERS['connection']="upgrade" 185 | read -r sha1 _ <<<"$(printf '%s' "${HTTP_HEADERS['sec-websocket-key']}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" | openssl dgst -binary -sha1 | base64)" 186 | HTTP_RESPONSE_HEADERS['sec-websocket-accept']="$sha1" 187 | unset "HTTP_RESPONSE_HEADERS['content-length']" 188 | unset "HTTP_RESPONSE_HEADERS['content-type']" 189 | fi 190 | 191 | # print output to logfile 192 | (( LOGGING )) && logPrint 193 | 194 | buildHttpHeaders 195 | # From HTTP RFC 2616 send newline before body 196 | printf "\n" 197 | 198 | (( websocketStart )) || printf '%s\n' "$(<"$TMPDIR/output")" 199 | 200 | # remove tmpfile, this should be trapped... 201 | # XXX: No needed anymore, since the clean will do the job for use 202 | # rm "$tmpFile" 203 | 204 | if (( websocketStart )); then 205 | _verbose 4 "Websocket Upgrade - $websocketRunner" 206 | local websocketStop 207 | websocketStop=0 208 | sleep 3 209 | while true; do 210 | "$websocketRunner" > "$TMPDIR/output" 211 | message="$(<"$TMPDIR/output")" 212 | send_encoded_frame "$message" 213 | # encode_message "$message" 214 | sleep 5 215 | (( websocketStop )) && break 216 | done 217 | fi 218 | 219 | } 220 | 221 | parseAndPrint(){ 222 | # We will alway reset all variables and build them again 223 | local REQUEST_METHOD REQUEST_PATH HTTP_VERSION QUERY_STRING 224 | local -A HTTP_HEADERS 225 | local -A POST 226 | local -A GET 227 | local -A HTTP_RESPONSE_HEADERS 228 | local -A COOKIE 229 | local -A SESSION 230 | local -a cookie_to_send 231 | 232 | # Now mktemp will write create files inside the temporary directory 233 | local -r TMPDIR="$serverTmpDir" 234 | 235 | # Parse Request 236 | parseHttpRequest 237 | 238 | # Create headers assoc 239 | parseHttpHeaders 240 | 241 | # Basic Auth 242 | if (( BASIC_AUTH )) then 243 | basicAuth || return 1 244 | fi 245 | 246 | # Parse Get Data 247 | parseGetData 248 | 249 | # Parse cookie data 250 | parseCookieData 251 | 252 | 253 | if [[ -z "${COOKIE["$SESSION_COOKIE"]}" ]] || [[ "${COOKIE["$SESSION_COOKIE"]}" == *..* ]]; then 254 | SESSION_ID="$(uuidgen)" 255 | else 256 | SESSION_ID="${COOKIE["$SESSION_COOKIE"]}" 257 | fi 258 | # Parse post data only if length is > 0 and post is specified 259 | # bash (( will not fail if var is not a number, it will just return 1, no need of int check 260 | if [[ "$REQUEST_METHOD" == "POST" ]] && (( ${HTTP_HEADERS['Content-Length']} > 0 )); then 261 | parsePostData 262 | fi 263 | 264 | buildResponse 200 265 | } 266 | 267 | basicAuth(){ 268 | local authData 269 | local user password 270 | 271 | [[ -f "$BASIC_AUTH_FILE" ]] || { 272 | _verbose 1 "Missing \$BASIC_AUTH_FILE" 273 | return 1 274 | } 275 | 276 | if [[ -z "${HTTP_HEADERS["Authorization"]}" ]]; then 277 | buildResponse 401 278 | return 0 279 | fi 280 | 281 | # Decode auth data 282 | # TODO: implement base64 in bash 283 | authData="$(base64 -d <<<"${HTTP_HEADERS["Authorization"]# Basic }")" 284 | 285 | # Split auth data into user and password 286 | IFS=: read -r user password <<<"$authData" 287 | 288 | # Check if user and password appear in users.csv 289 | while read -r r_user r_password; do 290 | [[ "$r_user" == "$user" && "$r_password" == "$password" ]] && { 291 | return 292 | } 293 | done < "$BASIC_AUTH_FILE" 294 | 295 | buildResponse 401 296 | return 1 297 | } 298 | 299 | sessionStart(){ 300 | [[ -d "${SESSION_PATH}" ]] || { 301 | _verbose 1 "Missing Session Path \$SESSION_PATH" 302 | return 1 303 | } 304 | 305 | if [[ -f "${SESSION_PATH}/$SESSION_ID" ]]; then 306 | return 0 307 | else 308 | cookieSet "$SESSION_COOKIE=$SESSION_ID; max-age=5000" 309 | return 1 310 | fi 311 | } 312 | 313 | sessionGet(){ 314 | sessionStart && { 315 | source "${SESSION_PATH}/$SESSION_ID" 316 | printf '%s' "${SESSION[$1]}" 317 | } 318 | } 319 | 320 | sessionSet(){ 321 | sessionStart && source "${SESSION_PATH}/$SESSION_ID" 322 | SESSION["$1"]="$2" 323 | declare -p SESSION > "${SESSION_PATH}/$SESSION_ID" 324 | } 325 | 326 | cookieSet(){ 327 | _verbose 2 "$1" 328 | cookie_to_send+=("$1") 329 | } 330 | 331 | serveHtml(){ 332 | if [[ -n "$DOCUMENT_ROOT" ]]; then 333 | DOCUMENT_ROOT="${DOCUMENT_ROOT%/}" 334 | 335 | # Don't allow going out of DOCUMENT_ROOT 336 | case "$REQUEST_PATH" in 337 | *".."*|*"~"*) 338 | httpSendStatus 404 339 | printf '404 Page Not Found!\n' 340 | return 341 | ;; 342 | esac 343 | [[ "$REQUEST_PATH" == "/" ]] && REQUEST_PATH="/index.html" 344 | if [[ -f "$DOCUMENT_ROOT/${REQUEST_PATH#/}" ]]; then 345 | printf '%s\n' "$(<"$DOCUMENT_ROOT/${REQUEST_PATH#/}")" 346 | else 347 | httpSendStatus 404 348 | printf '404 Page Not Found!\n' 349 | fi 350 | else 351 | httpSendStatus 404 352 | printf '404 Page Not Found!\n' 353 | fi 354 | } 355 | 356 | logPrint(){ 357 | local -A logformat 358 | local output="${LOGFORMAT}" 359 | 360 | logformat["%a"]="$RHOST" 361 | logformat["%A"]="${BIND_ADDRESS}" 362 | logformat["%b"]="${HTTP_RESPONSE_HEADERS["Content-Length"]}" 363 | logformat["%m"]="$REQUEST_METHOD" 364 | logformat["%q"]="$QUERY_STRING" 365 | logformat["%t"]="$TIME_FORMATTED" 366 | logformat["%s"]="${HTTP_RESPONSE_HEADERS['status']%% *}" 367 | logformat["%T"]="$(( $(printf '%(%s)T' -1 ) - TIME_SECONDS))" 368 | logformat["%U"]="$REQUEST_PATH" 369 | 370 | 371 | for key in "${!logformat[@]}"; do 372 | output="${output//"$key"/"${logformat[$key]}"}" 373 | done 374 | 375 | printf '%s\n' "$output" >> "$LOGFILE" 376 | } 377 | 378 | _verbose(){ 379 | # This function should be a simple debug function, which will print the line given based on debug level 380 | # implement in getops the following line: 381 | # (( DEBUG_LEVEL++)) 382 | local LEVEL=1 c printout 383 | local funcnamenumber 384 | (( funcnamenumber=${#FUNCNAME[@]} - 2 )) 385 | : "${DEBUG_LEVEL:=0}" 386 | (( DEBUG_LEVEL == 0 )) && return 387 | # Add level 1 if first char is not set a number 388 | [[ "$1" =~ ^[0-9]$ ]] && { LEVEL=$1; shift; } 389 | 390 | (( LEVEL <= DEBUG_LEVEL )) && { 391 | until (( ${#c} == LEVEL )); do c+=":"; done 392 | if (( funcnamenumber > 0 )); then 393 | printout+="(" 394 | for ((i=1;i<=funcnamenumber;i++)); do 395 | printout+="${FUNCNAME[$i]} <- " 396 | done 397 | printout="${printout% <- }) - " 398 | fi 399 | printf '%-7s %s %s\n' "+ $c" "$printout" "$*" 1>&2 400 | } 401 | } 402 | 403 | clean(){ 404 | kill -9 "$_pid" 405 | } 406 | 407 | main(){ 408 | 409 | local -A MIME_TYPES 410 | 411 | : "${HTTP_PORT:=8080}" 412 | : "${BIND_ADDRESS:=127.0.0.1}" 413 | : "${MIME_TYPES_FILE:=./mime.types}" 414 | : "${TMPDIR:=/tmp}" 415 | : "${LOGFORMAT:="[%t] - %a %m %U %s %b %T"}" 416 | : "${LOGFILE:=access.log}" 417 | : "${LOGGING:=1}" 418 | : "${SESSION_COOKIE:=BASHSESSID}" 419 | : "${BASIC_AUTH:=0}" 420 | TMPDIR="${TMPDIR%/}" 421 | 422 | ! [[ ${BIND_ADDRESS} == "0.0.0.0" ]] && acceptArg="-b ${BIND_ADDRESS}" 423 | 424 | enable -f accept accept || { 425 | printf '%s\n' "Cannot load accept..." 426 | exit 1 427 | } 428 | 429 | [[ -f "$MIME_TYPES_FILE" ]] && \ 430 | while read -r types extension; do 431 | read -ra extensions <<<"$extension" 432 | for ext in "${extensions[@]}"; do 433 | MIME_TYPES["$ext"]="$types" 434 | done 435 | done <"$MIME_TYPES_FILE" 436 | 437 | 438 | # Enable mktemp and rm as a builtin :D 439 | # Don't fail if it doesn't exist 440 | enable -f "mktemp" mktemp &>/dev/null || true 441 | enable -f "rm" rm &>/dev/null || true 442 | enable -f "finfo" finfo &>/dev/null || true 443 | 444 | trap clean EXIT 445 | 446 | case "$1" in 447 | serveHtml) 448 | run="serveHtml" 449 | ;; 450 | *) 451 | # source the configuration file and check if runner is defined 452 | [[ -z "$1" || ! -f "$1" ]] && { 453 | printf '%s\n' "please provide a file to source as the first argument..." 454 | exit 1 455 | } 456 | # source main file 457 | source "$1" 458 | type runner &>/dev/null || { 459 | printf '%s\n' "The source file need a function nammed runner which will be executed on each request..." 460 | exit 1 461 | } 462 | run="runner" 463 | ;; 464 | esac 465 | 466 | while :; do 467 | 468 | # create temporary directory for each request 469 | _verbose 1 "Listening on $BIND_ADDRESS port $HTTP_PORT" 470 | 471 | serverTmpDir="$(mktemp -d)" 472 | # Create the file, but do not zrite inside 473 | : > "$serverTmpDir/spawnNewProcess" 474 | 475 | ( 476 | # XXX: Accept puts the connection in a TIME_WAIT status.. :( 477 | # Verifiy if bind_address is specified default to 127.0.0.1 478 | # You should use the custom accept in order to use bind address and multiple connections 479 | accept $acceptArg "${HTTP_PORT}" || { 480 | printf '%s\n' "Could not listen on ${BIND_ADDRESS}:${HTTP_PORT}" 481 | exit 1 482 | } 483 | 484 | printf '1' > "$serverTmpDir/spawnNewProcess" 485 | printf -v TIME_FORMATTED '%(%d/%b/%Y:%H:%M:%S)T' -1 486 | printf -v TIME_SECONDS '%(%s)T' -1 487 | parseAndPrint <&${ACCEPT_FD} >&${ACCEPT_FD} 488 | 489 | # XXX: This is needed to close the connection to the client 490 | # XXX: Currently no other way found around it.. :( 491 | exec {ACCEPT_FD}>&- 492 | 493 | # remove the temporary directoru 494 | rm -rf "$serverTmpDir" 495 | ) & 496 | 497 | _pid="$!" 498 | 499 | until [[ -s "$serverTmpDir/spawnNewProcess" || ! -f "$serverTmpDir/spawnNewProcess" ]]; do : ; done 500 | 501 | # Since the patch, no need of sleep anymore 502 | #sleep "${TIME_WAIT:-0}" 503 | done 504 | 505 | } 506 | 507 | main "$@" 508 | --------------------------------------------------------------------------------