├── 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 |
--------------------------------------------------------------------------------