一個基於Java的開源URL嗅探器

2016-07-11    分類:JAVA開發、程式設計開發、首頁精華0人評論發表於2016-07-11

本文由碼農網 – Dee1024原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

這是一個可以檢測並規範化文字中的URL地址的Java庫。

今天,我們很高興做一個分享,因為我所在的 Linkedin 公司 開源了我們做的一個ULR探測工具:URL-Detector  Java 庫。

Linkedin 在每一秒鐘,會檢查數十萬數量級的 URLs 。這些 URL 可能是來自惡意軟體或者釣魚網站的,為了保障我們每一個使用者有一個安全的瀏覽體驗,同時防止潛在的危險,我們後端的內容檢查服務程式會檢查所有由使用者產生的內容。為了在這每秒數十萬規模的使用者內容上檢測不良的 URL,我們要有能夠在快速此規模上提取文字中URL 的方法。

我們的伺服器中的 URL地址有兩種形式:

  • 一種是單一的 URL
  • 一種是在一大塊的文字內容中

如果傳送過來的是單一的 URL,我們可以通過我們的內容檢查服務直接驗證;

如果傳送過來的是大塊的文字內容,我們會先通過我們的URL探測器 ,經過搜尋演算法來驗證這個文字是否有潛在危險的URL地址;

在我介紹URL探測器是如何工作的和它所能提供給的功能之前,讓我們先來了解一下我們做這個專案的動機。

我們的目標是:檢測出儘可能多的惡意連結,但是我們不希望緊緊侷限於檢測在 RFC 1738 中定義的URL地址,而是希望可以檢測出任何能夠在真正的瀏覽器位址列中輸入並且可以訪問到的URL地址。因為,一個瀏覽器的位址列中對 URL 的定義比起 在 RFC 1738 定義的來說,是非常鬆散的。同時,很多瀏覽器有不同的行為,所以,我們要找到一種URL文字規則能夠被大部分流行的瀏覽器解析,它不是像RFC中定義語法那樣簡單。

最初,我們開始第一種解決方案,基於正規表示式。它可以幫我們檢測到許多潛在的網址,其中有不少卻是真的有潛在危險的,但是其中也有不少是沒有的,而且有許多有危險的地址被遺漏了。用這種方式,為了抓取更多的地址這是一個反覆匹配的過程,這可能出現一些不狀況,比如,一個簡單URL匹配的正則:

Regex:
(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?

然後,如果你想檢測到不包含 scheme 的 URL,對應修改正則如下,這是其中一個的例子說明瀏覽器的位址列可以解析的地址,但是卻不符合 RFC 規範。

Regex:
((ftp|http|https):\/\/)?(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?

經過各種瀏覽器和多場景的相容,我們終於得到最後的正則:

Regex:
((((f|ht)tps?:)?//)?([a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]+(:[^ @:]+)?@)?((([a-zA-Z0-9\\-]{1,255}|xn--
[a-zA-Z0-9\\-]+)\\.)+(xn--[a-zA-Z0-9\\-]+|[a-zA-Z]{2,6}|\\d{1,3})|localhost|(%[0-9a-fA-F]{2})+|[0-9]+)
(:[0-9]{1,5})?([/\\?][^ \\s/]*)*)

正如你所看到的,每一個相容或者一個新的場景帶來的小的邏輯分支,就對應至少增加幾個字元的正規表示式長度。你可以在 這裡 看到這種情況究竟有多複雜(注意:一些類似的 URL 匹配的的正規表示式 長度竟然超過了5500字元)。我們發現許多通過這種方式檢測到的 URL 雖然是科學的 ,但是卻也給我們帶來了困擾。因為,我們越是頻繁的變動正則,我們發現會有對應更多的誤報 URL 。我們要是不優化這些正則,我們就發現我們遺漏的 URL 還很多。

因為我們發現太多的錯誤的匹配,我們採取了減少匹配數量的方法。編輯原始的複雜的正規表示式語句讓我們引入了更多錯誤。因此,我們需要多正規表示式。下面的例子是我們其中一個正規表示式,用來排除“localhost”和“由數字和點組成的IPv4地址”。

Blacklisted Regex: ^((\\d+(\\.\\d+){0,2})|(\\d+(\\.\\d+){4,})|localhost)$

這樣做的結果是,當解析大文字的時候,將耗費很長的時間,有些一次解析甚至是秒級別的。但是,我們的需要每秒處理數十萬數量級的的 URL,這麼耗時的這個方案明顯是不可行的。同時,我們還發現正規表示式有一個缺點,就是:匹配易,處理分析難、維護難。就這樣,我們的 URL探測器誕生了。

為了取代使用正規表示式,我們手工打造了一個有限狀態機來解析出在文字中的URL。 有限狀態機(你可以在這裡瞭解更多資訊)是由一組狀態組成,狀態之間可以由輸入事件來觸發狀態轉換。在這種請求下,輸入事件就是我們在文字中正在解析的字元。

這個有限狀態機有幾個狀態,主要是基於 URL 的各部分拆分的。狀態由一系列的布林變數保持,每一次一次消耗一個字元,同時進行一次狀態轉移。為了效能考慮,這個有限狀態機器被設計成這樣的邏輯拓撲順序,沒有箭頭會從後面的狀態指向前面的狀態。舉個例子,如果你在 host 後面檢測到 “/”,那麼狀態機就不會再跟蹤的 scheme ,因為它是點頭過去的一端。如果狀態機在任何一個位置撞到一個非預期的字元,它將返回上一次最新的結束狀態,同時重新開始這個演算法。

這個最棘手的部分是匹配字元。這些字元實際上,有可能在多個狀態中存在。舉個例子,比如冒號 “ : ”,它可以出現在至少三個地方:在 scheme 後面,在username 和 password 中間,或者是在 host 和 port 中間,並且,當我們處理IPv6的時候,它變的更加複雜,因為IPv6的地址也可以包括冒號的。但是,狀態回溯主要是發生在一些奇怪的情況下,比如一個文字包含一系列非空白字元其中又包含多個冒號,而相比之下,正規表示式狀態回溯會更加頻繁。所以,我們的狀態機的平均執行時間有顯著的改善:

以下是一些關於效能提升的統計資料( 正規表示式 VS URL的探測器 ):

關於這個庫的功能:

它是能夠找到並檢測任何網址,如:

  • HTML 5 Scheme – //www.linkedin.com<wbr>
  • 使用者名稱  -使用者:pass@linkedin.com<wbr>
  • 電子郵件  - fred@linkedin.com<wbr>
  • IPv4地址  – 192.168.1.1/hello.html
  • IPv4的八位位元組  – 0×00.0×00.0×00.0x00IPv4的十進位制  – HTTP:// 123 123 123 123 /
  • IPv6地址的  – FTP:// [:] /你好
  • IPv4對映的IPv6地址  – http://[fe30:4:3:0:192.3.2.1]/

另一個令人興奮的是,它也能識別已經被識別過的URL的部分。例如,對於 URL:http://user@example.com:39000/hello?boo=ff#frag,它能識別到以下的部分:

  • Scheme – “http”
  • Username – “user”
  • Password – null
  • Host – “example.com”
  • Port – 39000
  • Path – “/hello”
  • Query – “?boo=ff”
  • Fragment – “#frag”

這個庫還能夠處理引號匹配和HTML。根據你輸入的字串,你可能想以某種特別的方式來處理某些字元。例如你正在解析一段 HTML,你可能想去除引號或者尖括號,比如,你輸入的字串像這樣的:

<a href=”http://linkedin.com/abc”> linkedin.com </A>  ,那麼你可能想確保引號和尖括號被挑出來。出於這個原因,這個庫已經可以通過UrlDetectorOptions 這個 Java 類來設定不同的運作模式, 以改變對你輸入的內容的 “探測敏感度” 。通過這種方式,你可以正確的獲取到 linkedin.com 而不是linkedin.com</a> 。<wbr>

使用這個庫

想要使用這個庫,只需要簡單地從 GitHub 倉庫上克隆下來,並匯入 URL-Detector 庫。下面是一個使用的示例:

import com.linkedin.urls.detection.UrlDetector;
   import com.linkedin.urls.detection.UrlDetectorOptions;
   ...
   UrlDetector parser = new UrlDetector("hello this is a url Linkedin.com", UrlDetectorOptions.Default);
    List<Url> found = parser.detect();

    for(Url url : found) {
        System.out.println("Scheme: " + url.getScheme());
        System.out.println("Host: " + url.getHost());
        System.out.println("Path: " + url.getPath());
    }

有關更詳細的資訊,可以到 Readme 裡看 “如何使用”這一部分。

致謝

特別感謝 Vlad Shlosberg 和 Yulia Astakhova 對這個庫的貢獻。

總結

如果大家有對這個庫的改進建議,可以通過 GitHub 告訴我們;同時,如果有任何疑問,可以聯絡我們任何一個,我們將免費幫助你。祝你“探測”愉快!

譯文連結:http://www.codeceo.com/article/java-url-detector.html
英文原文:Open Sourcing URL-Detector
翻譯作者:碼農網 – Dee1024
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章