mirror of
https://github.com/ohmyzsh/ohmyzsh.git
synced 2026-02-11 05:39:45 +08:00
156 lines
5.0 KiB
Plaintext
156 lines
5.0 KiB
Plaintext
#autoload
|
|
|
|
# This function serves a specified file over HTTP 1.0 on localhost, at a random
|
|
# available port. It waits for a single GET request to the correct URL and then
|
|
# responds with the file contents. If any other request is received, it responds
|
|
# with a 404 Not Found. The server automatically shuts down after serving the
|
|
# file or after a specified timeout.
|
|
#
|
|
# Usage:
|
|
# serve-file-and-quit <file-to-serve> [timeout-secs] [Access-Control-Allow-Origin]
|
|
# <file-to-serve> Path to the file to be served (required)
|
|
# [timeout-secs] Number of seconds to wait for a connection before quitting (default: 30, max: 60)
|
|
# [Access-Control-Allow-Origin] Value for the Access-Control-Allow-Origin header (default: "null")
|
|
#
|
|
# If called from a subshell, it outputs the URL where the file can be accessed, and
|
|
# then closes stdout to avoid hanging while the server is listening. The caller can
|
|
# capture this output.
|
|
#
|
|
# It is intended for temporary sharing of files between local applications,
|
|
# such as opening a trace file in a web-based viewer (trace.ohmyz.sh). It is not
|
|
# meant for production use or serving files over the internet.
|
|
#
|
|
# It's a minimal implementation using zsh's built-in TCP capabilities, primarily
|
|
# to avoid external dependencies.
|
|
#
|
|
# Security considerations:
|
|
#
|
|
# - This server listens on 0.0.0.0 (all interfaces), so it may be accessible from
|
|
# other devices on the same network. I haven't found a way to bind only to localhost
|
|
# using zsh's ztcp module.
|
|
# - As the port is randomized, and the server is short-lived, the risk is mitigated,
|
|
# but not eliminated. An additional mitigation could be to use a random URL path segment.
|
|
# - It's not a proper HTTP server, so it uses very rudimentary request parsing and
|
|
# response generation. It currently does not support binary files, and probably
|
|
# never will.
|
|
|
|
setopt localoptions localtraps
|
|
|
|
zmodload zsh/net/tcp
|
|
zmodload zsh/zselect
|
|
|
|
local file="$1"
|
|
local timeout_secs="${2:-30}"
|
|
local AccessControlAllowOrigin=${3:-"null"}
|
|
|
|
if [[ -z "$file" ]]; then
|
|
print -ru2 "Usage: $0 <file-to-serve> [timeout-secs] [Access-Control-Allow-Origin]"
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -r "$file" ]]; then
|
|
print -ru2 "Error: file '$file' not found or not readable."
|
|
return 1
|
|
fi
|
|
|
|
if (( timeout_secs < 1 || timeout_secs > 60 )); then
|
|
print -ru2 "Error: timeout must be a positive integer between 1 and 60."
|
|
return 1
|
|
fi
|
|
|
|
local pathname="$(omz_urlencode -P "/${file:t}")"
|
|
|
|
# 1. Get a random available port
|
|
local port=$(( RANDOM % 32767 + 1100 ))
|
|
while ! ztcp -l "$port" &>/dev/null; do
|
|
(( port++ ))
|
|
|
|
if (( port > 65535 )); then
|
|
print -ru2 "Error: failed to start server: no available ports."
|
|
return 1
|
|
fi
|
|
done
|
|
ztcp -c $REPLY # inmediately close the test connection
|
|
|
|
# 2. Output file URL to stdout for caller to use
|
|
# Here the subcommand substitution captures the output and returns it to the caller
|
|
if [[ $ZSH_SUBSHELL -gt 0 ]]; then
|
|
echo "http://localhost:$port${pathname}"
|
|
exec >/dev/null # close stdout in the subshell to avoid hanging
|
|
fi
|
|
|
|
# Start of asynchronous server block
|
|
{
|
|
local response_body request_line listen_fd fd
|
|
|
|
# 1. Start listening on the selected port
|
|
if ! ztcp -l $port; then
|
|
print -ru2 "Error: failed to start server on port $port."
|
|
return 1
|
|
fi
|
|
listen_fd=$REPLY
|
|
|
|
print -ru2 "Serving file '${file:t}' on http://localhost:$port${pathname} ..."
|
|
|
|
# 2. Wait for a connection, with a timeout (in hundredths of a second)
|
|
while zselect -t $(( timeout_secs * 100 )) -r $listen_fd; do
|
|
# 3. Accept the connection
|
|
ztcp -a $listen_fd
|
|
fd=$REPLY
|
|
|
|
# 4. Read the request line (the first line is usually enough to determine the path)
|
|
# Note: read up to 2048 bytes to avoid blocking indefinitely
|
|
sysread -s 2048 -i $fd request
|
|
local request_line="${${(f)request}[1]}"
|
|
|
|
# 5. If the request is not for the expected pathname, respond with 404 Not Found
|
|
if [[ "${${(f)request}[1]}" != "GET ${pathname} HTTP/1."* ]]; then
|
|
local response_body="404 Not Found"
|
|
local http_response=$'HTTP/1.0 404 Not Found\r
|
|
Content-Type: text/plain\r
|
|
Content-Length: %d\r
|
|
\r
|
|
%s\r
|
|
'
|
|
>&$fd builtin printf -- "$http_response" \
|
|
$(( ${#response_body} + 2 )) \
|
|
"$response_body"
|
|
|
|
# 5.1. Close the connection file descriptor and try again
|
|
ztcp -c $fd
|
|
continue
|
|
fi
|
|
|
|
# 6. If the request matches the pathname, respond with the contents of the file
|
|
local response_body="$(<$file)"
|
|
local http_response=$'HTTP/1.0 200 OK\r
|
|
Access-Control-Allow-Origin: %s\r
|
|
Content-Type: text/plain;charset=utf-8\r
|
|
Content-Length: %d\r
|
|
\r
|
|
%s\r
|
|
'
|
|
>&$fd builtin printf -- "$http_response" \
|
|
"$AccessControlAllowOrigin" \
|
|
$(( ${#response_body} + 2 )) \
|
|
"$response_body"
|
|
|
|
# 6.1. Close the connection file descriptor and stop the loop
|
|
ztcp -c $fd
|
|
break
|
|
done
|
|
|
|
return 0
|
|
} always {
|
|
# Non-zero if error or timeout occurred
|
|
local ret=$?
|
|
|
|
# 7. Close remaining file descriptors and shut down the server
|
|
print -ru2 "Server stopped."
|
|
|
|
[[ -z "$fd" ]] || ztcp -c $fd 2>/dev/null
|
|
[[ -z "$listen_fd" ]] || ztcp -c $listen_fd 2>/dev/null
|
|
|
|
return $ret
|
|
} &|
|