2018年3月31日 星期六

Java - HTTPS 檢查證書的安全連線


如何在JDK1.5中支援TLSv1.2這篇文章中,有講到如何對TLSv1.2的server進行HTTPS request,但這只是最簡單的實作例子,沒有考慮到憑證安全的檢查,如果被例如中間人攻擊的話,可能會連線請求中間被竄改了資料而不自知。

在前例中,可以看到在自訂的TrustManager 中,所有的method (getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())都沒有被實作內容,
如果發生了中間人攻擊,也就是在client端和server端的連線中間被某中間人攔截,
這時中間人可能會修改某些資料偽裝server端回傳資料給我們(client)端,
如果我們沒有對其憑證進行檢查的話,可能就無法察覺此攻擊了。

尤其是行動裝置的場合,例如手機在連到了駭客分享的wifi,程式如果不檢查HTTPS連線有無被劫持,就很容昜造成資安問題。

經過了查詢資料及自行實作後,我在這邊提供一個如何對 HTTPS 的 憑證進行驗證的方法。


  1. 為了簡單化,我使用JDK1.8,跟JDK1.5不同,JDK1.8支持TLSv1.2,
    所以不用跟上一篇文(如何在JDK1.5中支援TLSv1.2) 一樣使用BoncyCastle。
    並且把Excpetion全部throws出去,不用try-catch來使程式碼較為清晰。

    Note:
    其實JDK1.8在對TLSv1.2連線時,是不用自已建立TrustManager的,並且自己就會檢查憑證,並在檢查憑證有問題時拋出錯誤。
    但是要注意如果自己建立了TrustManager來用在連線中,卻不檢查憑證的話,在憑證有問題時是不會拋出錯誤的。
    而JDK1.5因為要使用BoncyCastle並會需要自已建立TrustManager,所以就要特別使用檢查憑證的TrustManager。
  2. 我們可以使用封包監測工具,例如Fiddler,來模擬中間人攻擊,如果我們把Java的HTTPS request 掛上Proxy,讓連線中間經過Fiddler的話,相當於就是Fiddler當了中間人,這時對使用了沒有進行憑證檢查的TrustManager的Java程式,是不會發現有任何異狀的。

步驟:

  1. 這裡要連的實驗對像跟之前一樣是號稱只支持TLSv1.2的fancyssl網站,首先去此網站上下載憑證。
    下載方式如下圖,按下F12打開開發者console後,選Security --> View Certificate --> 詳細資料 --> 複制到檔案。
  2. 使用java的工具 keytool 加入憑證到你要的憑證檔案中
    keytool -import -alias {別名} -keystore {要被放入憑證的憑證檔} -file {從網站上下載的憑證}
    例如:
    keytool -import -alias fancyssl -keystore D:\test_cer -file D:\fancyssl_cer.cer
    之後會要你打密碼,第一次產生憑證檔自己取。如果是本來就有的檔案通常密碼沒改就是預設 : changeit
  3. 接下來我展示在Java1.8下的三種寫法:
    1. HttpsTest_notSafe.java
      (自己建立的,沒有檢查憑證的TrustManager)
      import java.io.BufferedReader;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.HttpURLConnection;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.security.KeyManagementException;
      import java.security.KeyStore;
      import java.security.KeyStoreException;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      import java.security.cert.Certificate;
      import java.security.cert.CertificateException;
      import java.security.cert.CertificateFactory;
      import java.security.cert.X509Certificate;
      
      import javax.net.ssl.HostnameVerifier;
      import javax.net.ssl.HttpsURLConnection;
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSession;
      import javax.net.ssl.TrustManager;
      import javax.net.ssl.TrustManagerFactory;
      import javax.net.ssl.X509TrustManager;
      
      public class HttpsTest_notSafe {
      
       public static void main(String[] args)
         throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException {
        // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊
        System.setProperty("http.proxyHost", "127.0.0.1");
        System.setProperty("http.proxyPort", "8888");
        System.setProperty("https.proxyHost", "127.0.0.1");
        System.setProperty("https.proxyPort", "8888");
        //
        
        // 自訂的TrustManager,沒有檢查憑證,不安全
        TrustManager trustManager = new X509TrustManager() {
         public X509Certificate[] getAcceptedIssuers() {
          return new X509Certificate[0];
         }
         public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
          // do nothing
         }
         public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
          // do nothing
         }
        };
      
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        // 自訂的TrustManager,沒有檢查憑證,不安全
        sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        
        HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection();
        httpsUrlConnection.connect();
      
        // 印出Response
        printFromInputStream(httpsUrlConnection.getInputStream());
        
        httpsUrlConnection.disconnect();
        
       }
       
       static void printFromInputStream(InputStream in) throws IOException {
        BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in)));
        StringBuffer responseTextStringBuffer = new StringBuffer();
        String tempString = null;
        while ((tempString = responseBufferedReader.readLine()) != null) {
         responseTextStringBuffer.append(tempString + "\n");
        }
        String responseText = responseTextStringBuffer.toString();
        System.out.println(responseText);
       }
      }
    2. HttpsTest_safe1.java
      (JDK1.8 直接使用自帶TLSv1.2的https支持,不用建立TrustManager)
      import java.io.BufferedReader;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.HttpURLConnection;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.security.KeyManagementException;
      import java.security.KeyStore;
      import java.security.KeyStoreException;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      import java.security.cert.Certificate;
      import java.security.cert.CertificateException;
      import java.security.cert.CertificateFactory;
      import java.security.cert.X509Certificate;
      
      import javax.net.ssl.HostnameVerifier;
      import javax.net.ssl.HttpsURLConnection;
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSession;
      import javax.net.ssl.TrustManager;
      import javax.net.ssl.TrustManagerFactory;
      import javax.net.ssl.X509TrustManager;
      
      public class HttpsTest_safe1 {
      
       public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException {
        // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊
        System.setProperty("http.proxyHost", "127.0.0.1");
        System.setProperty("http.proxyPort", "8888");
        System.setProperty("https.proxyHost", "127.0.0.1");
        System.setProperty("https.proxyPort", "8888");
        //
      
        //建立連線
        HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection();
        httpsUrlConnection.connect();
      
        // 印出Response
        printFromInputStream(httpsUrlConnection.getInputStream());
        
        httpsUrlConnection.disconnect();  
       }
       
       static void printFromInputStream(InputStream in) throws IOException {
        BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in)));
        StringBuffer responseTextStringBuffer = new StringBuffer();
        String tempString = null;
        while ((tempString = responseBufferedReader.readLine()) != null) {
         responseTextStringBuffer.append(tempString + "\n");
        }
        String responseText = responseTextStringBuffer.toString();
        System.out.println(responseText);
       }
      }
    3. HttpsTest_safe2.java
      (自已建立的,使用憑證檔產生出TrustManager)
      import java.io.BufferedReader;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.HttpURLConnection;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.security.KeyManagementException;
      import java.security.KeyStore;
      import java.security.KeyStoreException;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      import java.security.cert.Certificate;
      import java.security.cert.CertificateException;
      import java.security.cert.CertificateFactory;
      import java.security.cert.X509Certificate;
      
      import javax.net.ssl.HostnameVerifier;
      import javax.net.ssl.HttpsURLConnection;
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSession;
      import javax.net.ssl.TrustManager;
      import javax.net.ssl.TrustManagerFactory;
      import javax.net.ssl.X509TrustManager;
      
      public class HttpsTest_safe2 {
      
       public static void main(String[] args)
         throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException {
        // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊
        System.setProperty("http.proxyHost", "127.0.0.1");
        System.setProperty("http.proxyPort", "8888");
        System.setProperty("https.proxyHost", "127.0.0.1");
        System.setProperty("https.proxyPort", "8888");
        //  
        
              //從憑證產生TrustManager
              String password = "changeit";  //預設密碼為changeit
              //讀取憑證
              File file = new File("D:/test_cer");
              //通常會將憑證都加到%Java_Home%/lib/security/cacerts/中
              //File file = new File(System.getProperty("java.home") + "/lib/security/cacerts");
              InputStream in = new FileInputStream(file);
              KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
              keyStore.load(in, password.toCharArray());
              in.close();
              //建立TrustManager
              TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509", "SunJSSE");
              trustManagerFactory.init(keyStore);
      
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
      
        HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection();
        httpsUrlConnection.connect();
      
        // 印出Response
        printFromInputStream(httpsUrlConnection.getInputStream());
        
        httpsUrlConnection.disconnect();
        
       }
       
       static void printFromInputStream(InputStream in) throws IOException {
        BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in)));
        StringBuffer responseTextStringBuffer = new StringBuffer();
        String tempString = null;
        while ((tempString = responseBufferedReader.readLine()) != null) {
         responseTextStringBuffer.append(tempString + "\n");
        }
        String responseText = responseTextStringBuffer.toString();
        System.out.println(responseText);
       }
      }
我們可以打開 Fiddler,並在 Tools --> Options --> HTTPS,勾選 "Decrypt HTTPS traffic 設定攔截 HTTPS request 模擬中間人,
並且在上述的三個Java程式中,對設定Proxy的程式碼註解或不註解來測試。
可以發現以下結果:
有拋出錯誤沒有拋出錯誤有拋出錯誤沒有拋出錯誤
有經過Fiddler沒有經過Fiddler
HttpsTest_notSafe.java沒有拋出錯誤沒有拋出錯誤
HttpsTest_safe1.java有拋出錯誤沒有拋出錯誤
HttpsTest_safe2.java有拋出錯誤沒有拋出錯誤

可以看到,如果是JDK1.8如果使用了自帶TLSv1.2的支持寫法,即不自己建立TrustManager的話,程式是可以自行檢查出憑證錯誤的。

但如果使用了自已建立的TrustManager時,就要使用有檢查憑證的TrustManager才行,此篇文章使用的是由憑證檔產生TrustManager。
除了由憑證檔產生TrustManager以外,也可由自己撰寫檢查憑證相關的TrustManager method ((getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())) 。

原始碼下載:
HttpsTest.7z

參考資料
  1. Java 使用自签证书访问https站点
  2. 苹果核 - Android App 安全的HTTPS 通信
  3. Sun Java System Application Server Enterprise Edition 8.2 管理指南
  4. Java Keytool的使用及申請憑證(以Microsoft Active Directory Certificate Services為例)
  5. java中 SSL认证和keystore使用

Java使用Selenium進行瀏覽器自動化操作

Selenium是一個瀏覽器自動化的工具,可以使用它來控制瀏覽器執行各種操作,
在這篇文中我簡單介紹JAVA如何使用Selenium及展示一個範例。

前置步驟:
  1. 下載 Selenium 的 Java Lib
    到Selenium的下載頁面下載
    1. Selenium Client & WebDriver Language Bindings
    2. Selenium Standalone Server (我下載的為3.11.0)
  2. 下載各瀏覽器的WebDriver
    1. Chrome : ChromeDriver
    2. Firefox : geckodriver
  3. 在Java程式中引入Library和指定WebDriver就可以開始控制瀏覽器了。
在這邊展示一個簡單的程式,控制瀏覽器進到Google首頁,在搜尋框中打上關鍵字,然後將10頁的查詢結果Title和Link url印出來。

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class SeleniumTest1 {

 public static void main(String[] args) {
  System.out.println("Start");
  //自己選擇要用的瀏覽器 WebDriver
  //用Chrome
  System.setProperty("webdriver.chrome.driver","D:\\JavaLib\\Selenium\\webdrivers\\Chrome\\chromedriver.exe");
  WebDriver driver = new ChromeDriver();
  
  //用Firefox
  //System.setProperty("webdriver.gecko.driver","D:\\javaLib\\selenium-java-3.10.0\\Firefox\\geckodriver.exe");
  //WebDriver driver = new FirefoxDriver();
  
  //使用implicitlyWait,抓取DOM時,會等DOM出現才抓,最多等10秒
  driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
  driver.get("https://www.google.com.tw/"); //開啟瀏覽器到 Google 首頁
  
  //抓取DOM element,#lst-ib 為Google搜尋框
  WebElement searchInput = driver.findElement(By.id("lst-ib"));
  
  //執行Javascript範例
  //將Google搜尋框打上字,"keyword"
  JavascriptExecutor javascriptExecutor = (JavascriptExecutor) driver;
  javascriptExecutor.executeScript("arguments[0].value='keyword';", searchInput);
  
  //用WebElement物件直接操做DOM element範例
  //抓取DOM element,name=btnK 為Google搜尋按鈕
  WebElement searchBtn = driver.findElement(By.name("btnK"));
  searchBtn.click();
  
  //印出十頁的所有搜尋結果Title和Link url
  for (int i = 0; i < 10; i++) {
   //抓取DOM elements, (.r a) 為Google搜尋結果的link
   List<WebElement> searchReultATagList = driver.findElements(By.cssSelector(".r a"));
   for (WebElement searchReultATag : searchReultATagList) {
    System.out.println(searchReultATag.getText() + " : ");
    System.out.println(searchReultATag.getAttribute("href"));
    System.out.println("=======================");
   }
   //抓取DOM element, #pnnext 為Google搜尋下一頁按鈕
   WebElement nextPageBtn = driver.findElement(By.id("pnnext"));
   nextPageBtn.click();
  }
  
  driver.quit(); //關閉瀏覽器
 }

}



sss
參考資料

  1. Selenium using Java - The path to the driver executable must be set by the webdriver.gecko.driver system property
  2. How to get selenium to wait for ajax response?
  3. Selenium驱动火狐、IE、Edge和Chrome浏览器的方法

2018年3月20日 星期二

使用代理 + fiddler 來監看 java 的 http/https 封包 / 模擬中間人攻擊

在 Java 中進行如下 propxy 的設置,將網路請求都先經過本地端的 fiddler (這裡fiddler的port為8888),就可以在fiddler中監看 java 發出的封包

System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "8888");

System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "8888");

而此時也可以模擬中間人攻擊,因為http/https請求經過了Fiddler,可以在此情況下測試自己的防範中間人攻擊的程式

如何在JDK1.5中支援TLSv1.2

使用Java 傳 HTTPS request 時,如果接收端的server (例如別家公司開放的web api)是只使用 TLSv1.2 時,可能會因為舊版的 jdk 沒有支援,而造成連線錯誤。

如果是使用 JDK1.8,其支持 TLSv1.2,並且且預設就使用 TLSv1.2 來進行 HTTPS 的連線,
寫法可以參考之前寫的這篇文章 "以Java 由Url下載圖片" 裡面的 "getHttpURLConnectionFromHttps()" 方法來進行 HTTPS 的請求。

但是例如如果為較舊的 JDK1.5,則沒有支持 TLSv1.2,此時還用 JDK1.8的寫法去請求 TLSv1.2的server時,就會產生錯誤。

在這篇文章中,我紀錄了我找到的方法,如何讓 JDK1.5 也能進行對 TLSv1.2 server的請求。

=================================

我使用的JDK為 jdk1.5.0_22
測試的網站為 https://fancyssl.hboeck.de/
此網站聲稱只為TLSv1.2的request開放
可以使用線上SSL檢測工具來查看此網站的SSL相關設定
https://www.ssllabs.com/ssltest/analyze.html?d=fancyssl.hboeck.de

需要的 jar

  1. Bouncy Castle 的 Library : 
    1. Provider 
    2. DTLS/TLS API/JSSE Provider
  2. 解開Java密鑰長度限制的
    "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files"
    1. JDK 1.5 可到這裡下載 "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 5.0"。


步驟:

  1. 導入Bouncy Castle的 jar
    Bouncy Castle 是一個提供許多密碼演法等的 Java Library,而 TLSv1.2 跟 舊版的差別其中就有密碼演算等不同,其中更深的原理我沒有鑽研,在此只展示找到的可行解決方案。

    首先先到 Bouncy Castle官網 下載 Provider 及 DTLS/TLS API/JSSE Provider 的 jar 檔,我這邊選擇的為 bcprov-jdk15on-159.jarbctls-jdk15on-159.jar。 請下載並加到 Java 專案的 Library中。
  2. 測試程式進對指定的url進行TLSv1.2 HTTPS 請求,並將回應印出來,在這裡會將整個網頁的源始碼印出來。

    測試程式碼如下:
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.security.SecureRandom;
    import java.security.Security;
    import java.security.cert.CertificateException;
    import java.security.cert.X509Certificate;
    
    import javax.net.ssl.HostnameVerifier;
    import javax.net.ssl.HttpsURLConnection;
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.SSLSession;
    import javax.net.ssl.TrustManager;
    import javax.net.ssl.X509TrustManager;
    
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
    
    public class HttpsClientRequestTest {
    
        public static void main(String[] args)
                throws Exception
            {
                Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
                Security.insertProviderAt(new BouncyCastleProvider(), 1);
    
                Security.removeProvider(BouncyCastleJsseProvider.PROVIDER_NAME);
                Security.insertProviderAt(new BouncyCastleJsseProvider(), 2);
    
                /*
                 * TEST CODE ONLY. If writing your own code based on this test case, you should configure
                 * your trust manager(s) using a proper TrustManagerFactory, or else the server will be
                 * completely unauthenticated.
                 */
                TrustManager tm = new X509TrustManager()
                {
                    public X509Certificate[] getAcceptedIssuers()
                    {
                        return new X509Certificate[0];
                    }
    
                    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
                    {
                     //do nothing
                    }
    
                    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
                    {
                     //do nothing
                    }
                };
                
                HostnameVerifier customHostnameVerifier = new HostnameVerifier() {
                     public boolean verify(String arg0, SSLSession arg1) {        
                          return true;
                     }
                };
    
                SSLContext sslContext = SSLContext.getInstance("TLSv1.2", BouncyCastleJsseProvider.PROVIDER_NAME);
                sslContext.init(null, new TrustManager[]{ tm }, new SecureRandom());
                HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
                HttpsURLConnection.setDefaultHostnameVerifier(customHostnameVerifier);
                
                HttpURLConnection httpUrlConnection = (HttpURLConnection) (new URL("https://fancynossl.hboeck.de")).openConnection();
                
                httpUrlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.2.1; Nexus 7 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166  Safari/535.19");
                httpUrlConnection.connect();
    
                InputStream is = null;
         if (httpUrlConnection.getResponseCode() >= 400) {  
             is = httpUrlConnection.getErrorStream();  
         } else {  
             is = httpUrlConnection.getInputStream();  
         }
          
                BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(is)));
                StringBuffer responseTextStringBuffer = new StringBuffer();
                String tempString = null;
                while((tempString = responseBufferedReader.readLine()) != null) {
                    responseTextStringBuffer.append(tempString + "\n");
                }
                String responseText = responseTextStringBuffer.toString();
                httpUrlConnection.disconnect();
          
                System.out.println(responseText);
            }
    }
    
  3. 因為還沒有開放 Java 的密鑰長度限制,所以應該會出現以下錯誤java.security.InvalidKeyException: Illegal key sizeException
    此時將下載的 "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 5.0" 裡面的 "local_policy.jar" 和 "US_export_policy.jar" 覆蓋到JAVA_HOME\jre\lib\security\ 之下的 "local_policy.jar" 和 "US_export_policy.jar" ,再執行一次,應該就會成功了。
    下載頁面如圖所示:
    程式成功時輸出如圖,可以看到網頁的html源始碼被印了出來 (Eclipse):



參考資料:
  1. java.lang.IllegalArgumentException: TLSv1.2 on JRE 1.5
  2. The Legion of the Bouncy Castle
  3. bcgit/bc-java
  4. 解决java.io.IOException: HTTPS hostname wrong: should be
  5. AES加密时抛出java.security.InvalidKeyException: Illegal key size or default parameters

2017年10月11日 星期三

以Java 由Url下載圖片

今天要來記錄如何使用Java來下載網路上的圖片(或檔案,此例只下載圖片),

Java可以對Url進行Http Request來取得Response,
得到input stream後將檔案儲存下來,

為了避免有些Server會擋程式的Request,
我們必須模仿瀏覽器,在Request中加上User-Agent的Header (或更多的其他Header,模仿的越像越不容易被擋),

如果圖片的Url是http的話比較簡單,
但如果是https的話,即SSL,那就要取得對方Server網站的憑證,
或是自己實作一個 X509TrustManager,來所有憑證檢查都通過,

如果是JDK 1.8 以下,可能會有此Exception :
Could not generate DH keypair
使用 JDK 1.8 就可解決此問題,
可參考 [Java] 處理無法透過SSL抓取網站資料的問題

以下為程式碼範例,說明都寫在註解中:


import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class ImageDownloader {

 public static void main(String[] args) {

  String fileName_http = downloadImageFromUrl("http://www.image.com/files/8813/5551/7470/cruise-ship.png","D:" + File.separator, "HttpImgTest");
  System.out.println("Http的圖片下載: " + fileName_http);
  
  String fileName_https = downloadImageFromUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Plithocyon_armagnacensis.JPG/220px-Plithocyon_armagnacensis.JPG","D:" + File.separator, "HttpsImgTest");
  System.out.println("Https的圖片下載: " + fileName_https);

 }

 public static String downloadImageFromUrl(String url, String fileDirectoryPath, String fileNameWithoutFormat) {
  String filePath = null;
  
  BufferedInputStream in = null;
  ByteArrayOutputStream out = null;
  HttpURLConnection httpUrlConnection = null;
  FileOutputStream file = null;

  try {
   
   if (url.startsWith("https://")) {
    //HTTPS時
    httpUrlConnection = getHttpURLConnectionFromHttps(url);
   }
   //如果不是HTTPS或是沒成功得到httpUrlConnection,用HTTP的方法
   if(httpUrlConnection == null) {
    httpUrlConnection = (HttpURLConnection) (new URL(url)).openConnection();
   }
   
   // 設置User-Agent,偽裝成一般瀏覽器,不然有些伺服器會擋掉機器程式請求
   httpUrlConnection.setRequestProperty("User-Agent",
     "Mozilla/5.0 (Linux; Android 4.2.1; Nexus 7 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166  Safari/535.19");
   httpUrlConnection.connect();

   String imageType;
   if (httpUrlConnection.getResponseCode() == 200) {
    //成功取得response,
    //取得contentType
    String contentType = httpUrlConnection.getHeaderField("Content-Type");
    // 只處理image的回應
    if ("image".equals(contentType.substring(0, contentType.indexOf("/")))) {
     //得到對方Server提供的圖片副檔名,如jpg, png等
     imageType = contentType.substring(contentType.indexOf("/") + 1);

     if (imageType != null && !"".equals(imageType)) {
      //由HttpUrlConnection取得輸入串流
      in = new BufferedInputStream(httpUrlConnection.getInputStream());
      out = new ByteArrayOutputStream();

      //建立串流Buffer
      byte[] buffer = new byte[1024];

      file = new FileOutputStream(new File(fileDirectoryPath + File.separator + fileNameWithoutFormat + "." + imageType));

      int readByte;
      while ((readByte = in.read(buffer)) != -1) {
       //輸出檔案
       out.write(buffer, 0, readByte);
      }      

      byte[] response = out.toByteArray();
      file.write(response);      

      //下載成功後,返回檔案路徑
      filePath = fileDirectoryPath + File.separator + fileNameWithoutFormat + "." + imageType;
     }
    }

   }
  } catch (MalformedURLException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  } finally {
   //關閉各種串流
   try {
    if (out != null) {
     out.close();
    }
    if (in != null) {
     in.close();
    }
    if (httpUrlConnection != null) {
     httpUrlConnection.disconnect();
    }
    if (file != null) {
     file.close();
    }
   }catch (IOException e) {
    e.printStackTrace();
   }
   
  }
  return filePath;
 }

 public static HttpURLConnection getHttpURLConnectionFromHttps(String url) {
  HttpURLConnection httpUrlConnection = null;
  //建立一個信認所有憑證的X509TrustManager,放到TrustManager裡面
  TrustManager[] trustAllCerts;
  try {
   // Activate the new trust manager
   trustAllCerts = new TrustManager[] { new X509TrustManager() {

    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {     // TODO Auto-generated method stub
     //不作任何事
    }

    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {     // TODO Auto-generated method stub
     //不作任何事
    }

    public X509Certificate[] getAcceptedIssuers() {
     //不作任何事
     return null;
    }

   } };

   //設置SSL設定
   SSLContext sslContext = SSLContext.getInstance("SSL");
   sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
   HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

   //跟HTTP一樣,用Url建立連線
   httpUrlConnection = (HttpURLConnection) (new URL(url)).openConnection();
  } catch (KeyManagementException e) {
   e.printStackTrace();
  } catch (NoSuchAlgorithmException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  }
  
  return httpUrlConnection;
 }

}


源碼下載:
ImageDownloaderFromHttpOrHttps.7z

參考資料:

2017年10月5日 星期四

正規表逹式 - Java 範例

紀錄下Java的正規表示法使用方法範例:

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegTest {

 public static void main(String[] args) { 
  //(?i) 代表大小寫忽略
  //.*?為非貪婪演算,盡可能找最小範圍的結果值
  doRegularExpression("[ABC/def]kk[123/456][ABC/def]kk[123/456]", "(?i)\\[(a.*?)/(.*?)\\]");
  //.*為貪婪演算,盡可能找最大範圍的結果值
  doRegularExpression("[ABC/def]kk[123/456][ABC/def]kk[123/456]", "(?i)\\[(a.*)/(.*)\\]");
  
  /* 輸出為:
  ===========================
  測試的句子: [ABC/def]kk[123/456][ABC/def]kk[123/456]
  正規表示法: (?i)\[(a.*?)/(.*?)\]
  ---------------------------
  第1次匹配,找到2個Group:
  匹配的字串為: [ABC/def]
  第1個Group: ABC
  第2個Group: def
  ---------------------------
  第2次匹配,找到2個Group:
  匹配的字串為: [ABC/def]
  第1個Group: ABC
  第2個Group: def
  ===========================
  
  ===========================
  測試的句子: [ABC/def]kk[123/456][ABC/def]kk[123/456]
  正規表示法: (?i)\[(a.*)/(.*)\]
  ---------------------------
  第1次匹配,找到2個Group:
  匹配的字串為: [ABC/def]kk[123/456][ABC/def]kk[123/456]
  第1個Group: ABC/def]kk[123/456][ABC/def]kk[123
  第2個Group: 456
  ===========================
  */
 }
 
 static void doRegularExpression(String text, String regularExpression) {
  System.out.println("===========================");
  System.out.println("測試的句子: " + text);
  System.out.println("正規表示法: " + regularExpression);
  
  Pattern pattern = Pattern.compile(regularExpression);
  Matcher matcher = pattern.matcher(text);  
  
  
  for(int matchCount = 1 ; matcher.find(); matchCount++) {
   // groupCount不包括匹配的字串,即matcher.group(0)
   System.out.println("---------------------------");
   System.out.println("第" + matchCount + "次匹配,找到" + matcher.groupCount() + "個Group:"); 
   System.out.println("匹配的字串為: " + matcher.group(0));
   for(int groupCount = 1; groupCount <= matcher.groupCount(); groupCount++) {
          System.out.println("第" + groupCount + "個Group: " + matcher.group(groupCount));
      }
  }
  System.out.println("===========================");
  System.out.println();
 }

}


2017年6月22日 星期四

Gifsicle 和 Giflossy(Gifsicle的一個Github Fork) (nmake, visual studio)

Gifsicle是個功能強大的GIF處理程式,使用命令列視窗來執行,
可以將許多張圖合成一張GIF動畫、
查詢GIF的size (包括各frame)、
切割、提取、加入frame到GIF動畫中、
壓縮GIF等。

以下是它的資訊:
Gifsicle
官網 :  http://www.lcdf.org/gifsicle/
Github : https://github.com/kohler/gifsicle

gifsicle只提供了三種壓縮輸出,
-O1, -O2, -O3,其中-O3效果最好,
例如 cmd 指令:
gifsicle -O3 froSrc.gif  -o toSrc.gif
但對於一些GIF可能無法再壓縮的更小。

這時就可以使用Giflossy來幫忙了。
Giflossy是一個在Github上fork了gifsicle的一個分支專案,
擴充了gifsicle的功能,加了一個參數,--lossy=XX
XX可選數字,越大壓縮比率越高,但也越失真,
可以用如下cmd指令來壓縮:
gifsicle -O3 froSrc.gif --lossy=30 -o toSrc.gif

以下是Giflossy的資料

Giflossy
官網 : https://kornel.ski/lossygif
Github : https://github.com/pornel/giflossy


===================================================

Gifsicle和Giflossy的Github上都沒有提供給Windows現成的 exe 檔,
但有提供Makefile,可以利用Visual Studio 內建命令列視窗中的 make 工具來進行
編譯,
下載好Gifsicle或Giflossy後,如果有裝Visual Studio,可以打開"開發人員命令提示字元" ( Developer Command Prompt ),進到 gifsicle/src/ 內,打上
nmake -f Makefile.w32

就可以在當下路徑中看到編譯出來的 gifsicle.exe 了,
要注意的是它並沒有幫忙編譯 gifdiff.exe 。

如果有人沒有 Visual Studio,或是需要32位元的編譯版本,
我在這裡發現有人幫忙編譯好了 ,順便也放在這裡分享。