2016년 11월 7일 월요일

Node.js 웹 애플리케이션을 Heroku에 Deploy 하기

간단한 API 서버를 하나 구축해야 해서 일단 Node.js로 만들고 heroku를 이용해서 서비스를 띄워보려고 하는데…

해보니 정말 간단해서 깜짝 놀랐다. 그냥 사이트에 나와 있는데로 따라해보니 2~3분도 안걸린다.

우선 사전에 Node.js와 git이 로컬환경에 설치되어 있어야 하며 Path도 다 잡혀 있어야 한다.

1. Node.js로 구동되는 Express 앱을 하나 만든다.

eclipse에서 nodeclipse 플러그인을 이용하거나 IntelliJ 또는 WebStorm으로 새 프로젝트 만들기를 하여 Node.js(Express) 프로젝트를 선택하면 금방 간단한 껍데기 앱이 만들어진다. Express 앱 생성 후 해당 프로젝트 디렉터리에 가서 npm install 명령어를 실행하여 필요한 모듈들을 설치하고 그 후 npm start 명령어 실행…

2. http://localhost:3000 접속해서 위와 같이 뜨면 일단 문제없이 되었다.

3. Express 앱의 루트 디렉터리에 Procfile 이라는 이름의 파일을 생성하고 내용으로 web: node ./bin/www 을 입력해놓고 저장.

web: 다음 내용은 heroku 서버에서 앱을 Deploy 할 때 수행되는 명령어이다. forever나 2pm을 사용해서 돌린다면 얘네들을 이용해서 돌리는 명령어를 집어넣으면 될 듯…

4. heroku 상에서 Deploy 할 때, package.json 파일의 내용을 가지고 필요한 라이브러리들을 설치하므로, 앱 상에서 필요한 라이브러리는 package.json 상에 의존성이 정확히 적시되어 있어야 한다.

로컬 개발환경에서 전역설치해놓으면 package.json에 적시 안되어 있어도 앱은 잘 돌아가는 경우가 있어, 이 부분을 주의.

앱 소스 디렉터리에서 npm 라이브러리를 설치할 때 항상 npm install 라이브러리_명 -save 명령어로 설치해놓는다.

5. Express 앱은 일단 그대로 냅두고 heroku.com 에 접속하여 Sign up을 클릭하여 계정을 만든다.

6. 계정 만들고 로그인하면 위와 같이 대쉬보드로 접속되고 화면 오른쪽 상단의 New 버튼을 클릭하여 앱을 생성한다. 나는 앱을 두 개 만들어 놨다.

앱은 heroku 전용 CLI로 만드는 방법이 일반적인 것 같은데 나는 UI쓰는게 편해서 생성은 그냥 웹사이트 상에서 했다.

앱 생성 시에 입력해야 할 항목은 단 두 개인데 앱 이름과 서버 위치이다. 앱 이름은 소문자만 되고 서버는 유럽과 미국 두 군데 중 한 곳을 고르면 된다.

7. 앱을 생성하고 나서 대쉬보드에서 해당 앱 항목을 클릭해 Deploy 탭을 선택하면 어떻게 Deploy 하는지 친절하게 설명되어 있다.

그냥 이 페이지에 나와 있는대로 따라서 하면 올린 소스에 문제가 있지 않는 한 바로 된다.

대략 원리는 heroku에서 제공하는 Git Repository를 remote로 잡고 로컬에 있는 소스를 push하면 pipeline을 통해 소스 전송받고 프레임워크 식별한 다음 자동으로 Deploy하여 띄우는 것 같다.

8. https://devcenter.heroku.com/articles/heroku-command-line 에서 heroku 전용 CLI툴을 사용 운영체제에 맞는 것을 골라 설치한다.(여기서는 윈도우즈10)

9. 별도의 CLI 콘솔 프로그램을 구동시켜야 하는 줄 알았더니 그냥 윈도우즈의 프롬프트 창에서 heroku 배치파일 명령어를 실행하는 것으로 구현한다. heroku login 명령어를 입력해서 heroku.com의 계정정보를 입력하여 로그인 세션 생성.

10. heroku apps 명령어로 생성해 둔 앱 목록을 확인한다.

11. 예전 1번에서 만들어 놓은 Express 앱 소스 디렉터리로 들어가서 git init 명령어로 로컬 Repository를 생성하고 나서 heroku git:remote -a heroku에_생성해놓은_앱_이름 명령어로 heroku.com의 Git Repository를 원격 Repository로 설정한다.

12. git add ., git commit -am "init commit" 명령어로 로컬 Repository에 소스를 커밋.

13. 커밋하고 나서 git push heroku master 명령어로 heroku의 원격 Repository에 push하면 소스가 전송되면서 자동으로 프레임워크를 탐지하고 Deploy되는 과정이 로그로 쫘악~ 펼쳐진다.

14. https://앱_이름.herokuapp.com 으로 접속해보면 잘 뜬다.

3000포트를 URL뒤에 붙여줘야 하나? 생각했는데, 이것도 알아서 웹 서버 상에서 80포트로 서비스 되도록 설정을 해주는 듯…

이렇게 해놓고 보니 참 쉽고 생산성 부분에 대해서는 다시금 감탄하게 되는데 아무래도 정식 서비스로 사용하기에는 부족한 듯하다. 일단 서버가 해외에 있는 탓인지 내가 올려놓은 Node 소스에는 국내 서버와 I/F하는 부분이 있는데 좀 느린 것 같다.

웹 서버 말고도 PostgreSQL DB도 호스팅하고 있길래 DB계정도 하나 만들어봤는데 사용 못할 정도로 느리다. 그리고 접속을 잘 안하고 있으면 웹 서버가 Sleep 모드로 되서, 간만에 재접속하려고 하면 깨우느라(?) 응답에 조금 시간이 걸린다.

결국 쓸만하게 만드려면 유료로 전환해야 할 듯… 그래도 프로토타입이나 테스트 용도로 쓰기에는, 매우 괜찮아 보인다.

2016년 10월 18일 화요일

Filter를 사용한 XSS(Cross-Site Scripting) 처리

Spring 4로 간단한 개인 블로그 툴을 만들고 있다…

CKEditor source 모드에서 저렇게 하고 저장을 누르니

이런…

Cross-Site Scripting 처리를 해줘야 하는데 예전 직장에 있었을 때는 <script>&lt;script&gt; 따위로 바꿔주는 메서드를 만들어서 인자값을 받아 insert 혹은 update 수행하는 비즈니스 로직에 일일히 집어넣어서 처리했었으나… 그건 좀 단순무식한 방법 같고 이번에는 필터로 구현해보기로 한다.

우선 검색을 해보니 네이버에서 만든 lucy-xss-filter 라는 라이브러리가 꽤 유용한 듯하다.

이 라이브러리를 적용해서 HttpServletRequest 객체를 통해 받아오는 인자들을 Escape처리 하는 것으로 한다.

HttpServletRequest 객체에서 parameter 값들을 가져오는 것은 가능한데 이 값들을 바꿔서 동일한 key에 다시 셋팅하려면 어떻게 해야 하는가?

이럴 때는 ServletRequest 인터페이스를 참조하는 javax.servlet.http.HttpServletRequestWrapper 클래스를 사용하면 된다.

정확히는 HttpServletRequestWrapper 클래스의 확장 클래스를 생성하고… getParameter, getParameterValues, getHeader 등 값을 가져오는 메서드를 오버라이드하여 원 데이터에 필터를 적용한 값을 리턴하도록 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import com.nhncorp.lucy.security.xss.XssFilter; 
import com.nhncorp.lucy.security.xss.XssPreventer; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletRequestWrapper; 
import java.util.Arrays; 
 
public class RequestWrapperForXssFiltering extends HttpServletRequestWrapper { 
    public RequestWrapperForXssFiltering(HttpServletRequest httpServletRequest) { 
        super(httpServletRequest); 
    } 
    
    @Override 
    public String[] getParameterValues(String name) { 
        String[] values = super.getParameterValues(name); 
        if(values == nullreturn null
        boolean doPreventer = getPreventerFlag(name); 
        return Arrays.stream(values).map(v -> doFilter(v, doPreventer)).toArray(String[]::new); 
    } 
    
    @Override 
    public String getParameter(String name) { 
        String value = super.getParameter(name); 
        if(value == nullreturn null
        return doFilter(value, getPreventerFlag(name)); 
    } 
    
    @Override 
    public String getHeader(String name) { 
        String value = super.getHeader(name); 
        if(value == nullreturn null
        return doFilter(value, true); 
    } 
    
    private String doFilter(String value, boolean doPreventer) { 
        if(doPreventer) { 
            return XssPreventer.escape(value); 
        } else { 
            XssFilter xssFilter = XssFilter.getInstance("lucy-xss-config.xml"); 
            return xssFilter.doFilter(value); 
        } 
    } 
    
    private Boolean getPreventerFlag(String name) { 
        if(name == nullreturn true
        return !name.toLowerCase().startsWith("content")?true:false
    } 
}
cs

우선 lucy-xss-filter 라이브러리의 퀵 가이드를 참고하여 인자값을 Escape처리하여 그 결과값을 리턴하는 doFilter 라는 메서드를 만든다.

이 라이브러리는 크게 Lucy-XSS Filter와 Lucy-XSS Preventer 두 가지 기능을 제공하는데 단순 문자열에 대해서는 Preventer를 적용하여 전체 값을 바꿔주고 글 본문과 같이 HTML태그의 기능이 적용되어야 하는 내용은 Filter를 사용하여 스크립트 실행 같은 것들만 선별적으로 필터링되도록 구현해야 한다.

그래서 getPreventerFlag 메서드로 parameter명이 ‘content’로 시작하는 녀석은 HTML태그가 적용되어야 하는 것으로 간주하여 false와 true를 리턴하도록 하고 이 flag에 따라 Preventer 또는 Filter가 적용되도록 했다. ‘content’로 박아넣은 하드 코딩이 좀 마음에 안들지만…

어차피 내 개인 프로젝트니 넘어간다.

그리고 XSS 처리한 값을 리턴해줘야 할 필요성이 있는 getParameter 같은 메서드들을 오버라이딩하여 doFilter 메서드를 이용해 Escape된 값이 리턴되도록 구현한다. (여기에서는 getParameter, getParameterValues, getHeader에 적용)

Lucy-XSS Filter는 XML파일로 정의된 필터링 정책에 의거하여 구동되는데 XML파일의 설정에 대해서는 레퍼런스 가이드를 참고한다. 내 경우는 lucy-xss-superset.xml, white-url.xml 파일과 lucy-xss.xml 파일을 lucy-xss-config.xml라는 이름으로 바꾸어 이 3개 파일을 루트 디렉터리에 넣고 소스 상에서 lucy-xss-config.xml 파일을 읽어들이도록 했다. XML 파일 경로가 잘못되었거나 없으면 라이브러리 내 기본 설정을 읽어들이는데 필터링 레벨이 좀 낮다.

그리고 필터 클래스를 만들어 줌. Spring을 사용하기 때문에 Spring의 OncePerRequestFilter 클래스를 상속받아 만든다. HTTP method가 GET이나 POST일 경우에만 XSS Filter가 적용되도록 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.web.filter.OncePerRequestFilter; 
import javax.servlet.FilterChain; 
import javax.servlet.ServletException; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 
import java.io.IOException; 
 
public class CrossSiteScriptingFilter extends OncePerRequestFilter { 
    
    @Override 
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { 
        if(httpServletRequest.getMethod().equals("GET"|| httpServletRequest.getMethod().equals("POST")){ 
            filterChain.doFilter(new RequestWrapperForXssFiltering(httpServletRequest), httpServletResponse);
        } else { 
            filterChain.doFilter(httpServletRequest, httpServletResponse); 
        } 
    } 
}
 
cs

만든 필터 클래스를 web.xml에 아래와 같이 추가하고,

서버 리부트 후 스크립트를 포함하여 테스트 입력하면 아래와 같이 필터링되어 스크립트가 동작하지 않고 잘 들어가 있는 것을 확인할 수 있다.

2016년 2월 20일 토요일

OSX 엘 캐피탄에서 iPython Notebook 설치

Python을 활용한 데이터 분석에 관련된 강의를 주말마다 수강하고 있는데, iPython Notebook이라는 모듈 설치시 에러가 나서 이를 해결하는데 조금 애를 먹었다. OSX 최신 버전인 엘 캐피탄의 경우 발생하는 에러인데 나중에 가면 까먹을 것 같아 여기에 간단히 기록해둔다.

1
pip install "ipython[notebook]" 
cs

엘 캐피탄에서 위 명령어로 설치 시도 시 아래와 같이 오류가 발생한다.

우선 아래 명령어로 Numpy, Pandas, Matplotlib 등을 사전에 설치한다. pip로도 설치가 되긴 하지만, 나의 경우는 위와 같이 빨간글자 로그가 주르륵 떴다.

1
2
3
4
sudo easy_install numpy
sudo easy_install pandas
sudo easy_install matplotlib
sudo easy_install ipython
cs

위 명령어보다는 아래의 절차 처럼 homebrew를 이용하여 Python을 재설치한 다음 평범하게 pip로 설치하면 된다. easy_install을 사용해도 ipython 설치 시에는 오류가 발생했다.

찾아보니 뭔가 homebrew를 통한 방법이 있는 것으로 보인다.

구체적으로는 아래 웹사이트의 내용이 참고가 되었다. 

아래 명령어로 /usr/local 디렉터리에 퍼미션을 조정한다.

1
sudo chown $(whoami):admin /usr/local && sudo chown -R $(whoami):admin /usr/local
cs

그 다음 homebrew를 설치하는 명령어를 입력한다.

1
/usr/bin/ruby -"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
cs

그리고 나서 homebrew를 사용해 Python을 재설치한다.

1
brew install python
cs

다시 한번 명령어를 날려보면,

1
sudo pip install "ipython[notebook]"
cs

설치가 잘 된다…

참고로 나는 이것 외에 ‘brew doctor’, ‘brew install Caskroom/cask/jupyter-notebook-ql’ 등의 명령어로 날려보았는데, 해결책과는 거리가 먼 액션인 것 같다.

왜 하필 엘 캐피탄에서만 이런 이슈가 발생하는지는 내장 Python을 사용할 경우 보안설정으로 인해 잘 작동하지 않을 수도 있다고 한다.

Kotlin, SpringBoot 3, GraalVM 환경에서 Native Image로 컴파일하여 애플리케이션 실행

Spring Boot 3부터, GraalVM Native Image를 공식 지원하여 애플리케이션의 시작 속도와 메모리 사용량을 크게 줄일 수 있다. Native Image란 기존의 JVM 기반 위에서 돌아가는 Java 애플리케이션과는 달리 JVM 없이...