OpenWrt + uHTTPd +openSSH架設網站&個人雲端

邊界條件

固定IP
一台12年前的x86電腦
OpenWrt略知一二

設定固定IP

從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'

🛠 安裝 uHTTPd

opkg update
opkg install uhttpd uhttpd-mod-ubus

🔧 設定 uHTTPd 網站根目錄與 port

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/

🌐追加提供上傳下載(個人雲端)

🔧設定openSSH

https://deltawen2.github.io/just_Learning_notes/openwrt_OpenSSH.html

增加互動式上傳界面

建制後端驗證處理程序

整個方案將由兩個檔案組成

  1. 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
  1. 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

🧾 修改 uhttpd 設定

#編輯文件
vi /etc/config/uhttpd
#加在config uhttpd 'main'最後一行
option list_directories '1'

#在最下面加一組
config alias
    option alias '/image'
    option target '/www/image'

🔁 重新啟動 uhttpd

/etc/init.d/uhttpd restart

/etc/config/uhttpd 完整內容

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