웹구간 보안을 위해 SSL을 적용하려면 인증서를 생성해서 웹서버에 설치/설정해주고 브라우저에서는 "https" 프로토콜로 접속하면 됩니다. 그러나 만약 인증서가 Verisign 등 신뢰된 기관(root CA)에서 서명된 인증서가 아니라면, 브라우저는 사용자에게 보안 경고창을 보여주어 웹페이지를 볼 건지 말건지 물어봅니다.


root CA로부터 서명된 인증서라면 이런 경고창 없이 페이지 내용이 바로 보여집니다(참고로 특정 모바일기기에서 어떤 인증기관의 인증서를 root CA로 인식하는지는 http://www.ssltest.net/ 사이트에서 테스트 가능합니다).

root CA로부터 서명을 받기 위한 비용은 대략 1년에 수 십만원이기 때문에 가난한 프로젝트 또는 개인들은 root CA 서명이 아닌 자체(self) 서명된 인증서를 사용할 수밖에 없습니다. 문제는 이렇게 셀프 서명된 인증서는 공식적으로는 신뢰할 수 없으므로 위와 같은 보안 경고창이 뜬다는 것이죠.
사실 보안 경고창이 뜬다고 해도 사용자에게 "계속" 버튼을 누르도록 교육시키면 그리 큰 문제는 아닙니다. 그러나 폰갭에서 똑같은 페이지를 불러온다면 어떤 일이 벌어질까요?

loadUrl("https://yourserver.com/some/page.html");


웁스.. 기대와 달리 그냥 텅빈 화면만 나옵니다.-.-


공백 화면이 나오는 이유는 신뢰되지 않은 인증서인 경우 android.webkit.WebViewClient에서 아래와 같이 페이지로딩 작업을 cancel시키기 때문입니다.

public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    handler.cancel();
}


자 이제 원인을 알았으니 해결해보죠.

먼저 SSL에러가 발생하더라도 페이지 로딩을 계속 진행하도록 onReceivedSslError() 메소드를 재정의한 클래스를 만듭니다.(DroidGap을 상속한 Activity의 inner 클래스로 정의)

private class MyGapViewClient extends GapViewClient {
    public MyGapViewClient(DroidGap ctx) {
        super(ctx);
    }

   @Override
   public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        handler.proceed();  //SSL 에러가 발생해도 계속 진행!
    }
}


이렇게 만든 MyGapViewClient 클래스를 onCreate()에서 아래처럼 세팅해줍니다.

public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   … …
   super.init();
   this.setWebViewClient(this.appView, new MyGapViewClient(this)); 
   … …
   loadUrl("https://yourserver.com/some/page.html");
}

 
빌드하고 앱을 다시 실행해보면 정상적으로 SSL보안이 적용된 웹페이지가 뜨는 것을 확인할 수 있습니다.
Posted by 에코지오
,
PhoneGap(폰갭)에서는 anrdoid.app.Activity.onKeyDown() 메소드를 아래와 같이 재정의하여 사용자가 하드웨어 취소키(back키)를 누르면 브라우저의 뒤로가기를 수행합니다.

// com.phonegap.DroidGap.onKeyDown(int, KeyEvent) 메소드 

public boolean onKeyDown(int keyCode, KeyEvent event) {
 ... ...
     // If back key
     if (keyCode == KeyEvent.KEYCODE_BACK) {

     // If back key is bound, then send event to JavaScript
     if (this.bound) {
     this.appView.loadUrl("javascript:PhoneGap.fireEvent('backbutton');");
     }

     // If not bound
     else {

     // Go to previous page in webview if it is possible to go back
     if (this.appView.canGoBack()) {
     this.appView.goBack();
     }

     // If not, then invoke behavior of super class
     else {
     return super.onKeyDown(keyCode, event);
     }
     }
     }
... ...


그러나 때로는 사용자가 취소키를 누르면 이전 웹페이지로 이동하지 않고 앱을 종료시키는 것으로 정책을 바꾸고 싶은 경우도 있습니다. (특히나 아이폰을 고려하여 이전버튼을 포함하도록 웹페이지를 디자인한 경우).

폰갭에서는 취소키 동작을 재정의하는 2가지 방법이 있습니다.
 
1. DroidGap의 onKeyDown 메소드 오버라이딩
DroidGap을 상속받은 Activity에서 onKeyDown 메소드를 오버라이딩해서 백버튼을 적절히 핸들링해줍니다. 아래코드에서는 앱 종료여부를 묻는 확인창을 띄우도록 했습니다.

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        confirmAppExit();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}

private void confirmAppExit() {
    AlertDialog.Builder db = new AlertDialog.Builder(this);
    db.setTitle(R.string.exit_app_title)
         .setMessage(R.string.exit_app_message) // "프로그램을 종료하시겠습니까?"
         .setCancelable(true)
         .setPositiveButton(android.R.string.ok, new OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int which) {
                 finish(); //확인버튼 누루면 앱 종료
             }
         })
         .setNegativeButton(android.R.string.cancel, null)
         .show();
}



2. 폰갭 javascript의 'backbutton' 이벤트 재정의(phonegap 0.9.5 이상)

function onLoad() {
    document.addEventListener("backbutton", backKeyDown, true);
}
function backKeyDown() {
    if (confirm('프로그램을 종료하시겠습니까?')) {
        navigator.app.exitApp();
    }
}
...
...
<body onload="onLoad()">


단순히 취소키 눌렀을 때 페이지 뒤로가기를 막으려면 아래코드로 충분합니다.

navigator.app.overrideBackbutton(true);


Posted by 에코지오
,
제가 요즘 폰갭(PhoneGap)을 이용하여 하이브리드 형태의 안드로이드앱을 개발하고 있습니다. 폰갭은 하이브리드앱(웹앱) 개발 프레임워크 중 하나인데요, 웹페이지에서 폰의 다양한 기능/자원을 불러다 쓸 수 있게 해줍니다. 폰갭에서 이러한 기능들은 플러그인(plugin)이라 불리는 것으로 제공됩니다.

폰갭 플러그인 구분
폰갭 플러그인은 앱영역(java)에서 플러그인이 실행되는 방식에 따라 동기식 플러그인과 비동기식 플러그인으로 나눌 수 있습니다. 좀더 자세히 설명하자면, 웹영역(webview)에서 javascript 코드를 통해 플러그인을 호출한 후 플러그인의 실행이 완료되기를 기다리면서 블럭되면 동기식이고 그렇지 않으면 비동기식입니다.

폰갭 플러그인을 제작하려면 com.phonegap.api.Plugin 클래스를 상속받아야 하는데, 이 Plugin 클래스에 isSynch() 메소드가 있습니다. 기본적으로 false를 리턴합니다. 그러니까 이 메소드를 오버라이딩해서 true를 리턴하지 않는 이상 해당 플러그인은 비동기식으로 작동합니다.
비동기식 플러그인의 경우 플러그인 실행결과를 수신(콜백함수 호출)하기 위해 XMLHttpRequest를 이용하거나 또는 Polling 방식을 사용합니다.


 
동기식 플러그인 실행메커니즘


1. Javascript로 "window.plugins.플러그인명.액션명(...)"과 같이 플러그인 함수를 호출하게 되면, phonegap.js 내부적으로 PhoneGap.exec() 함수가 실행됩니다. PhoneGap.exec()에서는 prompt 함수를 실행하면서 플러그인 실행에 필요한 파라미터를 넘겨줍니다.

var r = prompt(xxx, "gap:xxx");


Javascript의 prompt() 함수가 호출되면 앱영역에서는 DroidGap$GapClient.onJsPrompt() 메소드가 실행됩니다. (아하, 폰갭은 Javascript -> Java 호출을 위해 prompt()를 활용하는군요.)

2. DroidGap$GapClient.onJsPrompt() 메소드에서는 prompt의 default value가 "gap:xxx" 형태의 문자열인 경우 PluginManager.exec(...) 메소드를 실행합니다. PluginManager.exec()에서는 아래와 같이 플러그인을 실행하고 그 결과를 JSON 스트링으로 리턴합니다.

// 우선 Plugin이 동기식인지 아닌지 판단 : Plugin.isSynch()
// 동기식인 경우 current thread에서 플러그인을 실행
// (아래 코드는 원래 코드를 단순화한 것입니다)

PluginResult cr = plugin.execute(xxx);
return cr.getJSONString();


3. DroidGap$GapClient는 PluginManager로부터 리턴받은 플러그인 실행결과 스트링을 prompt의 confirmation 응답으로 설정합니다.

jsPromptResult.confirm(플러그인 실행결과);


4. 비로소 웹영역에서는 앱영역에서의 플러그인 실행결과를 prompt 함수의 리턴값(var r 변수)으로 받아냈습니다. 동기식 플러그인의 경우 r 리턴문자열이 반드시 존재하며 r.status에 따라 success 또는 failure 콜백 함수를 호출합니다. 폰갭은 (일단 동기식 플러그인에 대해서) Java -> Javascript 콜백 호출을 위해 prompt 리턴값을 활용함을 알 수 있습니다.


비동기 플러그인 실행메커니즘(XMLHttpRequest 적용시)


1. com.phonegap.CallbackServer는 java.net.ServerSocket을 이용하여 간단한 폰내부 로컬 웹서버를 띄웁니다. 이 웹서버는 웹영역의 XMLHttpRequest(XHR) 요청에 응답합니다.

2. phonegap.js의 PhoneGap.JSCallback 함수에서 로컬 웹서버와 ajax 통신하기 위한 연결을 맺습니다.

xmlhttp.open("GET", "http://127.0.0.1:xxx/xxx", true);
xmlhttp.send(); 


이 커넥션은 오랫동안 살아있는(long-lived) 연결이며, CallbackServer는  연결을 계속 유지하기 위해 10초간격으로 비어있는 응답을 계속해서 송신합니다(callback ping).

3. (동기식 플러그인과 동일하므로 설명생략)

4. PluginManager.exec()에서는 아래와 같이 비동기 플러그인의 경우 별도의 쓰레드를 만들어서 플러그인을 실행하고 그 결과를 CallbackServer에 적재합니다.

// 우선 Plugin이 동기식인지 아닌지 판단 : Plugin.isSynch()
// 비동기식으로 정의된 경우 새로운 thread에서 플러그인을 실행
// (아래 코드는 원래 코드를 단순화한 것입니다)

new Thread(new Runnable() {
    public void run() {
       PluginResult cr = plugin.execute(xxx);
       ....
       ctx.sendJavascript(xxx);
    }
});
thread.start();
return ""; // 플러그인 실행이 끝날때까지 기다리지 않고 즉시 리턴

5. CallbackServer는 새로운 플러그인 실행결과(javascript문자열)가 공급되면 그것을 기존에 연결된 XHR에 응답으로 씁니다. 

// (아래 코드는 원래 코드를 단순화한 것입니다)
response = "HTTP/1.1 200 OK\r\n\r\n";
String js = this.getJavascript();
response += URLEncoder.encode(js, "UTF-8");
output.writeBytes(response); 


6. 앞서 콜백서버와 연결된 xmlhttp 자바스크립트객체는 응답으로 수신한 문자열을 통해 콜백 함수를 호출합니다(PhoneGap.JSCallback 함수 참조).

// (아래 코드는 원래 코드를 단순화한 것입니다)
if (xmlhttp.status === 200) {
    var t = eval(xmlhttp.responseText);
}



비동기 플러그인 실행메커니즘(Polling 적용시)


1. phonegap.js에서는 PhoneGap.JSCallbackPolling 함수를 주기적으로 실행합니다(기본 50ms 마다).

var msg = prompt("","gap_poll:");


2 ~ 3. (XHR 적용 비동기식 플러그인과 동일하므로 설명생략)

4. 폴링에 의해 호출된 DroidGap$GapClient.onJsPrompt() 메소드에서는 CallbackServer에 적재된 javascript문자열을 prompt의 confirmation 응답으로 설정합니다.

5. prompt 함수의 리턴값(var msg 변수)이 존재하면 그것을 evaluation하여 콜백 함수를 호출합니다.

XHR을 적용할 건가 Polling을 적용할 건가
폰갭은 비동기식 플러그인 실행결과를 수신하기 위해 default로 XMLHttpRequest를 이용합니다. 그러나 외부 웹서버로부터 로딩된 html에서는 결과를 수신하지  못하는 문제가 있습니다. 이는 cross domain 보안제약 때문이며, 안드로이드 WebView가 강제로 Ajax 응답연결을 끊어버려 javascript 콜백함수가 실행되지 않게 됩니다(file:// 주소에 의해 로딩된 html에는 이런 제약이 없습니다). 이 경우 XHR 방식 대신 Polling 방식을 적용해야 합니다.
즉, file:// 주소에 의해 로딩된 웹페이지가 아닌 경우 Cross-Domain Security 제한이 존재하므로 phonegap.js에서 PhoneGap.UsePolling 값을 true로 수정해주어야합니다.

PhoneGap.UsePolling = true;

 
Polling 방식 적용시 주의점
그러나 폴링방식 적용시 폰갭은 비동기식 플러그인의 실행결과를 수신하기 위해 매우 짧은 주기로 App 영역과 통신하므로, 폰갭 기능이 필요하지 않은 웹페이지에서는 phonegap.js를 포함하지 않는 것이 좋습니다. 이 문제는 향후 어떻게든 개선될 것으로 보입니다.

ps. 위 내용은 폰갭 0.9.5 버전을 분석한 것입니다.
 
Posted by 에코지오
,