#!/usr/bin/env bash
#
# A Gopher server forked from Dave Eddy's bash http server
# Philip Wittamore 24 January 2026
#
#
# Bash HTTP Server in Pure Bash.
# Watch how this was made on YouTube:
# - https://youtu.be/L967hYylZuc
# Author: Dave Eddy <dave@daveeddy.com>
# Date: July 08, 2025
# License: MIT

VERSION='v0.1'
PORT=70
HOST='0.0.0.0'
DIR="."
LOG="$DIR/woof.log"

read -d '' -r USAGE <<-EOF
	Usage: woof [-p port] [-b addr] [-d dir]

	An Gopher server in Pure Bash.

	Options
	  -b <addr>  HOST to bind to, defaults to localhost
	  -d <dir>   Directory to serve, defaults to your current directory.
	  -h         Print this message and exit.
	  -p <port>  Port to bind to, defaults to 70.
	  -v         Print the version number and exit.
EOF

fatal() {
	echo '[fatal]' "$@" >&2
	exit 1
}

log() {
	local path=$1
	local ip=$2
	local result=$3
	echo "$(date '+%d/%b/%Y:%H:%M:%S %z') $ip - - \"GET $path\" $result" >>$LOG
}

urldecode() {
	# Usage: urldecode "string"
	: "${1//+/ }"
	printf '%b\n' "${_//%/\\x}"
}

normalize-path() {
	local path=/$1

	local parts
	IFS='/' read -r -a parts <<<"$path"

	local -a part
	local -a out=()
	for part in "${parts[@]}"; do
		case "$part" in
		'') ;;  # ignore empty directories (multiple /)
		'.') ;; # ignore current directory
		'..') unset 'out[-1]' 2>/dev/null ;;
		*) out+=("$part") ;;
		esac
	done

	local s
	s=$(
		IFS=/
		echo "${out[*]}"
	)
	echo "$s"
}

list-directory() {
	local d=$1

	shopt -s nullglob dotglob

	local f
	# loop directories first (to put at top of list)
	for f in .. "$d"/*/; do
		local bname=${f%/}
		bname=${bname##*/}
		echo -e "1$bname\t$d/$bname\t$HOST\t$PORT"
	done
	# loop regular files next (non-directories)
	for f in "$d"/*; do
		[[ -f $f ]] || continue
		local bname=${f##*/}
		echo -e "0$bname\t$d/$bname\t$HOST\t$PORT"
	done
}

process-file() {
	local ext file
	file=$1
	ext="${file##*.}"
	if [[ $file = "gophermap" ]]; then
		tee <"$file" >&"$fd"
	else
		case "$ext" in
		txt)
			tee <"$file" >&"$fd"
			;;
		php)
			exec php "$file" >&"$fd" || echo "php not found" >&"$fd"
			;;
		*)
		    #internal tee terminates on pipefail
			cat "$file" >&"$fd"
			;;
		esac
	fi
}

process-request() {
	local fd=$1
	local ip=$2
	local path

	read -r -u "$fd" -n 1000 path

	path=${path%$'\r'}
	path=${path:1}
	path=$(normalize-path "$path")
	path=${path:-.}

	# try to serve a gophermap
	local totry=(
		"$path"
		"$path/gophermap"
	)
	local try file
	for try in "${totry[@]}"; do
		if [[ -f $try ]]; then
			file=$try
			break
		fi
	done

	if [[ -n $file ]]; then
		# a static file was found!
		log "$file" "$ip" "$result"
		process-file $file
	elif [[ -d $path ]]; then
		if [[ "$path" != */ ]]; then
			path="${path}/"
		fi
		log "$path" "$ip" "$result"
		list-directory "$path" >&"$fd"
	else
		# nothing was found
		printf 'Not Found\r\n' >&"$fd"
		printf '\r\n' >&"$fd"
	fi

	exec {fd}>&-

}

main() {
	enable accept || fatal 'failed to load accept'
	enable tee # fallback to POSIX tee if not loadable

	local OPTARG opt
	while getopts 'b:p:d:h:v' opt; do
		case "$opt" in
		b) HOST=$OPTARG ;;
		p) PORT=$OPTARG ;;
		d) DIR=$OPTARG ;;
		h)
			echo "$USAGE"
			exit 0
			;;
		v)
			echo "$VERSION"
			exit 0
			;;
		*)
			echo "$USAGE" >&2
			exit 2
			;;
		esac
	done

	cd "$DIR" || fatal "failed to move to $DIR"

	echo "listening on gopher://$HOST:$PORT"
	echo "serving out of $PWD"

	local fd ip
	while true; do
		accept -b "$HOST" -v fd -r ip "$PORT" ||
			fatal 'failed to read socket'
		process-request "$fd" &

		exec {fd}>&-
	done
}

main "$@"
