├── app
├── comics
├── js
│ ├── script.js
│ └── issue.js
├── favicon.ico
├── images
│ ├── test.jpg
│ ├── test.png
│ └── small-test.png
├── videos
│ └── test.mp4
├── 404.html
├── account.html
├── comics.html
├── issues.html
├── login.html
├── index.html
├── css
│ └── style.css
└── issue.html
├── .gitignore
├── sessions
├── 03469229-7265-4240-a5c6-fb1bf871c878
├── 08848c1d-2f93-49fa-b377-b7c706114f3e
├── 1db271d4-26e8-439d-99b5-b7db1a377afc
├── 5c4cd900-9649-44c5-93c4-aa2634454a4d
├── 5d1de5d6-3d52-421b-b75d-f6e8f8d0daca
├── 793c3c80-194e-42db-9bc7-a71c1b079eb9
├── aa0014a6-2b4b-4390-b9d0-9107e2345adb
├── b869a250-fd4c-4bca-a0a5-109ac7935ccd
├── e6f2f526-d183-4e9b-96e2-f517f7367428
└── ffca526a-64a6-4abe-83c5-d76eec8218f1
├── README.md
└── server.sh
/app/comics:
--------------------------------------------------------------------------------
1 | /home/media/comics/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | app/comics/
2 | sessions/
3 |
--------------------------------------------------------------------------------
/app/js/script.js:
--------------------------------------------------------------------------------
1 | console.log("Hi");
2 |
--------------------------------------------------------------------------------
/sessions/03469229-7265-4240-a5c6-fb1bf871c878:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/08848c1d-2f93-49fa-b377-b7c706114f3e:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/1db271d4-26e8-439d-99b5-b7db1a377afc:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/5c4cd900-9649-44c5-93c4-aa2634454a4d:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/5d1de5d6-3d52-421b-b75d-f6e8f8d0daca:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/793c3c80-194e-42db-9bc7-a71c1b079eb9:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/aa0014a6-2b4b-4390-b9d0-9107e2345adb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/b869a250-fd4c-4bca-a0a5-109ac7935ccd:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/e6f2f526-d183-4e9b-96e2-f517f7367428:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sessions/ffca526a-64a6-4abe-83c5-d76eec8218f1:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krowemoh/bash-server/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/app/images/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krowemoh/bash-server/HEAD/app/images/test.jpg
--------------------------------------------------------------------------------
/app/images/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krowemoh/bash-server/HEAD/app/images/test.png
--------------------------------------------------------------------------------
/app/videos/test.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krowemoh/bash-server/HEAD/app/videos/test.mp4
--------------------------------------------------------------------------------
/app/images/small-test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krowemoh/bash-server/HEAD/app/images/small-test.png
--------------------------------------------------------------------------------
/app/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404
6 |
7 |
8 | Resource not found.
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bash Server
2 |
3 |
4 | A HTTP server written in bash! This is just a toy to see if I could do it, and you definitely can write a web server as a shell script!
5 |
6 | Blog post at:
7 |
8 | https://nivethan.dev/devlog/a-web-server-in-bash.html
9 |
--------------------------------------------------------------------------------
/app/account.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Manga Readers Club - Account!
7 |
8 |
9 |
10 | Hello, "$username"
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/comics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Directory - Comic Book Readers Club!
7 |
8 |
9 |
10 | Directory - Comic Book Readers Club!
11 |
14 |
15 | $(
16 | cd ./app/comics
17 | for id in *
18 | do
19 | echo ""
20 | done
21 | )
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/issues.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | $comic_name Chapters!
7 |
8 |
9 |
10 | $comic_name Chapters!
11 |
12 | $(
13 | cd ./app/comics/$comic_name
14 | counter=0
15 | for chapter in *
16 | do
17 | counter=$((counter+1))
18 | echo ""
19 | done
20 | )
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Manga Readers Club - Login!
7 |
8 |
9 |
10 | Manga Readers Club - Login!
11 |
12 |
13 |
14 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/js/issue.js:
--------------------------------------------------------------------------------
1 | refresh_handler = function(e) {
2 | var elements = document.querySelectorAll("*[realsrc]");
3 |
4 | for (var i = 0; i < elements.length; i++) {
5 | var boundingClientRect = elements[i].getBoundingClientRect();
6 | if (elements[i].hasAttribute("realsrc") && boundingClientRect.top < window.innerHeight*3) {
7 | elements[i].setAttribute("src", elements[i].getAttribute("realsrc"));
8 | elements[i].removeAttribute("realsrc");
9 | }
10 | }
11 |
12 | var page = document.getElementById("page");
13 | var pageRect = page.getBoundingClientRect();
14 | var elements = document.querySelectorAll("img[src]");
15 |
16 | };
17 |
18 | window.addEventListener('scroll', refresh_handler);
19 | window.addEventListener('load', refresh_handler);
20 | window.addEventListener('resize', refresh_handler);
21 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Fun!
7 |
8 |
9 |
10 |
11 | Hello, World%
12 |
13 |
14 | Test PNG
15 |
16 |
17 |
18 | Test JPG
19 |
20 |
21 |
22 | Test MP4
23 |
24 |
25 |
26 |
27 |
28 | Test POST Form
29 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | max-width: 80ch;
3 | font-family:sans-serif;
4 | margin:0 auto;
5 | line-height: 1.5;
6 | padding: 1em 2em;
7 | padding-bottom:100px;
8 | background-color:#292929;
9 | color:#8db2e5;
10 | }
11 |
12 | h1, h2, h3, h4, h5, h6, {
13 | margin-top:1em;
14 | margin-bottom:1em;
15 | }
16 |
17 | code, pre {
18 | background-color: #eee;
19 | vertical-align:text-bottom;
20 | padding: 1em;
21 | overflow-x:scroll;
22 | }
23 |
24 | a {
25 | color:#8db2e5;
26 | }
27 |
28 | a:visited {
29 | color:rgb(211, 138, 138);
30 | }
31 |
32 | ol {
33 | margin-top:0.5em;
34 | margin-bottom: 2em;
35 | }
36 | img {
37 | max-width:80ch;
38 | height:auto;
39 | }
40 |
41 | video {
42 | height:100%;
43 | width:100%;
44 | }
45 |
46 | div {
47 | margin:10px 0px;
48 | }
49 |
50 | #command-center {
51 | position:fixed;
52 | top:50%;
53 | right:3%;
54 | transform:translateY(-50%);
55 | }
56 |
57 | #command-center input {
58 | width:300px;
59 | padding:5px;
60 | background-color:#292929;
61 | border:1px solid gray;
62 | color:white;
63 | margin-bottom:5px;
64 | }
65 |
66 | #command-center textarea{
67 | height:150px;
68 | width:300px;
69 | padding:5px;
70 | background-color:#292929;
71 | border:1px solid gray;
72 | color:white;
73 | font-family:sans-serif;
74 | }
75 |
--------------------------------------------------------------------------------
/app/issue.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | $comic_name - Issue $issue_number
7 |
8 |
9 |
10 |
11 |
12 | $comic_name - Issue $issue_number
13 |
14 |
15 |
16 |
17 |
18 |
19 | $(
20 | files=(./app/comics/"$comic_name"/*)
21 | folder=("${files["$issue_number-1"]}"/*)
22 |
23 | if [ -d ${folder} ]
24 | then
25 | folder=("${folder}"/*)
26 | fi
27 |
28 | counter=0
29 | for page in ${folder[@]}
30 | do
31 | counter=$((counter+1))
32 | page=$(echo "$page" | sed 's/\.\/app//g' | sed "s/'/%%27/g")
33 | if [[ "$counter" -le 3 ]]
34 | then
35 | echo ""
36 | else
37 | echo ""
38 | fi
39 | done
40 | )
41 |
42 |
43 |
--------------------------------------------------------------------------------
/server.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | declare -r CR=$'\r'
4 | declare -r LF=$'\n'
5 | declare -r CR_LF="${CR}${LF}"
6 |
7 | declare -r DEBUG=1
8 |
9 | declare -A function_dictionary=(
10 | [login]=login
11 | [signout]=signout
12 | ["^comics$"]=comics
13 | ["^comics/(.*)"]=issues
14 | ["^comics/(.*)/(.*)"]=issue
15 | )
16 |
17 | serve() {
18 | nc --listen --keep-open 0.0.0.0 7999 --sh-exec "./server.sh process"
19 | }
20 |
21 | log() {
22 | if [ $DEBUG = 1 ]
23 | then
24 | echo "$1" >&2;
25 | fi
26 | }
27 |
28 | check_session() {
29 | if [ ! -f "./sessions/$session_id" ]
30 | then
31 | render_template "./app/login.html"
32 | send_html "$template"
33 | fi
34 | }
35 |
36 | process() {
37 | local IFS=$'\n'
38 |
39 | # STDIN -> request_headers
40 | get_request_headers
41 |
42 | # request_headers -> request_type, request_body, request_cookies
43 | get_request_body_cookies
44 |
45 | # request_headers -> requested_resource
46 | handle_requested_resource
47 | }
48 |
49 | get_request_headers() {
50 | request_headers=()
51 |
52 | while true
53 | do
54 | read header
55 | if [ "$header" = $CR_LF ]
56 | then
57 | break
58 | fi
59 | request_headers=("${request_headers[@]}" "$header")
60 | done
61 | }
62 |
63 | get_request_body_cookies() {
64 | request_type="$(echo "${request_headers[0]}" | cut -d" " -f1)"
65 |
66 | post_length=0
67 | for i in "${request_headers[@]}"
68 | do
69 | header=$(cut -d":" -f1 <<< "$i")
70 | if [ "$header" = "Content-Length" ]
71 | then
72 | post_length=$(echo "$i" | cut -d":" -f2 | tr -d "$CR" | tr -d "$LF" | tr -d ' ')
73 |
74 | elif [ "$header" = "Cookie" ]
75 | then
76 | regex=".*session_id=(.*);?"
77 | [[ "$i" =~ $regex ]]
78 | session_id=$(echo "${BASH_REMATCH[1]}" | tr -d "$CR" | tr -d "$LF")
79 | fi
80 | done
81 |
82 | if [ "$post_length" -ne 0 ]
83 | then
84 | IFS= read -n "$post_length" request_body
85 | fi
86 | }
87 |
88 | handle_requested_resource() {
89 | regexp=".* (.*) HTTP"
90 | [[ "${request_headers[0]}" =~ $regexp ]]
91 |
92 | resource=$(printf "%s" "${BASH_REMATCH[1]}" | sed 's/%20/ /g' | sed "s/%27/'/g")
93 |
94 | requested_resource="./app$resource"
95 | if [ -f "$requested_resource" ]
96 | then
97 | send_file "$requested_resource"
98 | fi
99 |
100 | requested_resource="${resource:1}"
101 |
102 | for x in "${!function_dictionary[@]}"
103 | do
104 | if [[ "$requested_resource" =~ $x ]]
105 | then
106 | ${function_dictionary[$x]}
107 | fi
108 | done
109 |
110 | render_template "./app/404.html"
111 | send_html "$template"
112 | }
113 |
114 | set_response_content_type() {
115 | case "$1" in
116 | "html")
117 | content_type="Content-Type: text/html"
118 | ;;
119 | "css")
120 | content_type="Content-Type: text/css"
121 | ;;
122 | "js")
123 | content_type="Content-Type: text/javascript"
124 | ;;
125 | "ico")
126 | content_type="Content-Type: image/x-icon"
127 | ;;
128 | "png")
129 | content_type="Content-Type: image/png"
130 | ;;
131 | "jpg" | "jpeg")
132 | content_type="Content-Type: image/jpeg"
133 | ;;
134 | "mp4")
135 | content_type="Content-Type: video/mp4"
136 | ;;
137 | *)
138 | content_type="Content-Type: text/plain"
139 | ;;
140 | esac
141 | }
142 |
143 | get_requested_content() {
144 | length=$(stat --printf "%s" "$1")
145 | content_length="Content-Length: $length"
146 | content=$(cat "$1" | sed 's/\\/\\\\/g' | sed 's/%/%%/g' | sed 's/\x00/\\x00/g')
147 | }
148 |
149 | set_response_headers() {
150 | version="HTTP/1.1 200 OK"
151 | date="Date: $(date)"
152 | connection="Connection: Closed"
153 |
154 | if [ "$cookies" = "" ]
155 | then
156 | response_headers="${version}$CR_LF${date}$CR_LF$1$CR_LF$2$CR_LF${connection}"
157 | else
158 | response_headers="${version}$CR_LF${date}$CR_LF${cookies}$CR_LF$1$CR_LF$2$CR_LF${connection}"
159 | fi
160 | }
161 |
162 | build_response() {
163 | response="$1$CR_LF$CR_LF$2"
164 | }
165 |
166 | send_response() {
167 | printf -- "$1$CR_LF"
168 | exit
169 | }
170 |
171 | send_file() {
172 | # -> content_type
173 | requested_resource="$1"
174 | extension="${requested_resource##*.}"
175 | set_response_content_type "$extension"
176 |
177 | # -> data | content_length
178 | get_requested_content "$1"
179 |
180 | # -> response_headers
181 | set_response_headers "$content_type" "$content_length"
182 |
183 | # -> response
184 | build_response "$response_headers" "$content"
185 |
186 | # -> ECHO data | PRINTF data
187 | send_response "$response"
188 | }
189 |
190 | send_html() {
191 | content="$1"
192 | content_length="${#content}"
193 |
194 | set_response_content_type "html"
195 | set_response_headers "$content_type" "$content_length"
196 | build_response "$response_headers" "$content"
197 |
198 | send_response "$response"
199 | }
200 |
201 | render_template() {
202 | template=$(eval "cat <<- END
203 | $(cat "$1")
204 | END
205 | ")
206 | }
207 |
208 | login() {
209 | if [ "$request_type" = "GET" ]
210 | then
211 | content="$(cat ./app/login.html)"
212 | send_html "$content"
213 |
214 | else
215 | username=$(echo -n "$request_body" | cut -d'&' -f1 | cut -d'=' -f2)
216 | password=$(echo -n "$request_body" | cut -d'&' -f2 | cut -d'=' -f2)
217 |
218 | if [ "$password" = "123" ]
219 | then
220 | session_id=$(uuidgen)
221 | touch "./sessions/$session_id"
222 | cookies="Set-cookie: session_id=$session_id"
223 | fi
224 | render_template "./app/account.html"
225 | send_html "$template"
226 | fi
227 | }
228 |
229 | signout() {
230 | if [ -f "./sessions/$session_id" ]
231 | then
232 | rm "./sessions/$session_id"
233 | session_id=""
234 | cookies="Set-cookie: session_id=$session_id"
235 | fi
236 | render_template "./app/login.html"
237 | send_html "$template"
238 | }
239 |
240 | comics() {
241 | check_session
242 | render_template "./app/comics.html"
243 | send_html "$template"
244 | }
245 |
246 | issues() {
247 | check_session
248 | comic_name="${BASH_REMATCH[1]}"
249 | render_template "./app/issues.html"
250 | send_html "$template"
251 | }
252 |
253 | issue() {
254 | check_session
255 | comic_name="${BASH_REMATCH[1]}"
256 | issue_number="${BASH_REMATCH[2]}"
257 | render_template "./app/issue.html"
258 | send_html "$template"
259 | }
260 |
261 | "$1"
262 |
--------------------------------------------------------------------------------