2012년 3월 19일 월요일

HTTPS 통신

참고 - http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html

프로젝트명: HTTPSPractice


파일 구성:

 - HttpsClientWithCustomCert.java - 사용자가 만든 인증서를 이용한 서버 인증
 - HttpsClientWithDefaultCACert.java - 공인 인증된 인증서로 서버 인증
 - HttpsClientWithoutValidation.java - 인증 없이 서버 접속
 - SimpleHttpsServer.java - 사용자가 만든 인증키를 이용하는 간단한 HTTPS 서버
 - keystore.jks - 서버(SimpleHttpsServer) 에서 사용하는 인증키 저장소
 - truststore.jks - 클라이언트(HttpsClientWithCustomCert) 에서 서버 인증시 사용하는 인증서 저장소


다음과 같은 순서로 설명
1. HttpsURLConnection 을 이용한 접속 (인증 과정 없음)
2. 공인 인증된 인증서로 서버 인증
3. 사용자가 만든 인증서로 서버 인증


1. HttpsURLConnection 을 이용한 접속 (인증 과정 없음)

인증 과정을 결정하는 부분은 SSLContext.init() 함수이다.
아래 소스에는 init() 함수에 모두 null 을 집어 넣음으로써 인증 과정을 무시하고 서버의 내용물을 받는다.

소스:
package com.test.https.practice;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;


/**
 * 
 */
public class HttpsClientWithoutValidation {
 
 /**
  * 
  * @param urlString
  * @throws IOException
  * @throws NoSuchAlgorithmException
  * @throws KeyManagementException
  */
 public void getHttps(String urlString) throws IOException, NoSuchAlgorithmException, KeyManagementException {
  
  // Get HTTPS URL connection
  URL url = new URL(urlString);  
  HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
  
  // Set Hostname verification
  conn.setHostnameVerifier(new HostnameVerifier() {
   @Override
   public boolean verify(String hostname, SSLSession session) {
    // Ignore host name verification. It always returns true.
    return true;
   }
   
  });
  
  // SSL setting
  SSLContext context = SSLContext.getInstance("TLS");
  context.init(null, null, null);  // No validation for now
  conn.setSSLSocketFactory(context.getSocketFactory());
  
  // Connect to host
  conn.connect();
  conn.setInstanceFollowRedirects(true);
  
  // Print response from host
  InputStream in = conn.getInputStream();
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  String line = null;
  while ((line = reader.readLine()) != null) {
   System.out.printf("%s\n", line);
  }
  
  reader.close();
 }
 
 /**
  * 
  * @param args
  * @throws Exception
  */
 public static void main(String[] args) throws Exception {
  HttpsClientWithoutValidation test = new HttpsClientWithoutValidation();
  test.getHttps("https://www.google.com");
 }
}

결과:


2. 공인 인증된 인증서로 서버 인증

SSLContext.init() 함수의 두번째 인자에 X509TrustManager 를 선언하여 집어 넣었다.
그리고 JRE 를 설치하면 기본적으로 [JRE 경로]/lib/security/cacerts 라는 파일명의 공인 인증된 인증서 저장소 파일이 있다.
해당 인증서 저장소를 이용하여 서버의 인증서를 검사하게 하였다.

소스:
package com.test.https.practice;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
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.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

/**
 *
 */
public class HttpsClientWithDefaultCACert {
 
 /**
  * 
  * @param urlString
  * @throws IOException
  * @throws NoSuchAlgorithmException
  * @throws KeyManagementException
  */
 public void getHttps(String urlString) throws IOException, NoSuchAlgorithmException, KeyManagementException {
  
  // Get HTTPS URL connection
  URL url = new URL(urlString);  
  HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
  
  // Set Hostname verification
  conn.setHostnameVerifier(new HostnameVerifier() {
   @Override
   public boolean verify(String hostname, SSLSession session) {
    // Ignore host name verification. It always returns true.
    return true;
   }
   
  });
  
  // SSL setting
  SSLContext context = SSLContext.getInstance("TLS");
  context.init(null, new TrustManager[] { new X509TrustManager() {

   @Override
   public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    // client certification check
   }

   @Override
   public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    
    // Server certification check
    
    try {
     
     // Get trust store
     KeyStore trustStore = KeyStore.getInstance("JKS");
     String cacertPath = System.getProperty("java.home") + "/lib/security/cacerts"; // Trust store path should be different by system platform.
     trustStore.load(new FileInputStream(cacertPath), "changeit".toCharArray()); // Use default certification validation
     
     // Get Trust Manager
     TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
     tmf.init(trustStore);
     TrustManager[] tms = tmf.getTrustManagers();
     ((X509TrustManager)tms[0]).checkServerTrusted(chain, authType);
     
    } catch (KeyStoreException e) {
     e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
     e.printStackTrace();
    } catch (IOException e) {
     e.printStackTrace();
    }
    
   }

   @Override
   public X509Certificate[] getAcceptedIssuers() {
    return null;
   }
  } }, null);
  conn.setSSLSocketFactory(context.getSocketFactory());
  
  // Connect to host
  conn.connect();
  conn.setInstanceFollowRedirects(true);
  
  // Print response from host
  InputStream in = conn.getInputStream();
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  String line = null;
  while ((line = reader.readLine()) != null) {
   System.out.printf("%s\n", line);
  }
  
  reader.close();
 }
 
 /**
  * 
  * @param args
  * @throws Exception
  */
 public static void main(String[] args) throws Exception {
  HttpsClientWithDefaultCACert test = new HttpsClientWithDefaultCACert();
  test.getHttps("https://www.google.com");
 }
}

결과:


3. 사용자가 만든 인증서로 서버 인증

우선 인증키 저장소와 인증서 저장소를 만들어 보자.
Portecle 라는 툴을 사용했다.

3.1. 인증키 저장소 만들기(keystore.jks)
3.2. 인증키에서 인증서 추출
3.3. 인증서 저장소 만들기(truststore.jks)

서버는 지정된 인증키 저장소를 이용해 클라이언트에 자신을 인증하는 키를 보내게 된다. 인증과정이 성공하면 클라이언트에 html 내용물을 전달하고 종료한다.
클라이언트는 지정된 인증서 저장소를 이용해 서버를 인증한다. 인증과정이 성공하면 서버로 부터 html 내용물을 전달받는다.

서버 소스:
참고 - http://www.java2s.com/Tutorial/Java/0490__Security/SSLContextandKeymanager.htm
package com.test.https.practice;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;

public class SimpleHttpsServer {
 
 public void run(int port) throws NoSuchAlgorithmException, KeyManagementException, IOException, KeyStoreException, CertificateException, UnrecoverableKeyException {
  
  // create ssl context
  SSLContext context = SSLContext.getInstance("TLS");
  
  // set key store
  KeyStore keyStore = KeyStore.getInstance("JKS");
  keyStore.load(new FileInputStream("keystore.jks"), "changeit".toCharArray());
  KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
  kmf.init(keyStore, "changeit".toCharArray());
  context.init(kmf.getKeyManagers(), null, null);
  
  // create ssl socket
  SSLServerSocketFactory factory = context.getServerSocketFactory();
  SSLServerSocket socket = (SSLServerSocket)factory.createServerSocket(port);
  SSLSocket client = (SSLSocket)socket.accept();
  InputStream in = client.getInputStream();
  OutputStream out = client.getOutputStream();
  
  // read from client
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  reader.readLine();
  
  // write to client
  BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
  writer.write("HTTP/1.0 200 OK");
  writer.newLine();
  writer.write("Content-Type: text/html");
  writer.newLine();
  writer.newLine();
  writer.write("<html><head><title>Hello</title></head><body>Hellow!</body></html>");
  writer.flush();
  
  // close
  writer.close();
  reader.close();
  client.close();
 }
 
 /**
  * 
  * @param args
  * @throws Exception
  */
 public static void main(String[] args) throws Exception {
  SimpleHttpsServer server = new SimpleHttpsServer();
  server.run(9999);
 }
}


클라이언트 소스:
package com.test.https.practice;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
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.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

public class HttpsClientWithCustomCert {
 
 /**
  * 
  * @param urlString
  * @throws IOException
  * @throws NoSuchAlgorithmException
  * @throws KeyManagementException
  */
 public void getHttps(String urlString) throws IOException, NoSuchAlgorithmException, KeyManagementException {
  
  // Get HTTPS URL connection
  URL url = new URL(urlString);  
  HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
  
  // Set Hostname verification
  conn.setHostnameVerifier(new HostnameVerifier() {
   @Override
   public boolean verify(String hostname, SSLSession session) {
    // Ignore host name verification. It always returns true.
    return true;
   }
   
  });
  
  // SSL setting
  SSLContext context = SSLContext.getInstance("TLS");
  context.init(null, new TrustManager[] { new X509TrustManager() {

   @Override
   public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    // client certification check
   }

   @Override
   public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    
    // Server certification check
    
    try {
     
     // Get trust store
     KeyStore trustStore = KeyStore.getInstance("JKS");
     trustStore.load(new FileInputStream("truststore.jks"), "changeit".toCharArray()); // Use default certification validation
     
     // Get Trust Manager
     TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
     tmf.init(trustStore);
     TrustManager[] tms = tmf.getTrustManagers();
     ((X509TrustManager)tms[0]).checkServerTrusted(chain, authType);
     
    } catch (KeyStoreException e) {
     e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
     e.printStackTrace();
    } catch (IOException e) {
     e.printStackTrace();
    }
    
   }

   @Override
   public X509Certificate[] getAcceptedIssuers() {
    return null;
   }
  } }, null);
  conn.setSSLSocketFactory(context.getSocketFactory());
  
  // Connect to host
  conn.connect();
  conn.setInstanceFollowRedirects(true);
  
  // Print response from host
  InputStream in = conn.getInputStream();
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  String line = null;
  while ((line = reader.readLine()) != null) {
   System.out.printf("%s\n", line);
  }
  
  reader.close();
 }
 
 /**
  * 
  * @param args
  * @throws Exception
  */
 public static void main(String[] args) throws Exception {
  HttpsClientWithCustomCert test = new HttpsClientWithCustomCert();
  test.getHttps("https://127.0.0.1:9999");
 }
}

결과:

stdin 입력

import java.io.*;

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();

System.out.println(line);


2012년 1월 14일 토요일

XML 파싱하기

참고 - http://www.w3.org/TR/REC-xml/
(Extensible Markup Language (XML) 1.0 (Fifth Edition))


XML 파서의 종류는 SAX 파서와 DOM 파서가 있다.

SAX 파서와 DOM 파서의 차이점:

참고 - http://www.ibiblio.org/xml/books/xmljava/chapters/ch09s11.html
(Choosing between SAX and DOM)

 > 요약: 어떤 파서를 사용할지는 아래와 같은 상황을 고려하여 선택하는 편이 좋다.

SAX 파서를 사용하는 상황
  • 파싱하려는 XML 크기가 너무 클 때
  • 파싱하려는 XML 중 필요한 정보만 취합하고자 할 때
  • 실시간으로 파싱을 처리하고자 할때

DOM 파서를 사용하는 상황
  • XML 문서안에 넓게 분포된 자료들 한번에 취합하고자 할 때
  • 파싱하려는 XML 문서 구조가 매우 복잡할 때
  • XML 문서를 수정할 때
  • 파싱한 XML 정보를 여러 함수들이 사용하고자 할 때

파서 사용법:

예) RSS 파일을 파싱하는 예

NY times 사이트의 RSS 링크 중 하나를 선택하여 nyfeed.xml 이라는 파일명으로 저장하였다.
RSS 항목들 중 item 항목에 대한 title 과 link 만 추려서 출력한다.


프로젝트 구조는 아래와 같다.

실행 결과는 아래와 같다.

DOM 파서

SAX 파서

nyfeed.xml
아래 링크로 접속하여 소스 보기 한 후 내용을 복사하여 프로젝트 폴더에 nyfeed.xml 파일로 저장
http://feeds.nytimes.com/nyt/rss/World

XMLParseTest.java
같은 일을 하는 함수 두개를 만들었다. domParseTest() 와 saxParseTest() 함수이다.

1. nyfeed.xml 파일을 읽어 파싱한다.
2. item 항목의 title 과 link 내용을 출력한다.

package xml.parse;

import java.io.File;

import javax.xml.parsers.*;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * XMLParserTest
 */
public class XMLParserTest {
 
 private final String XML_FILE_PATH = "nyfeed.xml";

 /**
  * 
  * @param args
  * @throws Exception
  */
 public static void main(String[] args) throws Exception {
  XMLParserTest xpt = new XMLParserTest();
  xpt.domParseTest();
  xpt.saxParseTest();
 }
 
 /**
  * 
  * @throws Exception
  */
 public void domParseTest() throws Exception {

  System.out.println("==============================");
  System.out.println("domParseTest()");
  System.out.println("==============================");
  
  File xmlFile = new File(XML_FILE_PATH);
  
  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  DocumentBuilder db = dbf.newDocumentBuilder();
  Document doc = db.parse(xmlFile);
  
  doc.getDocumentElement().normalize();
  
  System.out.printf("Root element:%s\n", doc.getDocumentElement().getNodeName());
  NodeList itemNodeList = doc.getElementsByTagName("item");
  
  for (int s = 0; s < itemNodeList.getLength(); s++) {

   Node itemNode = itemNodeList.item(s);

   if (itemNode.getNodeType() == Node.ELEMENT_NODE) {

    Element itemElement = (Element)itemNode;
    
    NodeList titleNodeList = itemElement.getElementsByTagName("title");
    Element titleElement = (Element)titleNodeList.item(0);
    NodeList childTitleNodeList = titleElement.getChildNodes();
    System.out.printf("[title : %s]\n", ((Node)childTitleNodeList.item(0)).getNodeValue());
    
    NodeList linkNodeList = itemElement.getElementsByTagName("link");
    Element linkElement = (Element) linkNodeList.item(0);
    NodeList childLinkNodeList = linkElement.getChildNodes();
    System.out.printf("[link : %s]\n", ((Node)childLinkNodeList.item(0)).getNodeValue());
   }

  }
 }
 
 /**
  * 
  * @throws Exception
  */
 public void saxParseTest() throws Exception {
  
  System.out.println("==============================");
  System.out.println("saxParseTest()");
  System.out.println("==============================");
  
  File xmlFile = new File(XML_FILE_PATH);
  
  SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
  DefaultHandler dh = new DefaultHandler() {
   
   private boolean firstElement = true;
   private boolean inItem = false;
   private boolean inTitle = false;
   private boolean inLink = false;
   private StringBuilder characterSB;
   
   @Override
   public void startDocument() throws SAXException {
    System.out.println("startDocument");
    super.startDocument();
   }

   @Override
   public void endDocument() throws SAXException {
    System.out.println("endDocument");
    super.endDocument();
   }

   @Override
   public void startElement(String uri, String localName,
     String qName, Attributes attributes) throws SAXException {
    
    if (firstElement) {
     System.out.printf("Root element:%s\n", qName);
     firstElement = false;
    }
    
    if (qName.equals("item")) {
     inItem = true;
    } else if (qName.equals("title")) {
     inTitle = true;
    } else if (qName.equals("link")) {
     inLink = true;
    }
    
    if (inItem && (inTitle || inLink)) {
     characterSB = new StringBuilder();
    }
    
    super.startElement(uri, localName, qName, attributes);
   }

   @Override
   public void characters(char[] ch, int start, int length)
     throws SAXException {
    
    if (characterSB != null) {
     characterSB.append(handleCharacters(ch, start, length));
    }
    
    super.characters(ch, start, length);
   }

   @Override
   public void endElement(String uri, String localName, String qName)
     throws SAXException {
    
    if (characterSB != null) {
     if (inItem && inTitle) {
      System.out.printf("[title : %s]\n", characterSB.toString());
     } else if (inItem && inLink) {
      System.out.printf("[link : %s]\n", characterSB.toString());
     }
     characterSB = null;
    }
    
    if (qName.equals("item")) {
     inItem = false;
    } else if (qName.equals("title")) {
     inTitle = false;
    } else if (qName.equals("link")) {
     inLink = false;
    }
    
    super.endElement(uri, localName, qName);
   }
   

   /**
    * 
    * @param ch
    * @param start
    * @param end
    * @return
    */
   public String handleCharacters(char[] ch, int start, int length) {
    
    StringBuilder sb = new StringBuilder();
    for (int i = start; i < start + length; i++) {
     sb.append(ch[i]);
    }
    return sb.toString();
   }
  };
  parser.parse(xmlFile, dh);
 }
}


팁)
SAX 파싱 작업 중 더 이상 작업 할 필요가 없을 때는 SAXException 을 날려서 중단하면 된다.
참고 - http://www.ibm.com/developerworks/xml/library/x-tipsaxstop/
(Tip: Stop a SAX parser when you have enough data)

throw new SAXException("Enough!");