2013. 7. 24. 17:30

mongoose와 visual studio c++를 가지고 web application 만들기


source.zip


(바로 결과를 보고 싶으시다면, 위 source.zip의 압축을 해제한뒤, 본 블로그 포스트의 '무엇을 구현할 것인가?'와 '테스트' 쪽을 참고하시기 바랍니다.)


최근 ntfs forensic tool을 만들려고 하고 있습니다.

visual studio에서 하려다 보니 걱정되는것중 하나가 바로 ui입니다.

mfc등 도구가 있음에도 불구하고 까다롭기는 마찬가지 입니다.

그래서, web application으로 만들어 볼까 합니다.

이때 고려해야 하는것이,

- http web server 구현 / 오픈 소스 이용

- html/css/javascript 구현

- cgi / php / python / ... 과 같은 interaction 방식

- ...

이 있을듯 합니다.


일단 http web server는 당연히 구현하지는 않고 오픈 소스를 이용하였습니다.

바로 mongoose[링크]입니다. 가볍고 cross-platform이 지원되니 향후 linux에서도 쉽게 작업이 이뤄질 듯 합니다. 그리고 중요한건 license가 MIT라서, 사용에 제약이 없습니다.

(주의 : 현재 mongoose는 GPL or commercial입니다. 라이센스 이슈가 있다면, mongoose mit fork를 사용해야 할 듯 한데, 자세한건 2014/06/23 13:55 싯점의 댓글을 확인하세요)


mongoose에 대한 예제나 강의는 풍부하진 않았지만, 사용에 어려움이 없을 정도로 간단하였습니다.

단지, callback 함수에서 user context data를 구하는 방법이 없어, 소스 코드를 약간 수정하였는데, 이는 향후 다시 설명하겠습니다.


기본 프로젝트 생성 (MFC)

우선 mfc를 가지고 기본 프로젝트를 생성해 보도록 하겠습니다. 물론, mfc 사용없이도 가능한 내용입니다.


1) Dialog based로 생성

2) 다음과 같이 design (중간은 list box, CTRL 누른채 더블클릭하여 m_wndList로 연결)


mongoose 빌드

mongoose 소스를 다운로드한뒤, 다음과 같이 프로젝트 경로에 넣습니다.

mongoose.3.8 경로는 다음과 같습니다.

즉, mongoose.3.8 경로 하부에 mongoose.c / mongoose.h 두개 파일만 있어도 무방합니다.

그런다음, 다음과 같이 project에 파일을 추가합니다.

그다음, mongoose.c의 등록정보에 들어가서,

와 같이 precompiled header 설정을 off합니다.

이정도의 작업이였으면 빌드가 성공될 것입니다.

여태껏 open source들 중 Visual studio에서 가장 간편하지 않았나 싶습니다.


무엇을 구현할 것인가?

우선 어떤것을 구현할 것인지 부터 결정하고 진행하겠습니다.

우선, web application 으로 개발할 것이기 때문에,

- 모든 것은 browser를 통해서 접근한다.

- 로컬 접속인 경우, http://127.0.0.1 와 같이 시작한다.

- port는 8081로 한다.

- http://127.0.0.1:8081/a.html 과 같은 일반 web server를 지원한다.

- http://127.0.0.1:8081/api/xxxxxx 와 같은 REST를 지원한다. GetCurrentTime을 샘플로 만들어 본다.

- http://127.0.0.1:8081/aaa.cgi.html?arg1=1&arg2=2 와 같은 cgi 개발을 지원한다. cgi는 웹서버 프로세스에서 쓰레드로 실행된다. 즉, 별다른 프로세스가 생성되지 않는다.

- cgi를 실행하는 조건은 파일의 확장자가 .cgi.html인 경우로 가정한다.

- cgi 프로그램은 django나 php 같은 template언어로 구성된 html 형식이다.
즉, aaa.cgi.html가 다음과 같다고 가정하자.

<html>

<body>

cgi called.<br/>

[*<font color=red>the sum</font> is sum(v111,v22).*]

</body>

</html>

만일, aaa.cgi.html?arg1=1&arg2=2와 같이 호출하면,

<html>

<body>

cgi called.<br/>

<font color=red>the sum</font> is 3.

</body>

</html>

과 같이 처리될 것이다.

등을 지원할 예정입니다.


mongoose web server 시작 하기

Dialog based이기 때문에 main으로 사용되는 dialog.h가 있을 것입니다.

해당 파일에 들어가 아래와 같이 추가하면 됩니다.

// ntfsforensicserverDlg.h : header file
//

#pragma once
#include "afxwin.h"
#include "mongoose.3.8/mongoose.h"

// CntfsforensicserverDlg dialog
class CntfsforensicserverDlg : public CDialog
{
// Construction
public:
	CntfsforensicserverDlg(CWnd* pParent = NULL);	// standard constructor

// Dialog Data
	enum { IDD = IDD_NTFSFORENSICSERVER_DIALOG };

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV support

// Implementation
protected:
	HICON m_hIcon;

protected:
	// All about mongoose
	struct mg_context* m_pstMgCtx;

위와 같이 멤버 변수가 추가되었으니, dialog.cpp에서 아래와 같이 생성자에서 초기화 해줍니다.

CntfsforensicserverDlg::CntfsforensicserverDlg(CWnd* pParent /*=NULL*/)

: CDialog(CntfsforensicserverDlg::IDD, pParent)

{

m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);

m_pstMgCtx = NULL;

}

...

이제 위 dialog에서 Start 버튼의 OnBnClick 함수에서 다음과 같이 코딩합니다.

void CntfsforensicserverDlg::OnBnClickedButtonStart()

{

struct mg_callbacks stCallback = {0,};


const char *options[] = {

"listening_ports", "8081",

"num_threads", "10",

"document_root", ".\\html",

NULL

};


stCallback.begin_request = CntfsforensicserverDlg::begin_request_handler;


if (NULL != m_pstMgCtx)

{

AfxMessageBox(L"Already running.");

return;

}


m_pstMgCtx = mg_start(&stCallback, this, options);

if (NULL != m_pstMgCtx)

{

m_wndList.InsertString(0, L"Started");

}

else

{

m_wndList.InsertString(0, L"Failed to start");

}

}

...

이런 상태에서 빌드를 하게 되면, 당연히 begin_request_handler가 없다고 link 오류가 발생하게 됩니다. 

우선, mg_start 함수를 통해 web server가 동작하게 됩니다. 그 전에 option 부분도 알아둘 필요가 있습니다. 그리고, 각 event를 만들어, 이벤트 처리 루틴을 넣을수 있습니다. 여하튼, 이와 같이 웹서버 시작은 손쉽게 해결됩니다.


mongoose의 event handler 함수 추가하기

다음과 같은 함수를 추가합니다.


int CntfsforensicserverDlg::begin_request_handler(struct mg_connection *conn)

{

struct mg_request_info* pstRequestInfo = NULL;

INT nLength = 0;


if (NULL == conn)

{

goto FINAL;

}


pstRequestInfo = mg_get_request_info(conn);

if (NULL == pstRequestInfo)

{

goto FINAL;

}


if ((NULL != pstRequestInfo->uri) && ('\0' != pstRequestInfo->uri[0]))

{

nLength = strlen(pstRequestInfo->uri);


if (nLength > 5)

{

if (0 == strncmp(pstRequestInfo->uri, "/api/", 5))

{

// /api/로 들어온 경우

// api를 실행한다.

DispatchApi(conn, &pstRequestInfo->uri[5]);

return 1;

}

}


if (nLength > 9)

{

if (0 == strcmp(&pstRequestInfo->uri[nLength-9], ".cgi.html"))

{

// 확장자가 .cgi.html 이면

// cgi를 실행한다.

DispatchCgi(conn);

return 1;

}

}

}

FINAL:

return 0;

}

와 같이 추가합니다.

본 event 함수는 ui가 실행되는 primary thread가 아니라 worker thread에서 동작함을 주의하시기 바랍니다. 그래서 공유 변수등의 사용을 주의해야 합니다.

우선, mg_get_request_info를 통해 request 정보를 구해옵니다. 해당 request 정보는 요청된 uri나 GET/POST와 같은 method같은 값들을 알려줍니다. 우선, 요청 uri가 /api/로 시작하면 DispatchApi를 호출하고, 사용된 확장자가 .cgi.html이라면 DispatchCgi를 호출합니다. 그때는 이벤트 핸들러 내부에서 직접 응답 코드를 넣기 때문에 return 1을 해주고 있습니다. 만일 일반적인 a.html 같은 요청은 이런 해당 사항에 포함되지 않아 return 0를 해주는데, 이렇게 되면, mongoose의  기본 작업은 진행하게 됩니다.

여기에서 주의할 것은, 앞선 mg_start에서 user data를 전달하였는데, 이벤트 핸들러에서 해당 user data를 받을 방법이 마땅하지 않다는 겁니다.

물론, conn->ctx->user_data를 사용할 수 는 있습니다. 그런데, 다음과 같은 컴파일 오류가 발생합니다.

...

... error C2027: use of undefined type 'mg_connection'

... error C2227: left of '->ctx' must point to class/struct/union/generic type

...

살펴보면, struct mg_context가 mongoose.h가 아닌 mongoose.c에 정의되어 있고, 나름 좀 struct간 소유관계가 꼬여 있습니다. (되도록이면 struct등의 정의는 .h를 사용합니다.)

mongoose가 mit license이니 좀더 자유롭게 소스 수정이 가능하니, 다음과 같이 함수를 추가하면 해결됩니다.

<mongoose.h>


...

//  >0  number of bytes written on success

int mg_websocket_write(struct mg_connection* conn, int opcode,

                       const char *data, size_t data_len);


// greenfish added

void* mg_greenfish_get_user_data(struct mg_connection* conn);

...


<mongoose.c>


...

void* mg_greenfish_get_user_data(struct mg_connection* conn)

{

if (NULL == conn) return NULL;

if (NULL == conn->ctx) return NULL;

return conn->ctx->user_data;

}

이런다음 event hander등에서 mg_greenfish_get_user_data(conn)하면 user data를 받을 수 있습니다. 물론 compile도 잘 됩니다 !!!


REST api 만들기

이제 REST api를 만들어 보겠습니다.


VOID CntfsforensicserverDlg::DispatchApi(struct mg_connection *conn, const char* lpszApiName)

{

struct mg_request_info* pstRequestInfo = NULL;


if ((NULL == conn) || (NULL == lpszApiName))

{

goto FINAL;

}


pstRequestInfo = mg_get_request_info(conn);

if (NULL == pstRequestInfo)

{

goto FINAL;

}


if (0 == strcmp("GetCurrentTime", lpszApiName))

{

char buf[64] = {0,};

struct tm newtime = {0,};

__int64 ltime = 0;


// 만약 현재 시각을 요청한 경우,...

_time64(&ltime);

_localtime64_s(&newtime, &ltime);

asctime_s(buf, 64, &newtime);


// 현재 시각을 응답한다.

mg_printf(conn, "current time : %s", buf);


goto FINAL;

}


// 아직 정의되지 않은 api

mg_printf(conn, "api not defined : %s", lpszApiName);


FINAL:

return;

} 

만일 요청된 api 이름이 GetCurrentTime이라면 현재 시각을 전달합니다.

경우에 따라 HTTP 헤더를 사용할 수 있는데(이게 보다 일반적일듯),

다음과 같이 header를 추가해야 합니다.

mg_printf(conn, "current time : %s", buf);

==>

mg_printf(conn,

"HTTP/1.1 200 OK\r\n"

"Content-Type: text/html\r\n"

"Content-Length: %d\r\n"

"\r\n"

"%s",

strlen(buf),

buf);

위와 같은 경우, current time : 은 전달하지 않고, 순수히 현재 시각만 http로 전달하게 됩니다.

이와 같이 REST api 생성은 생각보다 어렵지 않습니다.

다만 php나 django와 같은 template html을 만드는 것이 관건인데, 이는 다음 cgi 처리에서 설명됩니다.


cgi 처리하기


VOID CntfsforensicserverDlg::DispatchCgi(struct mg_connection *conn)

{

// A2W, W2A 사용을 위함.

// MFC 사용을 하지 않으려면, WideCharToMultiByte를 직접 호출해야 한다.

// 그리고, A2W, W2A 자체에 문제가 있기 때문에,

// 되도록이면 USES_SMART_CONVERSION;

// 을 사용한다.

// 해당 부분은

// http://greenfishblog.tistory.com/132

// 를 사용한다.

USES_CONVERSION;


struct mg_request_info* pstRequestInfo = NULL;

HANDLE hFile = INVALID_HANDLE_VALUE;

char szBuffer[65535+2] = {0,}; // 주의 : .cgi.html 파일의 최대 크기는 64K로 한다.

char szArgValue[64+1]= {0,};

DWORD dwReaded = 0;

INT nStart = 0;

INT nFinish = 0;

INT nFunctionStart = 0;

INT nFunctionFinish = 0;

INT nComma = 0;

INT nSum = 0;


// String 변환 관련하여 MFC인 CString을 사용하였다.

// 아래 CString을 사용하지 않고 개발하면, MFC 의존성은 없어진다.

// 개발 가이드를 위함이므로,

// 아래 내용들에 대한 코드 최적화등등의 부분은 고려하지 않는다.

CString strPath;

CString strBuffer;

CStringA strBufferA;

CString strMid;

CString strMidExchange;

CString strSum;

CStringA strArg1;

CStringA strArg2;


if (NULL == conn)

{

goto FINAL;

}


pstRequestInfo = mg_get_request_info(conn);

if (NULL == pstRequestInfo)

{

goto FINAL;

}


// file 경로를 구한다.

// 귀찮아서 CString을 사용한다.

// 일반 string 연산을 이용하면, MFC 의존성을 없앨 수 있다.

strPath = L".\\html";

strPath += pstRequestInfo->uri;

strPath.Replace(L'/', L'\\');


hFile = ::CreateFile(strPath, 

GENERIC_READ, 

FILE_SHARE_READ | FILE_SHARE_WRITE, 

NULL, 

OPEN_EXISTING, 

FILE_ATTRIBUTE_NORMAL, 

NULL);

if (INVALID_HANDLE_VALUE == hFile)

{

mg_printf(conn, "Fail to open, %s, errcode=%d", W2A_CP(strPath, CP_ACP), ::GetLastError());

goto FINAL;

}


// 파일을 다 읽는다.

if (FALSE == ::ReadFile(hFile, szBuffer, 65535, &dwReaded, NULL))

{

mg_printf(conn, "Fail to read, %s, errcode=%d", W2A_CP(strPath, CP_ACP), ::GetLastError());

goto FINAL;

}


// UTF8로 된 파일을 WCHAR로 변환한다.

strBuffer = A2W_CP(szBuffer, CP_UTF8);


우선, request uri를 가지고 실제 경로를 구하는 과정이 있습니다. 즉, /aaa.cgi.html이 request되었다면, 해당 경로를 가지고, .\html\aaa.cgi.html를 구하는 것입니다. Win32 API인 CreateFile은 .\으로 시작하는 경로는 current directory로 인정하고 읽어주더군요. 그다음, 파일을 한꺼번에 다 읽고, 해당 내용을 WCHAR로 변환합니다. 사실은 해당 파일의 BOM을 읽어, UTF-8이라면 변환해 주는 것을 해주면 더욱 좋을 듯 합니다.(2010/12/15 - [프로그래밍/Win32] - WritePrivateProfileString unicode encoding 지원하기 참고). 그렇기 때문에 html이나 다른 파일들을 저장할때 UTF-8 형식으로 저장하시기 바랍니다. 일단, html 파일을 그대로 한꺼번에, 한번에, 그리고 하나의 버퍼에 읽고 있습니다. 썩 좋은 방법은 아닌 것으로 보여지네요. 일단, CStringA를 가지고 String 연산을 하면 문제가 있을 듯 합니다. (실제 ansi와 utf-8은 다르기 때문). 그래서 windows의 기본 string 인코딩인 UTF-16으로 변환하여 일반적인 CString 연산을 사용하였습니다.

// 무식하게 cgi 파일을 convert하자.

// [* .... *] 사이가 대상이다.

for (;;)

{

nStart = strBuffer.Find(L"[*");

if (-1 == nStart)

{

break;

}


nFinish = strBuffer.Find(L"*]", nStart);

if (-1 == nFinish)

{

// Error

mg_printf(conn, "Fail to parsing, at ... [%s]", W2A_CP(strBuffer.Mid(nStart, 20), CP_ACP));

goto FINAL;

}


// nStart ~ nFinish까지...

strMid = strBuffer.Mid(nStart + 2, nFinish - nStart - 2);


// 중간에 Function이 있는가?


// [* ... *]에는 여러개의 function만 가능하다.

// loop를 사용하여 함께 처리한다.

for (;;)

{

// <-- sum 함수 처리 -->

nFunctionStart = strMid.Find(L"sum(");

if (nFunctionStart >= 0)

{

nFunctionFinish = strMid.Find(L")", nFunctionStart);

if (-1 == nFunctionFinish)

{

mg_printf(conn, "Fail to parsing, at ... [%s]", W2A_CP(strMid.Mid(nFunctionStart, 20), CP_ACP));

goto FINAL;

}


nComma = strMid.Find(L",", nFunctionStart);

if ((nFunctionStart < nComma) && (nComma < nFunctionFinish))

{

// good

}

else

{

mg_printf(conn, "Fail to parsing, at ... [%s]", W2A_CP(strMid.Mid(nFunctionStart, 20), CP_ACP));

goto FINAL;

}


strArg1 = W2A_CP(strMid.Mid(nFunctionStart + 4, nComma - (nFunctionStart + 4)), CP_UTF8);

strArg2 = W2A_CP(strMid.Mid(nComma+1, nFunctionFinish - (nComma+1)), CP_UTF8);


if ((TRUE == strArg1.IsEmpty()) ||

(TRUE == strArg2.IsEmpty()))

{

mg_printf(conn, "Fail to parsing, at ... [%s]", W2A_CP(strMid.Mid(nFunctionStart, 20), CP_ACP));

goto FINAL;

}


// sum 함수

// strArg1, strArg2 사용됨

nSum = 0;


if (NULL == pstRequestInfo->query_string)

{

// 그런데, GET에 argument가 없음. 즉 query가 없음

mg_printf(conn, "Argument needed, at ... [%s]", W2A_CP(strMid.Mid(nFunctionStart, 20), CP_ACP));

goto FINAL;

}


if (mg_get_var(pstRequestInfo->query_string, strlen(pstRequestInfo->query_string), strArg1, szArgValue, 64) <= 0)

{

mg_printf(conn, "Argument needed, %s", strArg1);

}


nSum += atoi(szArgValue);


if (mg_get_var(pstRequestInfo->query_string, strlen(pstRequestInfo->query_string), strArg2, szArgValue, 64) <= 0)

{

mg_printf(conn, "Argument needed, %s", strArg2);

}


nSum += atoi(szArgValue);

strSum.Format(L"%d", nSum);


// 원본에 sum(..., ...) ==> 실제 sum 값으로 교환한다.

strMidExchange = strMid;

strMidExchange.Delete(nFunctionStart, nFunctionFinish - nFunctionStart + 1);

strMidExchange.Insert(nFunctionStart, strSum);


strMid = strMidExchange;


// 다시 loop 처리

continue;

}

// <-- sum 함수 처리 -->


// 처리할 함수 없음

break;

}


// strMid --> strMidExchange로 변환되었다.

// 즉, 함수가 처리되었다.


// [*, *] 문자를 제거한다.

strBuffer.Delete(nStart, nFinish - nStart + 2);


// 변환된 Mid를 붙인다.

strBuffer.Insert(nStart, strMidExchange);

}

앞에서 읽었던 buffer를 가지고 [*, *]를 찾습니다. 찾아서 그 내부에 만일 지원되는 함수(sum)가 있다면, 그 결과 값으로 replace를 해줍니다.

그래서 앞에서 설명한데로, 

[*<font color=red>the sum</font> is sum(v111,v22).*]

<font color=red>the sum</font> is 3.

형태로 변환될 것입니다. 코드는 깔끔하지 않지만, 전반적인 시나리오 설명은 충분하리라 봅니다.

// 이제 strBuffer가 최종 산출물이다.

strBufferA = W2A_CP(strBuffer, CP_UTF8);

mg_printf(conn,

"HTTP/1.1 200 OK\r\n"

"Content-Type: text/html\r\n"

"Content-Length: %d\r\n"

"\r\n"

"%s",

strBufferA.GetLength(),

strBufferA);


FINAL:

if (INVALID_HANDLE_VALUE != hFile)

{

::CloseHandle(hFile);

hFile = INVALID_HANDLE_VALUE;

}

return;

}

이제 위와 같이 http header를 가지고 마지막 값을 전달하면 목적이 달성됩니다.


테스트

ntfsforensicserver.exe를 실행하면 다음과 같습니다.

즉, Start를 누르면 웹 서버가 동작합니다.

그런다음,

와 같이 a.html 파일이 열리게 됩니다.

axxx.html 은 익숙한 404 오류가 발생합니다.

REST api인 GetCurrentTime도 잘 동작하네요. F5를 계속 누르면 시간 역시 변경됩니다.

지원되지 않는 API는 위와 같이 오류를 발생시킵니다.

이제 cgi를 위와 같이 실행하면, sum(v111,v22)쪽 argument가 필요하다고 나옵니다.

즉, v111=10, v22=5의 합을 계산해 줍니다.

소스 보기를 하면 다음과 같습니다.

<html>

<body>

cgi called.<br/>

<font color=red>the sum</font> is 15.

</body>

</html>

참고로, aaa.cgi.html의 원본은 다음과 같습니다.

<html>

<body>

cgi called.<br/>

[*<font color=red>the sum</font> is sum(v111,v22).*]

</body>

</html>

즉, [* ... *] 부분이 잘 변환되었음이 확인되었습니다.


이것으로 mongoose로 web application을 만드는 과정을 마칩니다.