邊界條件
固定IP
一台12年前的x86電腦
OpenWrt略知一二
從ISP業者(網際網路服務供應商)取得固定IP
這裡以中華電信pppoe為例
寫進/etc/config/network
config interface 'wan'
option device 'eth1'
option proto 'pppoe'
option username '********@ip.hinet.net'
option password '******************'
option ipv6 'auto'
opkg update
opkg install uhttpd uhttpd-mod-ubus
uci set uhttpd.main.listen_http='0.0.0.0:80'
uci set uhttpd.main.listen_https='0.0.0.0:443'
uci set uhttpd.main.home='/www'
uci commit uhttpd
/etc/init.d/uhttpd restart
#透過增加 script_timeout 成600秒 和 network_timeout 成300秒避免上傳時間過久而自動中止
vi /etc/config/uhttpd
option cgi_prefix '/cgi-bin'
list lua_prefix '/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua'
option script_timeout '600'
option network_timeout '300'
option http_keepalive '20'
option tcp_keepalive '1'
option ubus_prefix '/ubus'
# 開放 port 80 (HTTP)
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-HTTP'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].proto='tcp'
uci set firewall.@rule[-1].dest_port='80'
uci set firewall.@rule[-1].target='ACCEPT'
# 開放 port 443 (HTTPS) 可選
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-HTTPS'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].proto='tcp'
uci set firewall.@rule[-1].dest_port='443'
uci set firewall.@rule[-1].target='ACCEPT'
uci commit firewall
/etc/init.d/firewall restart
建立首頁index.html放在/www/下
確保 /www/index.html
存在或放置你要提供的網頁內容。
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>歡迎來到我的 OpenWrt_coarse fish 網頁</title>
<style>
body {
background-color: #f0f0f0;
font-family: "Segoe UI", "Noto Sans TC", sans-serif;
text-align: center;
padding: 50px;
}
h1 {
color: #2c3e50;
}
p {
color: #555;
}
.ip-box {
margin-top: 30px;
background-color: #fff;
padding: 20px;
border-radius: 8px;
display: inline-block;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<h1>🎉 OpenWrt_一頭雜魚的海洋 網站架設成功!</h1>
<p>您正在使用 uHTTPd 提供網頁服務</p>
<div class="ip-box">
<p><strong>LAN IP:</strong>甘你屁事</p>
<p><strong>Public IP:</strong> 1.34.212.93</p>
<p><strong>固定對端 IP:</strong>甘你屁事</p>
</div>
<hr>
<h2>檔案上傳</h2>
<form action="/cgi-bin/upload.sh" method="post" enctype="multipart/form-data">
<p>
<label for="username">使用者名稱:</label>
<input type="text" id="username" name="username" required>
</p>
<p>
<label for="password">密碼:</label>
<input type="password" id="password" name="password" required>
</p>
<p>
<label for="fileToUpload">選擇檔案:</label>
<input type="file" id="fileToUpload" name="fileToUpload" required>
</p>
<p>
<button type="submit">上傳檔案</button>
</p>
</form>
</body>
</html>
請注意,form
標籤中的 action="/cgi-bin/upload.sh"
假設您的後端處理腳本會放在 /www/cgi-bin/
目錄下,且檔名為 upload.sh
http:// ISP業者提供的IP/
https://deltawen2.github.io/just_Learning_notes/openwrt_OpenSSH.html
整個方案將由兩個檔案組成
upload.sh
(CGI Wrapper):一個非常簡單的 Shell 腳本,用來啟動 Python 腳本並設定環境。#創建/www/cgi-bin/upload.sh
vi /www/cgi-bin/upload.sh
#!/bin/sh
# 導向標準輸入 (stdin) 和環境變數給 Python 腳本
# uHTTPd 會將 POST 資料透過 stdin 傳入
/usr/bin/python3 /www/cgi-bin/upload.py
#設定執行權限
chmod +x /www/cgi-bin/upload.sh
upload.py
(主要邏輯):一個功能完整的 Python 腳本,負責處理上傳、驗證和儲存檔案。確保您的 OpenWrt 系統已安裝 Python 3
opkg update && opkg install python3
#創建/www/cgi-bin/upload.py
vi /www/cgi-bin/upload.py
#!/usr/bin/env python3
# 導入必要的模組
import cgi
import cgitb
import sys
import os
import crypt # 用於驗證系統使用者密碼
import datetime # 導入 datetime 模組來處理時間
# 啟用詳細的錯誤報告,方便除錯。
# 在生產環境中,建議禁用此功能,以避免洩露敏感資訊。
cgitb.enable()
# 檔案上傳的目標目錄。
# !!! 警告:請務必將此路徑修改為非網頁公開的目錄,以確保安全。
# 例如:/root/uploads/ 或 /mnt/sda1/uploads/
UPLOAD_DIR = "/www/uploads/" # 您目前設定的路徑。再次強烈建議修改為 /root/uploads/
# 確保上傳目錄存在
if not os.path.exists(UPLOAD_DIR):
try:
os.makedirs(UPLOAD_DIR)
except OSError as e:
# 如果創建目錄失敗,則印出錯誤並退出
print("Content-type: text/html; charset=UTF-8\n") # 修正:加上 charset=UTF-8
print("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>錯誤</title></head><body>") # 修正:加上 <meta charset="UTF-8">
print(f"<h3>❌ 伺服器錯誤</h3>")
print(f"<p>無法建立上傳目錄:{UPLOAD_DIR}。請檢查權限或路徑。錯誤訊息:{e}</p>")
print("</body></html>")
sys.exit(1) # 退出腳本
# 函式:驗證使用者帳號和密碼
def authenticate_user(username, password):
"""
使用系統的 /etc/shadow 檔案來驗證使用者密碼。
警告:直接讀取 /etc/shadow 檔案需要腳本有足夠的權限,這可能存在安全風險。
在更安全的生產環境中,建議使用 PAM 模組或更底層的系統驗證 API。
"""
if not username or not password:
return False # 使用者名稱或密碼不能為空
try:
with open("/etc/shadow", "r") as shadow_file:
for line in shadow_file:
parts = line.strip().split(':')
if parts[0] == username:
# 取得雜湊密碼 (hashed password)
hashed_password = parts[1]
# 使用 crypt 模組驗證密碼
return crypt.crypt(password, hashed_password) == hashed_password
except IOError:
# 如果無法讀取 /etc/shadow 檔案,則驗證失敗(例如權限不足)
sys.stderr.write("Error: Cannot read /etc/shadow. Check script permissions.\n")
return False
except Exception as e:
sys.stderr.write(f"Error during authentication: {e}\n")
return False
return False # 如果找不到使用者名稱
# 函式:主程式邏輯
def main():
# 設定 HTTP 回應的 Content-type 為 HTML,並明確指定 UTF-8 編碼
print("Content-type: text/html; charset=UTF-8\n") # 修正:加上 charset=UTF-8
print("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>檔案上傳結果</title></head><body>") # 修正:加上 <meta charset="UTF-8">
try:
# 建立 FieldStorage 物件來解析表單資料 (POST 請求的內容)
form = cgi.FieldStorage()
# 取得使用者名稱和密碼欄位的值
username = form.getvalue("username", "")
password = form.getvalue("password", "")
# 檢查帳號密碼是否正確
if not authenticate_user(username, password):
print("<h3>❌ 登入失敗</h3>")
print("<p>使用者名稱或密碼不正確。</p>")
print("</body></html>")
return # 驗證失敗,直接結束腳本
# 處理檔案上傳
# 檢查 'fileToUpload' 欄位是否存在且是檔案類型
if 'fileToUpload' in form and form['fileToUpload'].filename:
file_item = form['fileToUpload']
# 取得原始檔名,並確保只取檔名部分,避免路徑遍歷攻擊
# 這裡需要注意,如果檔名本身包含非 ASCII 字元,
# file_item.filename 可能已經是 UTF-8 編碼的位元組串,
# 但 os.path.basename 通常能正確處理。
file_name = os.path.basename(file_item.filename)
# 建立檔案的完整儲存路徑
file_path = os.path.join(UPLOAD_DIR, file_name)
# 檢查檔案是否已經存在,如果存在可以考慮重新命名或提示
if os.path.exists(file_path):
print("<h3>⚠️ 警告</h3>")
print(f"<p>檔案 **{file_name}** 已存在,將會覆蓋。</p>")
# 如果不希望覆蓋,可以在這裡修改檔名,例如加上時間戳
# import time
# file_name = f"{os.path.splitext(file_name)[0]}_{int(time.time())}{os.path.splitext(file_name)[1]}"
# file_path = os.path.join(UPLOAD_DIR, file_name)
# 記錄上傳開始時間
start_time = datetime.datetime.now()
# 將檔案內容寫入到指定路徑
with open(file_path, 'wb') as output_file:
# 逐塊讀取上傳的檔案內容並寫入,以處理大檔案
while True:
chunk = file_item.file.read(1024 * 10) # 每次讀取 10KB
if not chunk:
break
output_file.write(chunk)
# 記錄上傳結束時間
end_time = datetime.datetime.now()
# 計算上傳時間
upload_duration = end_time - start_time
# 取得檔案大小
file_size_bytes = os.path.getsize(file_path)
# 將檔案大小轉換為更易讀的格式 (KB, MB, GB)
def format_bytes(size):
# 格式化檔案大小的輔助函數
power = 2**10
n = 0
byte_labels = {0 : 'Bytes', 1: 'KB', 2: 'MB', 3: 'GB', 4: 'TB'}
while size > power:
size /= power
n += 1
return f"{size:.2f} {byte_labels[n]}"
formatted_file_size = format_bytes(file_size_bytes)
print("<h3>✅ 上傳成功!</h3>")
print(f"<p>檔案 **{file_name}** 已成功上傳到!</p>")
print(f"<p>檔案大小:{formatted_file_size}</p>")
print(f"<p>上傳時間:{upload_duration.total_seconds():.2f} 秒</p>")
else:
print("<h3>⚠️ 警告</h3>")
print("<p>沒有選擇檔案或上傳失敗。</p>")
except Exception as e:
# 捕獲所有未預期的錯誤,並印出錯誤訊息
print("<h3>❌ 發生錯誤</h3>")
print(f"<p>處理上傳時發生未預期的錯誤:{e}</p>")
# 為了除錯,也可以印出詳細的 traceback
# import traceback
# print("<pre>")
# print(traceback.format_exc())
# print("</pre>")
print("</body></html>")
# 確保腳本被直接執行時才呼叫 main 函式
if __name__ == "__main__":
main()
#設定執行權限
chmod +x /www/cgi-bin/upload.py
📁 目標
讓外部使用者可以透過瀏覽器存取http://your-ip/image/
,並看到/www/image
目錄下的檔案列表,但無法修改或上傳。
mkdir -p /www/image
chmod 755 /www/image
#編輯文件
vi /etc/config/uhttpd
#加在config uhttpd 'main'最後一行
option list_directories '1'
#在最下面加一組
config alias
option alias '/image'
option target '/www/image'
/etc/init.d/uhttpd restart
config uhttpd 'main' # 主 uhttpd 設定區塊
listen_http '0.0.0.0:80' # 監聽所有 IPv4 位址的 HTTP 連線(port 80)
listen_http '[::]:80' # 監聽所有 IPv6 位址的 HTTP 連線(port 80)
listen_https '0.0.0.0:443' # 監聽所有 IPv4 位址的 HTTPS 連線(port 443)
listen_https '[::]:443' # 監聽所有 IPv6 位址的 HTTPS 連線(port 443)
option redirect_https '0' # 不強制將 HTTP 轉導到 HTTPS(0 = 關閉)
option home '/www' # 設定網站根目錄為 /www
option rfc1918_filter '1' # 啟用 RFC1918 私有 IP 過濾(防止 WAN 端存取 LAN)
option max_requests '3' # 每個連線最多允許 3 個請求(限制資源消耗)
option max_connections '100' # 最大同時連線數為 100(防止過載)
option cert '/etc/uhttpd.crt' # HTTPS 使用的憑證檔案路徑
option key '/etc/uhttpd.key' # HTTPS 使用的私鑰檔案路徑
option cgi_prefix '/cgi-bin' # CGI 程式的 URL 前綴(例如 /cgi-bin/script.cgi)
lua_prefix '/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua' # LuCI 的 Lua 處理器路徑與 URL 對應
option script_timeout '600' # CGI/Lua 腳本最大執行時間(秒)
option network_timeout '300' # 網路連線最大等待時間(秒)
option http_keepalive '20' # HTTP keep-alive 保持連線時間(秒)
option tcp_keepalive '1' # 啟用 TCP keep-alive(保持連線活性)
option ubus_prefix '/ubus' # 設定 ubus API 的 URL 前綴(通常用於 RPC)
option list_directories '1' # 啟用目錄瀏覽功能(允許列出目錄內容)
# ------------------------------------------------------------
config cert 'defaults' # 預設憑證產生設定(用於 HTTPS)
option days '397' # 憑證有效天數(397 天)
option key_type 'ec' # 使用 EC(橢圓曲線)加密類型
option bits '2048' # 金鑰長度(若使用 RSA,這會生效)
option ec_curve 'P-256' # EC 加密使用的曲線(P-256 是常見選擇)
option country 'ZZ' # 憑證中的國家欄位(可自訂)
option state 'Somewhere' # 憑證中的州/省欄位(可自訂)
option location 'Unknown' # 憑證中的城市欄位(可自訂)
option commonname 'OpenWrt' # 憑證中的主機名稱(通常設為裝置名)
# ------------------------------------------------------------
config alias # URL 別名設定(用於虛擬目錄)
option alias '/image' # 設定 URL 路徑為 /image
option target '/www/image' # 對應實體目錄為 /www/image
/etc/mime.types
MIME(Multipurpose Internet Mail Extensions)類型是一種標準格式,用來告訴瀏覽器「這個檔案是什麼類型」,讓它知道該怎麼處理。
cat << 'EOF' > /etc/mime.types
# 常見 MIME 類型對照表
text/plain txt
text/html html htm
text/css css
text/javascript js
image/jpeg jpeg jpg
image/png png
image/gif gif
image/webp webp
application/pdf pdf
application/zip zip
application/json json
application/xml xml
audio/mpeg mp3
video/mp4 mp4
EOF
#重啟uhttpd
/etc/init.d/uhttpd restart