在 Ubuntu Server 上自動啟用 SSH Agent

當 我們的 SSH private key 有上 pass phrase 保護時, SSH agent 是個方便的好東西。因為它可以幫我們記住已經解鎖過的 private key。

可惜的是,Ubuntu server 18.04 的環境預設並不會幫你生一個 SSH agent 出來。

本文章記錄一點摸索的過程…

系統自帶的 SSH agent systemd unit

我看別人的 Ubuntu 登入之後就有 SSH agent 可以用啊?

很可惜的是我的環境沒有。研究一陣子之後,發現 SSH agent 應是在有圖形介面 的情況下才會被自動帶起。

dpkg --listfiles openssh-client 下可看到幾個重要的檔案

  • /usr/lib/openssh/launch-agent
  • /usr/lib/systemd/user/ssh-agent.service
  • /usr/lib/systemd/user/graphical-session-pre.target.wants/ssh-agent.service

看了這幾個檔案的內容後可得知

  1. 這是設計給圖形介面的登入 session 使用的 service
  2. 即使想要直接 enable ssh-agent.service 也無法,因為裡面沒有寫任何的 [Install] 參數

自行撰寫並啟用一個 SSH agent 服務

為了解決沒有 SSH agent 的問題,我們可以自己寫一個 systemd 的 user service, 讓系統在發現我登入之後,自動幫我把 SSH agent 拉起來。

首先編輯 ~/.local/share/systemd/user/ssh-agent.service (參考 man systemd.unit 此為預設的 user unit 路徑)

1
2
3
4
5
6
7
8
9
[Unit]
Description=SSH authentication agent

[Service]
ExecStart=/usr/bin/ssh-agent -a %t/ssh-agent.socket -D
Type=simple

[Install]
WantedBy=default.target

注意 ssh-agent-D 參數與 Type=simple 設定。

接著執行 systemctl --user enable ssh-agent.service。 這一步會在 .config/systemd/user/default.target.wants 資料夾下創出一個 symbolic link, 連回剛剛我們寫的 service file,表示要在登入時自動啟用此 unit。

接著重新登入該機器,應該就可以看到一個 ssh-agent process 跑起來了。

設定 SSH agent 所需的的環境變數

雖然 SSH agent 起來了,但此時若下 ssh-add -L 依然會發現無法連上 SSH agent。

Could not open a connection to your authentication agent.

這是因為 ssh 以及 ssh-add 等工具預設都是看 SSH_AUTH_SOCK 環境變數來得知 要透過哪個 Unix socket 與 agent 溝通。

為了處理此問題,我們需在 ~/.profile 內加入一行環境變數設定,確保在登入時能自動設定完成。

1
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"

註: $XDG_RUNTIME_DIR/ssh-agent.socket 與前述 unit file 內的 -a %t/ssh-agent.socket 對應。詳細可參考 man systemd.unit

下次登入重新讀取 profile 之後即可正常使用 SSH agent 囉。 :D

Alternative Solution

尋找解決方式的過程中,注意到了一些解法,透過純 shell script 的方式處理重複登入的問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SSH_ENV="$HOME/.ssh/environment"

function start_agent {
/usr/bin/ssh-agent | sed 's/^echo/#echo/' > "${SSH_ENV}"
chmod 600 "${SSH_ENV}"
. "${SSH_ENV}" > /dev/null
/usr/bin/ssh-add;
}

if [ -f "${SSH_ENV}" ]; then
. "${SSH_ENV}" > /dev/null
ps -ef | grep ${SSH_AGENT_PID} | grep ssh-agent$ > /dev/null || {
start_agent;
}
else
start_agent;
fi

若不考慮 race condition,該作法其實也很值得參考。可以在沒有 systemd 輔助的的生態系底下使用。

雜談

看 systemd 的文件時,發現 systemd 的 user mode 會非常遵守 XDG_ 系列的環境變數。 不過因為我們是在 Ubuntu server edition 下,所以大部分都略過不看。 :D

XDG_RUNTIME_DIR 這個變數除外,此變數雖然也是由 XDG Base Directory Specification 所規範,但在一般 Linux 發行版,此變數是由 pam_systemd 直接維護的。所以即使是在 server 環境也會有此變數存在。

References

在 macOS 上架設 Apache 與 PHP-FPM

工作上因為一些特殊需求,需要在 macOS 環境下架設 Apache + PHP-FPM 的使用環境。好在 macOS 本來就有預裝 Apache 以及 PHP-FPM,並提供 Apache 的 launchd 設定檔,要在 macOS 上架設這個服務並不困難。

本文介紹如何以最低限度的設定,在 macOS 上跑 Apache + PHP-FPM。以筆記的方式呈現,不會有太多的講解。

Notes

  • 筆者是在 macOS 10.14 與 10.15 上測試此流程
  • macOS 系統上,/etc 是一個 symblic link 連至 /private/etc/var, /tmp 也有相同行為。

設定與啟用 PHP-FPM

複製並修改 PHP-FPM 設定檔

系統內有會自帶 PHP-FPM 的 default 設定檔,將其複製一份出來,並修改內容。

1
2
$ sudo cp /etc/php-fpm.conf.default /etc/php-fpm.conf
$ sudo cp /etc/php-fpm.d/www.conf.default /etc/php-fpm.d/www.conf

將執行身分從 nobody 修改為 _www (與 Apache httpd一致)。

1
$ sudo vim /etc/php-fpm.d/www.conf
1
2
3
4
5
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
; will be used.
user = _www
group = _www

修改 error_log,調整 log file 的路徑。

1
$ sudo vim /etc/php-fpm.conf
1
2
3
4
5
6
; Error log file
; If it's set to "syslog", log is sent to syslogd instead of being written
; into a local file.
; Note: the default prefix is /usr/var
; Default Value: log/php-fpm.log
error_log = /var/log/php-fpm.log

新增 PHP-FPM 的 launchd 設定檔並啟用

創一個 launchd daemon 設定檔給 PHP-FPM 使用, 此舉目的為讓 PHP-FPM daemon 可以在 macOS 開機時自己啟用。

建議將設定檔放在 /Library/LaunchDaemons 下,參照 launchd 的文件, 此位置是供第三方軟體擺放 daemon 設定使用。

1
$ sudo vim /Library/LaunchDaemons/com.example.php-fpm.plist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Disabled</key>
<true/>
<key>Label</key>
<string>com.example.php-fpm</string>
<key>ProgramArguments</key>
<array>
<string>/usr/sbin/php-fpm</string>
<string>--nodaemonize</string>
</array>
<key>OnDemand</key>
<false/>
</dict>
</plist>

做好設定檔之後,用 launchctl 指令 load 此設定檔,並下參數告訴 macOS 之後此 daemon 要在開機時預設啟用。

1
$ sudo launchctl load -w /Library/LaunchDaemons/com.example.php-fpm.plist

上述指令執行完後, launchd 會把 PHP-FPM daemon 叫起。

1
2
3
4
$ ps aux | grep php
_www 515 0.0 0.0 4297608 648 ?? S 6:18PM 0:00.00 /usr/sbin/php-fpm
_www 514 0.0 0.0 4305800 628 ?? S 6:18PM 0:00.00 /usr/sbin/php-fpm
root 513 0.0 0.0 4305800 784 ?? Ss 6:18PM 0:00.00 /usr/sbin/php-fpm

設定並啟用 Apache Web Server

修改設定檔,讓 Apache 使用 proxy_moduleproxy_fcgi_module, 並確認 php7_module 沒被啟用。

需要本文的讀者應該不至於把 Apache PHP module 與 PHP-FPM 搞混.. XD

1
$ sudo vim /etc/apache2/httpd.conf
1
2
3
LoadModule proxy_module libexec/apache2/mod_proxy.so
LoadModule proxy_fcgi_module libexec/apache2/mod_proxy_fcgi.so
# LoadModule php7_module libexec/apache2/libphp7.so

<Directory "/Library/WebServer/Documents"> 或其他需要的地方內, 加入 PHP 的 handler,指向 PHP-FPM 預設提供服務的 socket。

1
2
3
4
5
6
7
<Directory "/Library/WebServer/Documents">
... 上略
<FilesMatch \.php$>
SetHandler "proxy:fcgi://localhost:9000/"
</FilesMatch>
</Directory>

Apache 的 daemon config 本來就存在於系統目錄內,但 Disable 的值被設為 true, 用下述 command 將 Apache daemon load 進 launchd 內,並讓 launchd 記錄此 daemon 應被啟用。

1
sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist

與 PHP-FPM 相同,此指令下去之後,launchd 就會把 Apache 拉起。

此時可去 http://localhost/ 確認,如果看到大大的標題寫著

It works!

即代表 Apache 有順利執行。

確認 PHP-FPM 運作正常

丟一個 phpinfo 到 web server 的預設根目錄下。

1
$ sudo vim /Library/WebServer/Documents/phpinfo.php
1
2
3
<?php
phpinfo();
?>

之後連上 http://localhost/phpinfo.php ,看到 Server APIFPM/FastCGI 即可。 :D

關於電子報一鍵退訂

前言

前先日子在 Gmail 內整理信件時,赫然注意到了一件事情:

某些訂閱來的電子報,在寄件者名字的右邊有個退訂的文字可以按! 但讓我訝異的其實不只是這個退訂文字.. 按下退訂文字後,Gmail 會彈出一個視窗給我,裡頭會問我是不是確定要退訂此電子報,以及一個藍色的退訂按鈕。 神奇的是,當我按下這個按鈕後,Gmail 直接告訴我,你已經退訂了此電子報! “You unsubscribed from xxxxxx..”

某人: 奇怪… 一般不是會連到某個電子報寄送方的頁面,然後讓我點個確認之類的嗎?

既然 Gmail 有辦法在不離開 web mail 介面的情況下,幫我完成退訂的動作,那大概是有某種標準程序,可以讓 mail 供應商自動化的幫我處理吧? 於是乎,本著實事求是的精神,就有了今天這篇文章。

工程師的直覺

注意到有這個退訂按鈕可以按之後,我就開始一封信一封信看,有退訂按鈕的都來給他點看看,看會發生什麼事。但我很快地就注意到… 並不是每個有按鈕可按的 Gmail,都可以直接幫我完成退訂。 像是 Linkin 和 Google Map 寄來的信,都是會幫你打開某連結,讓你到該頁面去做後續處理。

但不管如何,既然 Gmail 可以幫我把這個連結取出來,大概是有某種標準的 mail header 來記錄這些資訊吧?把 Linkin 那封原始信件點開來一看,發現了這個東西

1
List-Unsubscribe: <https://www.linkedin.com/e/v2?e=6f948d7f&t=ad69-4827&...>

再把這個 List-Unsubscribe 拿去查,就找到一篇 RFC 了

1
2
3
4
5
6
7
8
9
Network Working Group                                      G. Neufeld
Request for Comments: 2369 Nisto
Category: Standards Track J. Baer
SkyWeyr Technologies
July 1998


The Use of URLs as Meta-Syntax for Core Mail List Commands
and their Transport through Message Header Fields

RFC 2369 “The Use of URLs as Meta-Syntax for Core Mail List Commands”。仔細一讀後發現,以前的人為了用 mail 標準化 mail list 的各種處理動作,還下了不少苦心。 XD 光是這份 RFC 內有提到的部分 header 就有:

  • List-Help: 所有和此 mail list 相關的資訊都從這邊取得
  • List-Unsubscribe: 使用者快速退訂的方式
  • List-Subscribe: 使用者快速訂閱的方式
  • List-Post: 使用者發表文章至此 mail list 的方式

其中的 List-Unsubscribe 就是我們要的東西。參照 RFC 說明,這個 header 內可以放 HTTP 的連結或 mailto 的連結。這樣看起來,如果是 HTTP 連結,Gmail 就會幫我們連上該頁面。而那些沒有額外跳出頁面的,大概就是 Gmail 直接幫我們寄退訂信件了吧。

1
2
List-Unsubscribe: <http://www.host.com/list.cgi?cmd=unsub&lst=list>,
<mailto:list-request@host.com?subject=unsubscribe>

開始驗證

為了確認上面的猜想,我又點了封有退訂按鈕且不會跳出額外頁面的信件。這次的實驗對象是 The Hacker News。在點擊退訂之後,在我的寄件信箱內找到 Gmail 自動幫我產生的信件,Gmail 不只有幫我寄這封信,連信件主旨和內文都幫我填好好的。 XD

1
2
3
4
5
6
# 按下退訂後,我寄出的信 (省略部分 header)
To: f081fcde-0c7e-4617-afaf-c0c35eeea170@unsubscribe.netline.com
Subject: Unsubscribe
Content-Disposition: inline

$You will be unsubscribed from this list within ten days of sending this reply

在回去看一下原本納封電子報的原始信件,果然在 header 內找到這對應的 List-Unsubscribe mailto 連結,且信件主旨和信件內文和這個連結後方帶的資訊完全吻合。

1
2
# The Hacker News 信件的 List-Unsubscribe header
List-Unsubscribe: <mailto:f081fcde-0c7e-4617-afaf-c0c35eeea170@unsubscribe.netline.com?subject=Unsubscribe&body=$You%20will%20be%20unsubscribed%20from%20this%20list%20within%20ten%20days%20of%20sending%20this%20reply>

看到這裡,似乎是真相大白了。參與制定 RFC 的人們真偉大!趴機趴機趴機!

用 HTTP(S) 連結做退訂的會開一個 HTTP Get 請求,讓使用者到某頁面按退訂;而那些用 mailto 連結的,mail agent 可以幫我自動寄出退訂信件,於是達成一鍵退訂的功能!

不過… 我剛剛好像也一鍵退訂了 Pinkoi 的電子報,但好像沒有看到自動寄出的信件?

案外案: RFC 8058

原本以為該懂得都懂了,一切就是那麼的單純,都在我的掌握之中。 直到我注意到 Pinkoi 的電子報 (X

在實驗的過程中,Pinkoi 也像 The Hacker News 一樣,可以在 mail 介面中直接完成退訂。但不一樣的是,mail 系統沒有自動幫我產生並寄出退訂用的信件。 …看來這當中一定還有些我不知道的東西!

有了先前的經驗,這次很快地把 Pinkoi 電子報的原始信件打開來看,並直接搜尋 unsubscribe 字眼。一搜不得了,看到了一個沒在 RFC 2369 中出現的 header: List-Unsubscribe-Post 。拿這個 header 去找 RFC,結果找到了這個東西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Internet Engineering Task Force (IETF)                         J. Levine
Request for Comments: 8058 Taughannock Networks
Category: Standards Track T. Herkula
ISSN: 2070-1721 optivo GmbH
January 2017


Signaling One-Click Functionality for List Email Headers

Abstract

This document describes a method for signaling a one-click function
for the List-Unsubscribe email header field. The need for this
...

“Signaling One-Click Functionality for List Email Headers”,嗯.. 看來這就是我要的東西了…

在經過快速地閱讀之後,這個 RFC 的部分動機大概是這樣的

防毒軟體一般會掃過所有在信件內的 HTTP(S) 連結。電子報供應商為了避免防毒軟體不小心幫使用者退訂,通常會把連結做成需要使用者互動的網頁,像是在頁面中放個額外的確認按鈕等。但此作法又會造成信件軟體或信件服務商,無法在取得使用者的同意後,自動化的幫使用者退訂電子報。因此,需要訂出一個標準的方法,讓 HTTPS 的退訂連結也可以達成一鍵退訂。

這動機看起來是很清楚了.. (其實還有部分有關垃圾信的處理問題,這邊就不翻譯了)。不過究竟該怎麼做一鍵退訂呢?

根據 RFC 8058 的描述,信件若要用 HTTPS 連結做一鍵退訂,至少需要滿足以下幾點:

  1. List-Unsubscribe header 內至少有一 HTTPS 連結
  2. 需要額外有 List-Unsubscribe-Post header,且其值必須為 List-Unsubscribe=One-Click
  3. 必須有 DKIM 簽章來驗證上述兩個欄位

第一點是挺合理的,這個 RFC 是 2017 年出來的,大概不會有人還想推 HTTP 連結了。而第二點的 List-Unsubscribe-Post header,是要告訴 mail 軟體說 “我這個連結可以吃 HTTP POST 請求喔!喔對了記得 POST 過來時內容要帶 List-Unsubscribe=One-Click 喔”。至於第三點,單純是要確保上述兩個 header 是沒有被竄改過的。

因為有講好 client 應該用 POST 方法去戳這個連結,於是電子報的 server 就可以很清楚的分辨,哪些請求是防毒軟體不小心誤發的,哪些請求是使用者真的想退訂才發的。且因為有清楚的表達意圖,這個 HTTPS POST 請求也不需要回一個要使用者額外互動的頁面,server 可以在收到請求後,直接處理使用者的退訂動作。

1
2
3
4
5
6
7
8
9
10
11
# 假設信件內有以下 header
List-Unsubscribe: <https://example.com/unsubscribe/opaquepart>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

# 那 mail 軟體(提供商) 可以簡單透過以下 HTTPS POST 幫使用者退訂
POST /unsubscribe/opaquepart HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 26

List-Unsubscribe=One-Click

可喜可賀.. 可喜可賀 XDD

其他軟體的支援性和 Google 的工人智慧

發現有這個標準後,其實很好奇其他的 web mail 或信件軟體對這些 header 的支援度如何。無奈的是,沒找到比較完整的整理結果,似乎也沒多少人在意這個東西。 XD 不過從唯一一份找到的資料來看,在 iOS Mail, Gmail, Outlook 與 Yahoo Mail 四者中,mailto 的退訂連結都有支援,而 RFC 8058 所定義的一鍵退訂則是只有 Gmail 可以做到。(至少在 2018 年 11 月還是如此)

另外,我也發現到,即使有些信件完全沒有 List-Unsubscribe header,Gmail 仍然可以生出退訂的按鈕給使用者按,Twitter 的通知信件即是一例。Twitter 的信件 header 內沒有相關的資訊,但 Gmail 可以從信件內文內把退訂的連結 parse 出來。至於這部分是 Google 偉大的工人智慧,還是有一些我還不知道的標準可參考,這我就還沒研究到了。

結論

這篇文章基本上是把某個週末因為三分鐘熱度而去學的東西記錄下來。但過了兩週之後再回來看,其實好像也不是多重要的東西。就算今天我們不知道有這個標準,或是根本沒注意到有這個功能,日子也還是過得很好 (?

但是.. 小工程師滿足自己的好奇心後,心中所獲得的那種成就感,是無可取代的!

利用 GitHub Page 經營 Blog

如果要用一句話來簡單說明 GitHub Page,那基本上就是

指定一個 Git 版本庫來作為存放網站資源的地方,然後讓 GitHub 幫你把網站架起來。

任何人只要申請一個 GitHub 帳號,都可以免費的享有這個服務。

當然,考量到 GitHub 只是把我們放在版本庫上的檔案,讓別人透過瀏覽器瀏覽, 那種需要用到資料庫的可互動網站基本上是很難達成。 但若我們只是要經營一個部落格,或是存放專案文件等靜態網站時,GitHub Page 就會是個很合適且方便的選擇。

本文會粗淺的介紹如何利用 GitHub Page 來經營自己的 Blog, 以省去自行架設機器的各種煩惱。 :D

GitHub Page 的類別

目前 GitHub Page 有兩類的站台,一類是 User Page、另一類是 Project Page。 (其實還有 Organization Page,但這邊就不花時間贅述) User Page 與 Project Page 最主要的差別在於專案名稱的限制,與網站的 URL 格式這兩點。簡單整理如下

User Page 特點:

  • 專案名稱須為 <username>.github.io,其中 <username> 即為 GitHub 帳號的使用者名稱。
  • 站台會擺在 http(s)://<username>.github.io 供他人瀏覽

Project Page 特點:

  • 專案名稱沒有限制。 若假設專案名稱為 <projectname>
  • 站台會擺在 http(s)://<username>.github.io/<projectname> 供他人瀏覽

因為專案名稱的限制,一個 GitHub 帳號只能有一個 User Page 但可以有多個 Project Page。

更詳細的介紹請參考 官方網站的說明

GitHub Page 使用方式

GitHub Page 的使用方式也可以簡單分成兩種。

第一種是直接建置好的整個網站直接 push 到 GitHub 上,供使用者瀏覽。 若我們需要架一個 Blog,可以先用 Markdown 等 markup language 撰寫文章, 之後利用 Jekyll(Ruby)、Hugo(Golang) 或 Hexo(JS) 等靜態網站生成工具, 建出一個 Blog 網站並 push 上去。 又或我們需要 host 一個專案文件站台時,可以將 Doxygen 或 Sphinx 等工具 產生出的網站推上 GitHub。

1
2
3
4
5
6
7
8
                                    +
Local Project | GitHub Project
| github.io site
+
+----------+ +--------+ +------+ User
| Markup | Build | Site | Push | Site | Browse
| config.. | +------> | | +-----> | | +------->
+----------+ +--------+ +------+

如果我們是使用 Jekyll 來建置我們的網站,那 GitHub Page 有提供我們第二種用法。 我們可以將 Markup 和其他 Jekyll 需要的設定檔 push 上 GitHub,讓 GitHub 幫我們 建置網站,並在 github.io 網域上放出網站供人瀏覽。

1
2
3
4
5
6
7
8
               +                    +
Local Project | GitHub Project | github.io site
| |
+ +
+----------+ +----------+ +------+ User
| Markup | Push | Markup | Build | Site | Browse
| config.. | +----> | Config.. | +-----> | | +------->
+----------+ +----------+ +------+

第二個做法的缺點是,GitHub 只支援 Jekyll 這套工具,其他同性質的工具的不支援。 但相對來說也有優點,即是我們不須把工具建出的網站內的所有檔案都進到 commit 中。 (在 git project 中看到許多無意義 diff 實在不是工程師所樂見的事情 XD)

針對這兩個方式的更詳細說明,也請見官方文件

簡單的流程說明

接下來會介紹使用 Jekyll,並讓 GitHub 幫忙 build 與 host 網站的簡單步驟。

參照 官方介紹 的說明,最簡單的方式,其實只需要 我們點開 project 的 GitHub 設定頁面,找到 GitHub Page 的設定選項,設定一個 Jekyll 使用的主題,並用 Markdown 寫一個首頁文章即可。

用此方法會在專案內產生首頁的 index.md 檔案及一個 Jekyll 的設定檔 _config.yml。 檔案內僅一行你選的主題名稱

1
theme: jekyll-theme-minimal

但基本上,一個 完整的 Jekyll 專案 不會只有這兩個檔案,到最後我們還是得把其他需要的檔案生出來。 所以個人推薦使用下述方法建立我們的專案。

(假設我們已經裝好 Git 和 Jekyll 等工具。)

建立 Git 專案

1
2
mkdir website && cd website
git init

在專案資料夾建立 Jekyll 的 template 檔案

1
jekyll new .

此時應該會看到 jekyll 預設產生的檔案

1
2
$ ls
404.html about.md _config.yml Gemfile Gemfile.lock index.md _posts

將所有產生的檔案 add 並 commit 起來 (要不要略過 Gemfile.lock 看個人需求)

1
2
git add .
git commit

之後將專案 push 上 GitHub,並至專案設定內啟用 GitHub Page 即可。 沒意外的話,大概十秒內就可以在對應的 URL 看到生成好的網站了。

有關 Jekyll 的安裝說明或其他細部設定,可參考 官方網站

在 GitHub Page 服務上使用個人客製的網址

如果不想使用 <username>.github.io 來提供自己的網站,而是透過自己購買的域名, 所需的麻煩差事 GitHub Page 也幫我們做得好了。

在 GitHub 專案開啟 GitHub Page 功能後,可以看到一個額外的選項 Custom domain, 可以填入我們可控制的 DNS hostname。

假設我們想在 blog.example.com 提供我們的網站,只需要在 DNS 設定中加入一筆 CNAME,將 blog.example.com 指向 <username>.github.io。並去 GitHub Page 所用的 GitHub 專案設定頁面內,在 Custom domain 欄位內填入 blog.example.com 即可。

設定完後,即可透過 blog.example.com 瀏覽我們要的網站。 同時 GitHub 也會在一天內生出對應的 SSL 憑證,即使透過 blog.example.com 瀏覽, 也可以享有 HTTPS protocol 帶來的安全性。 :D

雜談

大概從 2018 十月開始,小弟我在與朋友以及公司同事談話後,漸漸有了經營自己 Blog 的想法。

經歷了數週的拖拉散漫後,終於在 2018 十二月底刷卡買了自己的 domain,並利用 GitHub Page 架設好 Blog。但因為一直沒想好要寫什麼文章,於是第一篇就先來寫寫 我自己的架站筆記。

期許自己未來能不斷產出新文章,成為一位散發正面能量的一倍工程師。

Samba 內檔案的異常執行權限

許多場合會利用 Samba 來建立給所有工作者共用的 SMB 工作目錄。

SMB 是 Microsoft 針對 Windows 體系設計的 protocol,但因為種種因素, 目前 Mac 及各 Linux 桌面發行版也都有不錯的 SMB client 支援。 已 SMB 提供共用的工作目錄,似乎已成為最常見的協同工作方法。

在另一方面,Samba 是一套基於 Unix 環境開發的開源 SMB server 實作。 但因為 Windows 與 Unix 在檔案系統上設計的根本性差別,儘管 Samba 歷史悠久且功能齊全, 仍然會有一些先天性的問題。

為了解釋問題到底從何而來,以下要簡單介紹一下 Windows 與 Unix 的檔案相關特性。

Windows File Attribute 與 Unix File Mode

傳統上,Windows 系統下的每個檔案都會需要有以下四個屬性:

  • Archive: 紀錄此檔案在上次備份後是否更動過。
  • Hidden: 紀錄此檔案是否要隱藏。Windows 內建 dir 或檔案總管均會遵從此設定。
  • System: 紀錄此檔案是否爲系統檔案。
  • Read-only: 紀錄此檔案是否只能讀取。

Unix 作業系統採取了與 Windows 不同的設計。 在 Unix 內,每個檔案紀錄針對檔案擁有者、檔案擁有群組及其他人分別紀錄三組權限

  • Readable: 檔案是否可讀取
  • Writable: 檔案是否可寫入
  • Executable: 檔案是否可執行

現在我們可以知道,Windows 與 Unix 對於檔案應該紀錄的特性/模式其實有不一樣的要求。 許多和檔案系統相關的功能也會用不同的方式來達成。

比方說,有關檔案 是否隱藏 這件事情,在 Windows 上會有一個獨立的 attribute 來處理。 而在 Unix 上,則是依據 “. 開頭的檔案應被隱藏” 的常規。除了檔名看起來有點不一樣之外, 隱藏檔案和一般檔案在檔案系統裡沒有差別。

另外我們也可以注意到Windows 和 Unix 對於 可執行 這個概念的處理方式也不同。 在 Unix 的世界中,每個檔案的 mode 中會紀錄這個檔案是否可執行。而在 Windows 中, 檔案並沒有是否可執行的概念,而是讓作業系統維護一個表單,裡面紀錄各種副檔名的檔案應該如何開啟或執行。

Samba 的設計與產生的問題

Samba 是個 SMB 的 server 實作,作為在 Unix 環境上跑的服務,但同時也要支援 Windows 的 file attribute,亦即前述提到的 Archive、Read-only 等。這些 attribute 是必得以某種 方式存在 Samba server 上。在這裏 Samba 的實作非常有趣:

利用 Unix 環境中 Windows 用不到的 executable bit 來存放 Windows 需要的 file attribute。

具體來說,Archive、System 與 Hidden 屬性會分別存在擁有者、擁有群組與其他人的 file mode 的 executable bit 當中。而 Read-only 則是影響檔案擁有者的寫入權限是開啟。 簡單圖示如下:

1
2
3
4
5
6
    Owner    Group    Others
+----------------------------+
| r w x r w x r w x |
+-^--^--^--------^--------^--+
| | | | |
ReadOnly Archive System Hidden

但此做法舉其實會導致其他問題。這會其他在 Mac 或 Unix 環境下掛載 SMB share 的使用者, 會看到檔案有時莫名的變成可執行,或原本可執行的 script 突然變成不可執行。

於是乎,在 Window 與 Linux 混用的工作環境中,RD 的 terminal 下常會看到一片花花綠綠的 source code 資料夾。(一般 shell 都會預設開 ls 的 colorize 選項)

Executable Bit 異常的解決方式

若要避免 Samba 的這類行為,可以在 smb.conf 中加入以下設定

1
2
3
4
map archive = no
map system = no
map hidden = no
map read only = no

如此 Samba 就不會嘗試利用 Unix 的 file mode 來存放這些 attribute。

又或是如果想支援 Windows 的 attribute,但又不想影響 Unix 下的執行權限,可以將這些 attribute 寫進 extended attributes 裡。這需要使用以下設定

1
store dos attributes = yes

碎碎唸

作為一個軟體工程師,常聽到 Every detail matters 或其他類似精神的標語。 魔鬼蔵在細節裏,確保每個細節的正確(或至少看起來正確)是每個工程師都應該追求的境界。 而這個追求細節的精神,必須從乾淨的工作環境開始做起。

為什麼會寫出這邊文章?其實就只看到公司的 VCS 內各種怪異的檔案權限, 感到困惑而已。清楚的變數命名、符合直覺的 API 設計有助於開發人員理解專案。正確的檔案權限 其實也是如此。設定檔應該是 rw-,script 應該是 r-x,不違背直覺的專案狀態才不會 阻礙工程師工作。

當然,一切的根本原因還是在於 Samba 的預設設定會把 Archive attribute map 到 file mode 裡面。在 Window 環境工作的工程師一般都是在無意的情況下把 file mode 的異動 寫道 VCS 裡面。這時只能抱怨爲何當年 Samba 的作者要做出這種設計了。

相關連結