Reputation: 21
I have been bashing my brains for the past few hours trying to build a poor man's http server using socat. Don't ask why or suggest alternatives. I need to do this in pure bash. So I run socat like this:
socat -v TCP-LISTEN:1234,reuseaddr,fork SYSTEM:./httpd.sh
The httpd.sh is supposed the read the http request, parse it and then send a response.
e.g. GET /index.html will output Index! and GET /random will output a random number.
The problem I have is reading the entire http request. Consider the following code used to read each line of the http request:
while read -r LINE
do
echo "$LINE"
done
Normally it should output back the request to the browser. The problem is after I open 127.0.0.1:1234 it just hangs waiting for something. If I CTRL + C socat, the connection closes and the response shows up in the browser. I think the while loop continues forever thus preventing the transmission of the response.
If I use the code below:
while read -r LINE
do
echo "$LINE"
[ -z "$LINE" ] && break
done
the while loop breaks and the browser doesn't hang anymore. Seems like a good solution. But... in the case of a POST request the POST data isn't recorded because the break occurs right after the headers (blank line)...
POST /index.html HTTP/1.0
User-Agent: Firefox
Content-Type: application/x-www-form-urlencoded
parameter1=test
What can I do to read the entire http request via the shell script, process it and send the response without any hanging?
EDIT:
Here is something that I think might hold the answwer. If I run this command:
socat TCP4-LISTEN:1234,reuseaddr,fork,crlf SYSTEM:"echo hello world"
Everything works just fine, hello world is outputed everytime.
How does socat know when the HTTP request ends?
Upvotes: 2
Views: 2144
Reputation: 21
u must detect if data Header is finish,
while read -r LINE
do
if [[ $LINE =~ "Content-Length:" ]] ; then // check if line is content-length data
contentLength=("$( cut -d ':' -f2 <<< $LINE |tr -d ' ')"); // get data content-length
fi
if [ -z "$LINE" ]; then // detect if line is empty, this means the header data is completed
limit=500; // set limit time, limit x 0.05s, this optional u can remove it
li=0;
postLength=0;
contentLength=$((contentLength-1))
while [ $postLength -lt $contentLength ]; do // Check the amount of length that has been obtained
postData="$postData$(timeout 0.05 cat|sed 's/\x0/*#Mystring#*/g')"; // reading post data
postLength=$(sed 's/\*#Mystring#\*/\x0/g' <<< "$postData" |wc -c); // get the amount of length that has been obtained
li=$((li+1));
if [ $li -eq $limit ] ; then //remove this if, if you remove limit=, or when you not need this
break;// remove this line, if u not need limit timeout
fi;
done
break; //stop while, and then stop script
fi
done
this is example socat http POST
#!/bin/bash
contains() {
string="$1"
substring="$2"
if test "${string#*$substring}" != "$string"
then
return 0 # $substring is in $string
else
return 1 # $substring is not in $string
fi
}
Methods=()
REQUESTs=()
HOSTs=()
contentLengths=()
contentTypes=()
connections=()
referers=()
ranges=()
encodings=()
cookies=()
line=""
timeout="timeout=5, max=1000"
while read -t 5 linec
do
linec=$(echo "$linec" | tr -d '\r\n')
line=$(echo -e "$line\n$linec")
if grep -qE '^GET /' > /dev/null <<< "$linec" ; then
Methods+=("GET")
REQUESTs+=("$(awk '{if(NR==1)print $0}' <<< $linec | cut -d ' ' -f2)") # extract the request
elif echo "$linec" | grep -qE '^POST /' > /dev/null ; then
Methods+=("POST")
REQUESTs+=("$(awk '{if(NR==1)print $0}' <<< $linec | cut -d ' ' -f2)") # extract the request
elif echo "$linec" | grep -qE '^OPTIONS /' > /dev/null ; then
Methods+=("OPTIONS")
REQUESTs+=("$(awk '{if(NR==1)print $0}' <<< $linec | cut -d ' ' -f2)") # extract the request
fi
if [[ $linec =~ "Host:" ]] ; then
HOSTs+=("$( cut -d ':' -f2,3 <<< $linec|tr -d ' ')");
fi
if [[ $linec =~ "Content-Type:" ]] ; then
contentTypes+=("$( cut -d ':' -f2 <<< $linec|tr -d ' ')");
fi
if [[ $linec =~ "Content-Length:" ]] ; then
contentLengths+=("$( cut -d ':' -f2 <<< $linec|tr -d ' ')");
fi
if [[ $linec =~ "Connection:" ]] ; then
connections+=("$( cut -d ':' -f2 <<< $linec|tr -d ' ')");
fi
if [[ $linec =~ "Referer:" ]] ; then
referers+=("$( cut -d ':' -f2,3,4 <<< $linec|tr -d ' '| sed "s|https://${HOSTs[0]}||g" )")
fi
if [[ $linec =~ "Range:" ]] ; then
ranges+=("$( cut -d ':' -f2 <<< $linec|tr -d ' ')")
fi
if [[ $linec =~ "Accept-Encoding:" ]] ; then
encodings+=("$( cut -d ':' -f2 <<< $linec|tr -d ' ')");
fi
if [[ $linec =~ "Cookie:" ]] ; then
cookies+=("$( cut -d ':' -f2 <<< $linec|tr -d ' ')");
fi
if [[ $linec =~ "Android" ]] ; then
androidDevice="true"
fi
#HOST=$(echo $line | awk '{if(NR==2)print $0}' | cut -d ':' -f2 | sed -e 's/^[[:space:]]*//')
if [ -z "$linec" ]; then
echo "$line" >> /root/range10
line=""
#echo >> /root/range
proc=true
index=0
for Method in "${Methods[@]}" ; do
REQUEST=${REQUESTs[index]}
HOST=${HOSTs[index]}
contentType=${contentTypes[index]}
#echo "$contentType" >/root/ct
contentLength=${contentLengths[index]}
connection=${connections[index]}
referer=${referers[index]}
range=${ranges[index]}
encoding=${encodings[index]}
cookie=${cookies[index]}
if [ "$Method" = "POST" ] ; then
echo "AAA:$contentType" >> /root/testResult2;
limit=500;
li=0;
postLength=0;
if [[ $contentType =~ "application/x-www-form-urlencoded" ]] ; then
contentLength=$((contentLength-1))
while [ $postLength -lt $contentLength ]; do
postData="$postData$(timeout 0.05 cat|sed 's/\x0/*#Mystring#*/g')";
postLength=$(sed 's/\*#Mystring#\*/\x0/g' <<< "$postData" |wc -c);
li=$((li+1));
if [ $li -eq $limit ] ; then
break; // remove this line, if u not need limit timeout
fi;
done
elif [[ $contentType =~ "application/json" ]] ; then
contentLength=$((contentLength-1))
while [ $postLength -lt $contentLength ]; do
postData="$postData$(timeout 0.05 cat|sed 's/\x0/*#Mystring#*/g')";
postLength=$(sed 's/\*#Mystring#\*/\x0/g' <<< "$postData" |wc -c);
li=$((li+1));
if [ $li -eq $limit ] ; then
break;// remove this line, if u not need limit timeout
fi;
done
fi
elif [ "$Method" = "OPTIONS" ] ; then
echo 'HTTP/1.1 204 No Content';
echo "Access-Control-Allow-Origin: *";
echo 'Access-Control-Allow-Methods: GET, POST, OPTIONS';
echo 'Access-Control-Allow-Headers: X-PINGOTHER, Content-Type';
echo 'Access-Control-Max-Age: 86400';
echo 'Vary: Accept-Encoding, Origin';
echo 'Keep-Alive: timeout=2, max=100';
echo 'Connection: Keep-Alive';
echo;
fi
index=$(( index + 1 ));
done
if [ -z "$line" ]; then
exit
fi
if [ "$connection" = "keep-alive" ] && [ "$proc" = "true" ] ; then
Methods=()
REQUESTs=()
HOSTs=()
contentLengths=()
connections=()
ranges=()
encodings=()
cookies=()
line=""
elif [ "$proc" = "true" ] ; then
exit;
fi
fi
done
Upvotes: 0
Reputation: 19375
What can I do to read the entire http request via the shell script, process it and send the response without any hanging?
As you write, you already have a solution for GET; in the case of a POST request, you just have to read one more line (multiple data values are &
separated on one line). After sending the response, you have to exit httpd.sh
or at least close its output.
How does socat know when the HTTP request ends?
socat
knows that the response ended when the data pipe from echo
is closed on termination of that process.
Upvotes: 1
Reputation: 490
You cannot read the request line by line. HTTP requests might contain line breaks. You need to read the whole request and parse that.
On GET requests, or Head requests, you only need the headers, so you can read only one line.
For Posts, you need to read either the number of bytes stated in the content-length header, or until you receive an EOF.
Suffices to say, your script becomes more complicated.
Upvotes: 0