Skip to content

Instantly share code, notes, and snippets.

@leandronsp
Last active February 28, 2024 19:44
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save leandronsp/3a81e488b792235b2be73f8def2f51e6 to your computer and use it in GitHub Desktop.
Save leandronsp/3a81e488b792235b2be73f8def2f51e6 to your computer and use it in GitHub Desktop.
A complete yet simple Web server (with login & logout system) written in Shell Script
#!/bin/bash
## Create the response FIFO
rm -f response
mkfifo response
function handle_GET_home() {
RESPONSE=$(cat home.html | \
sed "s/{{$COOKIE_NAME}}/$COOKIE_VALUE/")
}
function handle_GET_login() {
RESPONSE=$(cat login.html)
}
function handle_POST_login() {
RESPONSE=$(cat post-login.http | \
sed "s/{{cookie_name}}/$INPUT_NAME/" | \
sed "s/{{cookie_value}}/$INPUT_VALUE/")
}
function handle_POST_logout() {
RESPONSE=$(cat post-logout.http | \
sed "s/{{cookie_name}}/$COOKIE_NAME/" | \
sed "s/{{cookie_value}}/$COOKIE_VALUE/")
}
function handle_not_found() {
RESPONSE=$(cat 404.html)
}
function handleRequest() {
## Read the HTTP request until \r\n
while read line; do
echo $line
trline=$(echo $line | tr -d '[\r\n]') ## Removes the \r\n from the EOL
## Breaks the loop when line is empty
[ -z "$trline" ] && break
## Parses the headline
## e.g GET /login HTTP/1.1 -> GET /login
HEADLINE_REGEX='(.*?)\s(.*?)\sHTTP.*?'
[[ "$trline" =~ $HEADLINE_REGEX ]] &&
REQUEST=$(echo $trline | sed -E "s/$HEADLINE_REGEX/\1 \2/")
## Parses the Content-Length header
## e.g Content-Length: 42 -> 42
CONTENT_LENGTH_REGEX='Content-Length:\s(.*?)'
[[ "$trline" =~ $CONTENT_LENGTH_REGEX ]] &&
CONTENT_LENGTH=$(echo $trline | sed -E "s/$CONTENT_LENGTH_REGEX/\1/")
## Parses the Cookie header
## e.g Cookie: name=John -> name John
COOKIE_REGEX='Cookie:\s(.*?)\=(.*?).*?'
[[ "$trline" =~ $COOKIE_REGEX ]] &&
read COOKIE_NAME COOKIE_VALUE <<< $(echo $trline | sed -E "s/$COOKIE_REGEX/\1 \2/")
done
## Read the remaining HTTP request body
if [ ! -z "$CONTENT_LENGTH" ]; then
BODY_REGEX='(.*?)=(.*?)'
while read -n$CONTENT_LENGTH -t1 line; do
echo $line
trline=`echo $line | tr -d '[\r\n]'`
[ -z "$trline" ] && break
read INPUT_NAME INPUT_VALUE <<< $(echo $trline | sed -E "s/$BODY_REGEX/\1 \2/")
done
fi
## Route request to the response handler
case "$REQUEST" in
"GET /login") handle_GET_login ;;
"GET /") handle_GET_home ;;
"POST /login") handle_POST_login ;;
"POST /logout") handle_POST_logout ;;
*) handle_not_found ;;
esac
echo -e "$RESPONSE" > response
}
echo 'Listening on 3000...'
## Keep the server running forever
while true; do
## 1. wait for FIFO
## 2. creates a socket and listens to the port 3000
## 3. as soon as a request message arrives to the socket, pipes it to the handleRequest function
## 4. the handleRequest function processes the request message and routes it to the response handler, which writes to the FIFO
## 5. as soon as the FIFO receives a message, it's sent to the socket
## 6. closes the connection (`-N`), closes the socket and repeat the loop
cat response | nc -lN 3000 | handleRequest
done
HTTP/1.1 404 NotFound
Content-Type: text/html
<h1>Sorry, not found</h1>
HTTP/1.1 200
Content-Type: text/html
<html>
<head>
<style>
section {
display: inline-block;
margin-left: 40%;
margin-top: 10%;
}
section p {
color: black;
}
</style>
</head>
<body>
<section>
<p>Hello, {{name}}</p>
<form method="POST" action="/logout">
<input type="submit" value="Logout" />
</form>
<a href="javascript:void(0)">Blue theme</a>
</section>
</body>
<footer>
<script>
let themeElem = document.querySelector('a');
let nameElem = document.querySelector('section > p');
themeElem.addEventListener('click', function(evt) {
if (nameElem.style.color == 'blue') {
nameElem.style.color = 'black';
themeElem.text = 'Blue theme';
} else {
nameElem.style.color = 'blue';
themeElem.text = 'Black theme';
}
})
</script>
</footer>
</html>
HTTP/1.1 200
Content-Type: text/html
<html>
<head>
<style>
section {
display: inline-block;
margin-left: 40%;
margin-top: 10%;
}
input[name="name"] {
height: 30px;
margin-top: 20%;
border: 1px solid #999;
border-radius: 4px;
}
</style>
</head>
<body>
<section>
<form method="POST" action="/login">
<input type="text" name="name" />
<input type="submit" value="Login" />
</form>
</section>
</body>
</html>
HTTP/1.1 301
Location: http://localhost:3000/
Set-Cookie: {{cookie_name}}={{cookie_value}}; path=/; HttpOnly
HTTP/1.1 301
Location: http://localhost:3000/login
Set-Cookie: {{cookie_name}}={{cookie_value}}; path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT
@CodeMan99
Copy link

crlf tips!

  • nc -C will automatically translate lf to crlf.
  • Use read -d $'\r\n' to simplify header parsing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment