tedshd's DevNote

tedshd's DevNote

Develop & Design Note by Ted

PHP - 使用 urlencode rawurlencode 的差異和使用 http_build_query

on 2021-01-14

最近剛好遇到個問題就順便筆記一下(但是遇到的問題和要寫的內文無關就是了 XD)

前言

基本上在 url query string 的 value 都要做 url encode

URL encode 會用到以下標準

RFC 1738

RFC 2396

RFC 3986

主要會使用 % 字符來針對需要 escape 的字元做編碼

ex: / -> %2F, + -> %2B

但是主要又有幾個問題是基於 HTTP GETPOSTapplication/x-www-form-urlencoded 的問題

基本上在使用 HTML form 表單使用時

採用的會是把 空格轉成 +

但這些都不算是大問題

因為基於 CGI 和程式語言的實作

會把 urldeode 回來

所以在程式語言接到 query string 時都是 urldecode 回來的值

問題是在於 browser 上發出到 server 的 URL(就是打開 server 的 access log 看到進來的 Path 拉)

就是該 URL 是不是有在 query string 上是經過 urlencode 的

一但是沒有 urlencode 的

server 接進來後可能就會有問題

給個最簡單的例子

我要搜尋 iphone7+

URL 就會是 https://www.example.com/?q=iphone7%2B

如果你的 URL 是 https://www.example.com/?q=iphone7+

程式語言接到的 query string 就會變成 iphone7

當然就不是所預期的結果

正常的情況下如果是走 HTML submit

這樣當然在 input 填入 iphone7+

在 submit 時 browser 會幫你把 url encode

但是如果你是個 a link

是絕對不能用 <a href="https://www.example.com/?q=iphone7+">iphone7+</a>

得用 <a href="https://www.example.com/?q=iphone7%2B">iphone7+</a>

所以要記住一個原則

要讓 server 收到的 URL 它的 query string 得是經過 url encode 的

所以有些地方就得注意了

  1. a link 的 url query string 得是 url encode 的

  2. AJAX 在處理 API 時, 如果是 GET, query string 是要經過 url encode 的

也許有人會疑惑為啥在寫 web 時 2 的情況在使用時都沒有處理 url encode 阿

因為 library 已經幫你處理掉了

如果自己手刻一個 XMLHttpRequest 或是用 Postman 之類的工具去試試一個 GET 的 API 然後不處理 url encode 的話

就會出現問題了

那有人會疑惑說 POST 不用處理嗎?

POST 還真的不用處理

因為 POST 資料是在 content 裡面的

不是帶在 URL 上面

講了一堆廢話

還有一個要注意的重點

就是關於 +%20 的糾葛

PHP 有兩個 function

urlencode

rawurlencode

兩個 function 的實作的 RFC 是不同

簡單的說就是如下

php > echo urlencode(' ');
+
php > echo rawurlencode(' ');
%20

兩種 encode 的結果會是不一樣的

所以要注意的是用哪種 encode 就要用哪種 decode

個人偏好

因為把 轉成 + 是很古老的用法

且在一些情況底下容易出錯(但這種錯誤大多是使用上造成的錯誤), 基本上堅守著 用哪種方式 encode 就用哪種方式 decode 的原則就不太會有問題

所以當有問題發生時請先確認兩端的 encode decode 方式是否一致

因為大多數都是這個問題

所以我比較偏好 轉成 %20

遇到的問題

扯了那麼多廢話

現在才要開始講踩到的一個雷就是

在使用 http_build_query 時沒注意到的部分…

如前面提到的其實在 url query string 接進來時會自動處理 urldecode 回來原來的樣子

然後有個需求就是要做一個 redirect

redirect 有個重要的要點就是要處理 query string 決定要留哪些 query string 或過濾掉不要的 query string 後在 redirect 到新的 URL

所以身為一個懶人工程師當然是會用到 http_build_query 這個好用的 function

首先當然是要把當前的 URL 取出來處理

所以很理所當然的就是用到 parse_url 這個好用的 function

然後再把取出來的 query 用 parse_str 轉成 array

在做一些處理後就丟到 http_build_query

這樣完美的新 query string 就出來了誒

太棒了

<?php
function url_update_query($url = '', $query_string = [])
  {
    $url_data = parse_url($url);
    $query_array = [];
    if (isset($url_data['query'])) {
      $query_data = $url_data['query'];
      parse_str($query_data, $query_array);
    }
    if (!empty($query_string)) {
      foreach ($query_string as $key => $value) {
        $query_array[$key] = $value;
      }
      $url_data['query'] = http_build_query($query_array, null, '&', PHP_QUERY_RFC3986);
    }

    $scheme   = isset($url_data['scheme']) ? $url_data['scheme'] . '://' : '';
    $host     = isset($url_data['host']) ? $url_data['host'] : '';
    $port     = isset($url_data['port']) ? ':' . $url_data['port'] : '';
    $user     = isset($url_data['user']) ? $url_data['user'] : '';
    $pass     = isset($url_data['pass']) ? ':' . $url_data['pass']  : '';
    $pass     = ($user || $pass) ? "$pass@" : '';
    $path     = isset($url_data['path']) ? $url_data['path'] : '';
    $query    = isset($url_data['query']) ? '?' . $url_data['query'] : '';
    $fragment = isset($url_data['fragment']) ? '#' . $url_data['fragment'] : '';

    return $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
  }

然後上線後

就開始收到 error 了…

一開始以為是處理的 function 有問題

這邊我的作法是把 $_SERVER['QUERY_STRING'] 接近來這個 function 處理

$_SERVER['QUERY_STRING'] 進來的 query string 是以 STRING 呈現的全部 query string

沒有處理 urldecode

但是 parse_str 會處理 url encode(這邊 PHP 會自動把 +%20 轉成空白)

然後 http_build_query 會再組成 query string 時做 url encode

所以這過程是沒有 url decode 和 encode 的問題

雖然使用情境不是上述的例子就是了(但也是一樣是 query string 相關的)

那為啥會寫一大串是因為我都照了上面的情況排查了一遍發現都不是上述的問題…

最後去檢查 access log 發現是有的過來的 log 根本就是沒有 url encode…

只是目前還沒找出來為何會有沒有 url encode 的來源

因為來源的連結全部都是有處理過的…

而且也不是 100% 的 log 都是沒處理的…

又因為不是重要的頁面所以當初 log 沒有加 User Agent(因為我們有做 log filter, 為了可以倒進去 elasticsearch 去用 kibana 看, 所以只有 filter 重要的值出來)

但是為了良好的解決問題

這邊就採用了另一個處理方式

解決方案

我們遇到的問題在於因為有部分的 query string 的值是使用 base64

在使用 base64 可能會產生 + 等需要進行百分號編碼處理的字元

但正常來說我們在使用時會有 url encode 處理 query string 然後是給使用者這樣的連結

但是就是遇到部分的情況造成在 request 過來時是沒有 url encode 的情況

就產生問題了…

所以就採用 base64url

base64url 是基於 RFC 4648 裡面有提到針對部分情況例如 URL 或是某些系統的檔案名稱無法有效處理需要做百分號編碼的情況額外會再做一次編碼解碼

主要的處理有

+ => -

/ => _

還有基於長度編碼

= 填滿長度

這樣就解決掉一些 request 進來時沒有被有效的 url encode 的問題

算是一個有趣的經歷

因為這在 QA 和 RD 測試時都沒有遇到

真的上線後才遇到…

還好不是百分之百發生的情況…

Refer - String based data encoding: Base64 vs Base64url

Refer - Base64的介绍以及Base64URL介绍

Refer - 百分號編碼

Read more

使用 IMAP & SMTP 來加強處理 Gmail

on 2021-01-12

Intro

Gmail 提供 IMAP & SMTP 的功能

內送郵件 (IMAP) 伺服器

  • imap.gmail.com
  • 需要安全資料傳輸層 (SSL):是
  • 通訊埠:993

外寄郵件 (SMTP) 伺服器

  • smtp.gmail.com
  • 需要安全資料傳輸層 (SSL):是
  • 需要傳輸層安全性 (TLS):是 (如果可用)
  • 需要驗證:是
  • 安全資料傳輸層 (SSL) 通訊埠:465
  • 傳輸層安全性 (TLS)/STARTTLS 通訊埠:587

Refer - 透過其他電子郵件平台查看 Gmail

簡單的說 IMAP(Internet Message Access Protocol) 可以存取該 mail 的信件

SMTP(Simple Mail Transfer Protocol) 是可以利用該 mail 發信

詳細說明可以參照 wiki

Refer - 網際網路資訊存取協定

Refer - 簡單郵件傳輸協定

另外還有 POP3(Post Office Protocol) 這個和 IMAP 類似的協定

Refer - 郵局協定

主要 POP3 和 IMAP 的差異主要差在 POP3 是把信件內容拉下來, IMAP 不會, IMAP 是即時連線存取, 但是 IMAP 的好處就是即時連線同步, 這樣在多個裝置就可以即時的更新信件的狀態

所以在一些常見的客戶端程式例如 outlook 或是 Mac OS 裡面的郵件都是利用這些協定在處理 email

這邊當然也可以利用支援這些協定的程式語言寫一些功能來處理 email

使用情境

因為公司的服務每天會有大量的 feedback mail 寄到設定的 Gmail 信箱

雖然會有實習生協助整理和處理 feedback mail 但是還是很花人力

這年頭最貴的就是人力了

所以公司有個基於 Python 的程式在協助處理 feedback mail

那程式主要做的事情就是簡單的辨識 SPAM 與替 feedback 的內容打上 tag 以便於之後快速處理使用者的 feedback

How to do?

  1. 先開啟 Gmail 裡面的 IMAP 的功能

  1. 設定用於第三方應用程式驗證的密碼(並非 Google OAuth)

  1. 開始寫程式

怎麼開始寫 code?

這邊以 Python 為例

在開始前得先搞懂流程要幹什麼

  1. 連接 IMAP server
  2. 撈信件
  3. 讀取信件主旨(subject) 和信件內容(content) 和確認附件(attachement)

smaple code

以下使用 Python

建立 IMAP server 連去信箱讀取信件

from datetime import datetime, timedelta, date

import imaplib
import email

from email.header import decode_header
# pip install imap-tools
from imap_tools.imap_utf7 import encode, decode

ACCOUNT = '@gmail.com'
PWD = ''

mail = imaplib.IMAP4_SSL('imap.gmail.com')
mail.login(ACCOUNT, PWD)
status, data = mail.list()
mail.select('inbox')
# 取前一天的時間
today = (date.today() - timedelta(1)).strftime("%d-%b-%Y")
# 找出前一天未讀的信件
typ, data = mail.search(None, '(UNSEEN)', '(SENTSINCE {0})'.format(today))

# 取到 message id list(message id 就是每封信的 id)
ids = data[0]
id_list = ids.split()

for i in id_list:
    typ, data = mail.fetch(i, '(RFC822)')
    if typ == 'OK':
        print('get mail ok')

    for response_part in data:
            if isinstance(response_part, tuple):
                rawMail = response_part[1].decode()
                # 取得信件
                msg = email.message_from_string(rawMail)

基本上到這裡拿到 msg 就是這封信件的資訊了

就可以取出來做一些處理

例如: 判斷來信者來回信, 判斷內容替信件添加標籤 等等

關於 imap.search 可以如何使用可參考

Refer - Python’s imaplib (Example)

回信

Read more