⚠️ 不要在家尝试这个:仅用 Bash 编写的 CMS??
又来了。
在仅使用 CSS 构建图像模式(完全忽略了可访问性(抱歉,@grahamthedev))并尝试将 CSS 确立为后端语言(尽管它有效,但出于某种原因,你们并不太喜欢它。我不知道为什么。)之后,我们终于回来做一些你和我可能永远不应该做的事情。
今天:让我们使用 Bash(是的,Bash)来创建一个 CMS!
帕斯卡号
帕斯卡,是的!帕斯卡总是好的!
功劳归功劳:这个想法源于我和一位同事关于如何把事情复杂化的一次有趣讨论。他提出了一个“bash 服务器”,我们开始把它越做越大,直到我说“接受挑战”,于是我们就这么定了下来。
免责声明:除了一些密码加密和大多数最佳实践外,我们将忽略安全性。我永远不会在生产环境中使用这个工具,亲爱的读者,您也不应该这样做。拜托。好了。我已经警告过你了。
定义它的作用
我们将创建一个非常基本的 CMS:
- 用户可以登录和注销(因此我们需要用户和会话处理)
- 登录用户可以创建、更新和删除页面
- 匿名用户可以阅读所有页面
- 页面由导航标题、路由路径、一些标记以及是否应显示在主导航中的标志组成
然而,要使用 Bash 作为后端语言,我们首先需要让它处理 HTTP 请求。幸运的是,有一些 Bash 实用程序可以用来监听 TCP 请求并发送响应,其中最值得注意的是netcat
。我们“仅仅”需要解析请求并生成响应。
一旦完成,我们将使用 SQLite 加载请求的页面并呈现其标记。
让我们继续讨论第一部分代码。
数据库模式
先说无聊的部分。我们将使用以下数据库模式作为我们的 SQLite 数据库,并添加一些默认记录:
CREATE TABLE IF NOT EXISTS pages (
routePath TEXT NOT NULL,
navTitle TEXT,
isInMainNavigation BOOLEAN NOT NULL,
markup TEXT NOT NULL
);
INSERT INTO pages VALUES
('/', 'Home', 1, '<h2>Hello, Bash CMS!</h2>'),
('/about', 'About', 1, '<h2>About</h2><p>This page was created entirely with BashCMS!</p> <p><a href="/about/bash-cms">Learn more</a></p>'),
('/about/bash-cms', NULL, 0, '<h2>About BashCMS</h2><p>BashCMS is a CMS entirely written in Bash!</p>');
CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL,
password TEXT NOT NULL /* Hash generated with sha256sum */
);
INSERT INTO users VALUES
('admin', 'fc8252c8dc55839967c58b9ad755a59b61b67c13227ddae4bd3f78a38bf394f7'); /* pw: admin */
CREATE TABLE IF NOT EXISTS sessions (
sessId TEXT NOT NULL,
userRowId INTEGER
);
我们可以通过 CLI 工具访问数据库sqlite3
,并从 Bash 脚本中向数据库提供数据。
实际服务器
现在让我们从 Bash 部分开始。为了监听 HTTP 请求,我们使用 Netcat。我们将使用 OpenBSD 版本的 Netcat。
在监听模式下,Netcat 是交互式的。它会在 STDOUT 上打印出请求的详细信息(例如,请求头、请求体、HTTP 方法等等),并要求用户在 STDIN 上写入响应。
对于不熟悉 Linux/Bash 的人来说,STDIN 和 STDOUT 是与程序通信的默认方式。我们在终端中看到的内容通常是 STDOUT,键盘输入是 STDIN。程序可以从 STDIN 读取并写入 STDOUT。
一旦我们向 Netcat 发送了数据,它就会通过网络发送并终止。这意味着我们一次只能处理一个请求。为了让服务器持续运行,我们需要在它终止后重新启动 Netcat 并让它监听。
要从 Netcat 进行读写,我们还需要一种以编程方式读取 STDOUT 并写入 STDIN 的方法。我们可以使用一个名为 的实用程序来实现这一点coproc
,该实用程序在子 shell 中异步执行给定命令。只要 Netcat 正在等待传入请求,我们什么也不做。只有当 Netcat 开始写入 STDOUT 时,我们才会开始读取并将其保存到变量中。
然而,有一个小问题:Netcat 不会告诉我们它是否以及何时完成了对 STDOUT 的写入。我们需要自己判断。最直接的方法是等待一个空的新行,然后就此停止。
我们最终会得到如下结构:
while true # You know your script will be fun when it starts with an endless loop.
do
coproc nc -l -p 1440 -q1 -v # Spawns netcat such that we can read from and write to it
REQ_RAW="" # This will contain the entire request
IFS="" # Delimiter for `read`
while read -r TMP; do # Read every line from STDOUT
REQ_RAW+=$TMP$'\n' # Append the line to the REQ variable
# If the length of TMP is equal to one byte
if [[ ${#TMP} -eq 1 ]] ; then
break
fi
done <&"${COPROC[0]}" # Reads from the coproc STDOUT, line by line
echo $REQ_RAW # Output the request for testing purposes
kill "$COPROC_PID" # Kill the process for the subsequent request
wait "$COPROC_PID" # Wait until it's actually gone
done
如果你真的不熟悉 Bash,这看起来会让人望而生畏。即使对 Bash有所了解的人,可能也会如此。我在这次实验中学到了很多,并且深信 Bash 的工作原理非常类似于量子物理学:人们可能不理解 Bash,但习惯了它。
言归正传……当我们想要读取 POST 请求的 HTTP 正文时,“空行”方法就失效了。幸好,HTTP 有一个叫做 的头Content-Length
,可以告诉我们确切的字节数。
这极大地破坏了我们的服务器代码:
while true
do
coproc nc -l -p 1337 -q1 -v # Spawns netcat such that we can read from and write to it. Listens on port 1337.
REQ_RAW="" # This will contain the entire request
IFS="" # Delimiter for `read`
while read -r TMP; do
REQ_RAW+=$TMP$'\n' # Append the line to the REQ variable
TMPLEN=$(echo $TMP|wc -c) # Figure out the length of $TMP in bytes
# Deduct the length of the read bytes from the rest of the body length
if [[ $BODYLENGTH -ge 0 ]]; then # Still some body left to read
BODYLENGTH=$((BODYLENGTH - TMPLEN))
fi
# If the request has a body (determined by the header, which is usually the last one)
# We continue reading the exact number of bytes
if [[ "$TMP" =~ ^"Content-Length: " ]]; then
BODYLENGTH=$(echo "$TMP"|grep -o '[[:digit:]]\+')
HAS_BODY=1
fi
# Read the entire body; abort reading
if [[ $HAS_BODY -eq 1 ]] && [[ $BODYLENGTH -le 0 ]]; then
break
fi
# No body but empty line encountered, abort reading
if [[ $HAS_BODY -eq 0 ]] && [[ $TMPLEN -le 2 ]]; then
break
fi
done <&"${COPROC[0]}" # Reads from the coproc STDOUT, line by line
# Display the entire request for debugging
echo $REQ_RAW
kill "$COPROC_PID" # Kill the process for the subsequent request
wait "$COPROC_PID" # Wait until it's actually buried
done
这已经运行良好了。我们现在基本上有一个请求记录器了。进展顺利!
HTTP 请求的剖析
首先,我们需要解析请求,以确定服务器应该执行什么。让我们看看我们正在处理什么。
典型的 HTTP 请求结构如下:
[Method] [Path + Query String] HTTP/[HTTP Version]
[Headers]
[Body]
当我在服务器上执行 GET 请求时,它会输出如下内容:
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Cache-Control: no-cache
Host: localhost:1440
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
另一方面,POST 请求可能如下所示:
POST / HTTP/1.1
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Cache-Control: no-cache
Host: localhost:1440
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------328683080620751780512479
Content-Length: 169
----------------------------328683080620751780512479
Content-Disposition: form-data; name="hello"
world
----------------------------328683080620751780512479--
我们可以利用这一点。
添加一些逻辑
该请求作为单个字符串存储在名为的变量中REQ_RAW
,因此我们可以使用其他几个 Bash 实用程序对其进行解析。
我们创建一个名为的函数parse_request
,并将其放入单独的文件中,以便保持条理清晰。然后在读取循环之后调用此函数:
#!/usr/bin/bash
source ./server/parse_request.sh
while true # Continue doing this, kind of like an event loop.
do
coproc nc -l -p 1440 -q1 -v # Spawns netcat such that we can read from and write to it
## ...
# Declare an associative array called `REQUEST`
declare -A REQUEST=()
parse_request $REQ_RAW REQUEST
# Print the contents of the associative array
declare -p REQUEST
# Add more magic here
kill "$COPROC_PID" # Kill the process for the subsequent request
wait "$COPROC_PID" # Wait until it's actually gone
done
此函数需要同时执行几件事:
- 确定 HTTP 方法
- 确定用户请求的路线
- 解析任何 GET 变量(即
?foo=bar
等) - 解析主体
- 解析出 cookies
我们可以解析请求的第一行来获取 HTTP 方法和路由路径。之后,我们解析 Cookie 并检查是否需要解析请求体(这只发生在 POST 和 PUT 请求中)。
#
# Parses the entire request
#
function parse_request() {
RAW_REQ=$1
# This makes the REQUEST associative array available to write to
# We need to make sure to not call it REQUEST, though, because
# that name is already reserved in the outer scope
declare -n INNER_REQ="$2"
# Extract the request line: method, path (+ query string) and version
REQUESTLINE=`echo "${RAW_REQ}" | sed -n 1p`
IFS=' ' read -ra PARTS <<< "$REQUESTLINE"
METHOD=${PARTS[0]}
REQUEST_PATH=${PARTS[1]}
# Split query string from the actual route
IFS='?' read -ra REQUEST_PATH_PARTS <<< "$REQUEST_PATH"
REQUEST_ROUTE=${REQUEST_PATH_PARTS[0]}
QUERY_STRING=${REQUEST_PATH_PARTS[1]}
if [[ "$QUERY_STRING" != "" ]]; then
parse_query_string $QUERY_STRING INNER_REQ
fi
parse_cookies $RAW_REQ INNER_REQ
# If we're dealing with either a POST or a PUT request, chances are there's a form body.
# We extract that with the previously found $FORMDATA_BOUNDARY.
if [[ "$METHOD" == "POST" ]] || [[ "$METHOD" == "PUT" ]]; then
parse_body $RAW_REQ INNER_REQ
fi
INNER_REQ["METHOD"]="$METHOD"
INNER_REQ["ROUTE"]="$REQUEST_ROUTE"
}
查询字符串解析非常简单:
#
# Parses the query string and assigns it to the request object
#
function parse_query_string() {
RAW_QUERY_STRING=$1
declare -n REQ_ARR="$2"
# Split the query parameters into a hashmap
IFS='&' read -ra QUERYPARTS <<< "$QUERY_STRING"
for PART in "${QUERYPARTS[@]}"; do
IFS='=' read -ra KEYVALUE <<< "$PART"
KEY=${KEYVALUE[0]}
VALUE=${KEYVALUE[1]}
REQ_ARR["QUERY","$KEY"]="$VALUE"
done
}
Cookie 解析也是如此:
#
# Parses cookies out of the request headers
#
function parse_cookies() {
RAW_REQ_BODY=$1
declare -n REQ_ARR="$2"
COOKIE_LINE=`echo $RAW_REQ_BODY|grep 'Cookie:'`
COOKIE=${COOKIE_LINE#"Cookie:"}
if [[ "$COOKIE" != "" ]]; then
IFS=';' read -r -d '' -a COOKIEPARTS <<< "$COOKIE"
for PART in "${COOKIEPARTS[@]}"; do
if [[ "$PART" != "" ]]; then
IFS='=' read -ra KEYVALUE <<< "$PART"
KEY=${KEYVALUE[0]//" "/""} # Remove all spaces, so we don't have leading spaces
VALUE=${KEYVALUE[1]}
REQ_ARR["COOKIE","$KEY"]=${VALUE::-1}
fi
done
fi
}
在这两个函数中,我们仔细地从整个请求中抽取必要的部分,并用一些字符进行拆分,例如,查询字符串?
用和,cookie 用和。然后,我们删除一些不必要的空格,并将其写入关联数组。=
;
=
REQUEST
解析正文更加复杂。我们需要处理multipart/form-data
格式以支持多行字符串,甚至可能支持文件上传。我发现它实际上比任何 URL 编码都更简单易用。
#
# Parses the POST body and assigns it to the request object
#
function parse_body() {
RAW_REQ_BODY=$1
declare -n REQ_ARR="$2"
FORM_BOUNDARY_LINE=`echo $RAW_REQ_BODY|grep 'Content-Type: multipart/form-data; boundary='`
FORM_BOUNDARY=${FORM_BOUNDARY_LINE#"Content-Type: multipart/form-data; boundary="}
# Replace the $FORMDATA_BOUNDARY with a single character so we can split with that.
TMP_BODY_PARTS=`echo "${RAW_REQ_BODY//"$FORM_BOUNDARY"/$'§'}" | head -n -2` # We need to use _some_ character to use `read` here.
IFS='§' read -r -d '' -a BODYPARTS <<< "$TMP_BODY_PARTS"
for PART in "${BODYPARTS[@]}"; do
KEY=`echo "${PART}" | grep -o -P '(?<=name=").*?(?=")'`
if [[ "$KEY" != "" ]]; then
VALUE=`echo "${PART}" | head -n -1 | tail -n +4`
REQ_ARR["BODY","$KEY"]=${VALUE::-1}
fi
done
}
当我们使用之前的 GET 请求运行代码时,我们会从 Bash 服务器获得以下输出:
declare -A REQUEST=([ROUTE]="/" [METHOD]="GET" )
(是的,declare -p
创建一个declare -A
语句,因此可以再次执行该语句以获得相同的关联数组。)
上述 POST 请求将输出以下内容:
declare -A REQUEST=([BODY,hello]="world" [ROUTE]="/" [METHOD]="POST" )
整洁的!
对请求作出反应
与数组类似REQUEST
,我们声明一个RESPONSE
数组。该数组将包含我们传递的 DOM、状态码以及一些标头,例如用于重定向的Set-Cookie
或。Location
由于我们需要区分用户(有些用户已登录,有些用户未登录),我们实现了一个名为 的函数set_session
。该函数会生成一个会话 ID,将其写入 SQLite 数据库,并设置一个会话 Cookie。来自同一客户端的任何后续请求都将发送相同的会话 ID Cookie。
function set_session() {
declare -n REQ="$1"
declare -n RES="$2"
if [[ "${REQ[COOKIE,SESSID]}" != "" ]]; then
# SESSID cookie was already set once; reset it
RES["COOKIES,SESSID"]="${REQ[COOKIE,SESSID]}"
else
# No SESSID cookie, so let's generate one
SESSID=`echo $RANDOM | md5sum | head -c 20; echo;` # Taken from SO.
# Save cookie into database
sqlite3 db.sqlite "insert into sessions values ('${SESSID}', NULL);" ".exit"
RES["COOKIES,SESSID"]="$SESSID"
fi
}
请注意,我们需要REQ
和数组:我们已经通过设置一个带有名为的子键的键RES
来写入数组。RESPONSE
COOKIES
SESSID
我们在调用之后调用此函数parse_request
:
while true; do
# Request reading shenanigans
declare -A REQUEST=()
declare -A RESPONSE=()
parse_request $REQ_RAW REQUEST
set_session REQUEST RESPONSE
# More stuff later, don't worry
kill "$COPROC_PID" # Kill the process for the subsequent request
wait "$COPROC_PID" # Wait until it's actually gone
done
接下来,我们可以实现一个函数来响应实际的请求。我们称之为render_cms_page
。在这个函数中,我们在数据库中查找任何与请求中的路由匹配的条目:
function render_cms_page() {
REQUEST=$1
declare -n RES="$2"
DOM=`sqlite3 db.sqlite "select markup from pages where routePath='${REQUEST[ROUTE]}';" ".exit"`
if [[ "$DOM" == "" ]]; then
RES["BODY"]=`render_response "Not found."`
RES["STATUS"]="404 Not found"
else
RES["BODY"]=`render_response $DOM`
RES["STATUS"]="200 OK"
fi
}
你可能render_response
也注意到了里面的函数。我们用它生成所有相关的 HTML,比如页面标题、导航和一些 CSS:
function render_response() {
DOC_START=`doc_start $1`
PAGE_HEADER=`page_header`
DOC_END=`doc_end`
cat <<EOF
$DOC_START
$PAGE_HEADER
<main>
$2
</main>
$DOC_END
EOF
}
然而,在那里doc_start
我们有函数,page_header
和doc_end
:
function doc_start() {
cat <<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>$1</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jgthms/minireset.css@master/minireset.min.css">
<style>
* { font-family: monospace; font-size: 16px; }
h1 { font-size: 1.5rem; font-weight: bold; }
h2 { font-size: 2rem; font-weight: bold; margin-bottom: 0.5rem; }
p { margin-bottom: 0.5rem; }
header { padding: 1rem; border-bottom: 1px solid #000; }
main { padding: 1rem; }
nav ul { margin-top: 1rem; display: flex; gap: 1rem; }
label { display: block; }
</style>
</head>
<body>
EOF
}
function doc_end() {
cat <<EOF
</body>
</html>
EOF
}
function page_header() {
# Fetch header-relevant pages
cat <<EOF
<header>
<h1>Bash CMS</h1>
<nav>
<ul>
EOF
while read -r PAGE_ROW; do
IFS='|' read -r -d '' -a PAGE_PARTS <<< "$PAGE_ROW"
NAV_TITLE=${PAGE_PARTS[0]}
ROUTE_PATH=${PAGE_PARTS[1]}
cat <<EOF
<li><a href="$ROUTE_PATH">$NAV_TITLE</a></li>
EOF
done <<< `sqlite3 db.sqlite "select navTitle, routePath from pages where isInMainNavigation=1;" ".exit"`
LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`
if [[ "$LOGGEDIN_USERID" == "" ]]; then
cat <<EOF
<li><a href="/login" class="login">Login</a></li>
EOF
else
cat <<EOF
<li><a href="/add-new-page" class="login">Add new page</a></li>
<li><a href="/logout" class="login">Logout</a></li>
EOF
fi
cat <<EOF
</ul>
</header>
EOF
}
这样我们就快完成了。
实际响应的最后一步是渲染响应字符串。与 HTTP 请求类似,响应是一个由不同部分组成的多行字符串。我们只需要正确地组装它:
function generate_response_string() {
declare -n RES="$1"
# Transform cookie entries into Set-Cookie headers
COOKIES=""
for RESKEY in "${!RES[@]}"; do
if [[ "$RESKEY" =~ ^"COOKIES," ]]; then
COOKIE_NAME=${RESKEY#"COOKIES,"}
COOKIES+="Set-Cookie: $COOKIE_NAME=${RES[$RESKEY]}
" # Adds a newline after this Set-Cookie header.
fi
done
RES["CONTENT_TYPE"]="text/html"
RES["HEADERS","Content-Type"]="${RES[CONTENT_TYPE]}; charset=UTF-8"
RES["HEADERS","Server"]="Bash. Please don't send cat /etc/passwd as a cookie because hacking is bad :("
HEADERS=""
for RESKEY in "${!RES[@]}"; do
if [[ "$RESKEY" =~ ^"HEADERS," ]]; then
HEADER_NAME=${RESKEY#"HEADERS,"}
HEADERS+="${HEADER_NAME}: ${RES[$RESKEY]}
" # Adds a newline after this Set-Cookie header.
fi
done
# declare -p RES
cat <<EOF
HTTP/1.1 ${RES[STATUS]}
${COOKIES::-1}
${HEADERS}
${RES[BODY]}
EOF
}
一切准备就绪,让我们看看它做了什么:
(附注:现在在任何地方使用反引号都让我感到紧张。谁知道它会执行什么操作......)
添加路线
现在我们已经实现了动态路由,让我们来处理静态路由,例如/login
、/edit
、和。为此,我们再添加两个函数:一个用于登录表单,一个用于编辑表单:/add-new-page
/logout
/delete
function render_login_page() {
cat <<EOF
<form method="POST" action="/login" enctype="multipart/form-data">
<label for="username">User name</label>
<input type="text" name="username" id="username" value="$1">
<label for="password">Password</label>
<input type="password" name="password" id="password">
<button type="submit">Login</button>
</form>
EOF
}
function render_edit_form() {
NAVTITLE=$1
IS_IN_MAIN_NAVIGATION=""
ROUTEPATH=$3
DOM=$4
if [[ "$2" == "1" ]]; then
IS_IN_MAIN_NAVIGATION=" checked"
fi
cat <<EOF
<form method="POST" enctype="multipart/form-data">
<label for="navtitle">Navigation title</label>
<input type="text" name="navtitle" id="navtitle" value="$NAVTITLE">
<label for="routepath">Route path</label>
<input type="text" name="routepath" id="routepath" value="$ROUTEPATH">
<label for="is_in_main_navigation">
Is it in navigation?
<input type="checkbox" value="1" name="is_in_navigation"$IS_IN_MAIN_NAVIGATION>
</label>
<div>
<label for="dom">Content</label>
<textarea name="dom" id="dom" style="width: 100%; height: 300px;">$DOM</textarea>
</div>
<button type="submit">Save</button>
</form>
EOF
}
最后,我们扩展该render_cms_page
函数:
function render_cms_page() {
REQUEST=$1
declare -n RES="$2"
if [[ "${REQUEST[ROUTE]}" == "/login" ]]; then
if [[ "${REQUEST[METHOD]}" == "POST" ]]; then
USERNAME=${REQUEST[BODY,username]}
PASSWORD=`echo ${REQUEST[BODY,password]} | sha256sum`
USERID=`sqlite3 db.sqlite "select rowid from users where username='$USERNAME' and password='${PASSWORD::-3}'"`
if [[ "$USERID" == "" ]]; then
DOM=`render_login_page $USERNAME`
DOM+="<p>Username or password incorrect</p>"
RES["BODY"]=`render_response "Login" $DOM`
RES["STATUS"]="200 OK"
else
sqlite3 db.sqlite "update sessions set userRowId = $USERID where sessId = '${REQUEST[COOKIE,SESSID]}'"
RES["STATUS"]="307 Temporary Redirect"
RES["HEADERS","Location"]="/"
fi
else
DOM=`render_login_page`
RES["BODY"]=`render_response "Login" $DOM`
RES["STATUS"]="200 OK"
fi
DOM=`render_login_page ${REQUEST[BODY,username]}`
elif [[ "${REQUEST[ROUTE]}" == "/logout" ]]; then
sqlite3 db.sqlite "update sessions set userRowId = NULL where sessId = '${REQUEST[COOKIE,SESSID]}'"
RES["STATUS"]="307 Temporary Redirect"
RES["HEADERS","Location"]="/"
elif [[ "${REQUEST[ROUTE]}" == "/add-new-page" ]]; then
if [[ "${REQUEST[METHOD]}" == "POST" ]]; then
IS_IN_MAIN_NAVIGATION="0"
if [[ "${REQUEST[BODY,is_in_navigation]}" == "1" ]]; then
IS_IN_MAIN_NAVIGATION="1"
fi
sqlite3 db.sqlite "insert into pages values ('${REQUEST[BODY,routepath]}', '${REQUEST[BODY,navtitle]}', ${IS_IN_MAIN_NAVIGATION}, '${REQUEST[BODY,dom]}');" ".exit"
RES["STATUS"]="307 Temporary Redirect"
RES["HEADERS","Location"]="${REQUEST[BODY,routepath]}"
else
DOM=`render_edit_form`
RES["BODY"]=`render_response "New page" $DOM`
RES["STATUS"]="200 OK"
fi
elif [[ "${REQUEST[ROUTE]}" == "/edit" ]]; then
LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`
if [[ "$LOGGEDIN_USERID" == "" ]]; then
RES["STATUS"]="403 Forbidden"
RES["BODY"]=`render_response "Nope" "Not allowed to do that"`
else
if [[ "${REQUEST[METHOD]}" == "POST" ]]; then
IS_IN_MAIN_NAVIGATION="0"
if [[ "${REQUEST[BODY,is_in_navigation]}" == "1" ]]; then
IS_IN_MAIN_NAVIGATION="1"
fi
sqlite3 db.sqlite "update pages set routePath='${REQUEST[BODY,routepath]}', navTitle='${REQUEST[BODY,navtitle]}', isInMainNavigation=${IS_IN_MAIN_NAVIGATION}, markup='${REQUEST[BODY,dom]}' where routePath='${REQUEST[QUERY,route]}';" ".exit"
RES["STATUS"]="307 Temporary Redirect"
RES["HEADERS","Location"]="${REQUEST[BODY,routepath]}"
else
PAGE=`sqlite3 db.sqlite "select navTitle, isInMainNavigation, routePath, markup from pages where routePath='${REQUEST[QUERY,route]}'"`
IFS='|' read -r -d '' -a PAGEPARTS <<< "$PAGE"
DOM=`render_edit_form ${PAGEPARTS[0]} ${PAGEPARTS[1]} ${PAGEPARTS[2]} ${PAGEPARTS[3]}`
RES["BODY"]=`render_response "Edit" $DOM`
RES["STATUS"]="200 OK"
fi
fi
elif [[ "${REQUEST[ROUTE]}" == "/delete" ]]; then
LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`
if [[ "$LOGGEDIN_USERID" == "" ]]; then
RES["STATUS"]="403 Forbidden"
RES["BODY"]=`render_response "Nope" "Not allowed to do that"`
else
sqlite3 db.sqlite "delete from pages where routePath='${REQUEST[QUERY,route]}';" ".exit"
echo "delete from pages where routePath='${REQUEST[QUERY,route]}';"
RES["STATUS"]="307 Temporary Redirect"
RES["HEADERS","Location"]="/"
fi
else
DOM=`sqlite3 db.sqlite "select markup from pages where routePath='${REQUEST[ROUTE]}';" ".exit"`
LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`
if [[ "$LOGGEDIN_USERID" != "" ]]; then
DOM+="<div style='margin-top: 20px;'><a href='/edit?route=${REQUEST[ROUTE]}'>Edit</a> | <a href='/delete?route=${REQUEST[ROUTE]}'>Delete</a></div>"
fi
if [[ "$DOM" == "" ]]; then
RES["BODY"]=`render_response "Not found" "Not found."`
RES["STATUS"]="404 Not found"
else
RES["BODY"]=`render_response "Bash CMS!" $DOM`
RES["STATUS"]="200 OK"
fi
fi
}
一切顺利!我们用 443 行代码,仅用 Bash 从头开始编写了一个基本的 CMS!
演示时间!
(该 gif 可能需要几秒钟才能加载...)
问答时间!
问:性能好吗?
答:不,完全不会。这个脚本一次只能处理一个请求。Apache 也可以同时处理数百个连接。
问:我应该使用这个...
答:不。看在上帝的份上,请不要这么做。
问:字体一定要等宽吗?这太90年代了。
答:是的。我们用的是 Bash,那为什么不用等宽字体呢?
问:还有什么吗?
答:顺便说一下,我使用 Arch。
希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下你的❤️ !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。
如果你想支持我的努力, 可以请我喝杯咖啡☕ ,或者 在推特上关注我🐦 ! 你也可以直接通过PayPal支持我!
鏂囩珷鏉ユ簮锛�https://dev.to/thormeier/dont-try-this-at-home-a-cms-writing-in-bash-only-4j6i