├── 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 |
signout
13 |
Comics
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 |
12 | signout 13 |
14 |
15 | $( 16 | cd ./app/comics 17 | for id in * 18 | do 19 | echo "
$id
" 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 "
$chapter
" 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 |
15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 |
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 | 26 | 27 |
28 |

Test POST Form

29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
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 | 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 | --------------------------------------------------------------------------------