Web API¶
Oasis는 HTTP, HTTPS 서버와 클라이언트를 지원합니다.
헤더 파일¶
OasisWeb.h
HttpUpStreamDelegate 인터페이스¶
Oasis Web 서버와 클라이언트는 HttpUpStreamDelegate 인터페이스로 부터 유도된 사용자 정의 객체를 통하여 HTTP 요청과 응답을 처리합니다.
class HttpUpStreamDelegate : public std::enable_shared_from_this<HttpUpStreamDelegate>
{
public:
HttpUpStreamDelegate();
virtual ~HttpUpStreamDelegate();
public:
virtual void onRequest(const NetMessageRef &msg);
virtual void onEarlyResponse(const NetMessageRef &msg); //when the response header is ready
virtual void onResponse(const NetMessageRef &msg); //when the response header and contents ready
virtual void onGatewayResponse(const struct sockaddr_in &local_addr, const struct sockaddr_in &remote_addr, int32_t status_code, const char *status_reason, const char *content_type, const char *content, size_t content_length, key_value_map_t ¶meters);
};
웹 서버 함수¶
HTTPS를 활성화하려면 아래 key-value map이 필요합니다.
- 0: 성공
- -1: 실패
- 0: 성공
- -1: 실패
- 0: 성공
- -1: 실패
- 0: 성공
- -1: 실패
아래는 웹소켓 서비스 생성에 필요한 key-value map 목록입니다.
- 0: 성공
- -1: 실패
아래 코드는 웹 문서 확장자가 osp인 경우, 웹 서버 스크립트 수행하는 예입니다.
static int32_t httpInterpretCallback(const char *url, void *callback_data, key_value_map_t *callback_arguments, const char *key_name, std::string &output)
{
// key_name에 해당하는 값을 구하여 output에 리턴합니다.
return 0;
}
void MyHttpConfigUpStreamDelegate::onRequest(const NetMessageRef &msg)
{
bool reply_now = true;
std::string url;
netMessageUriGetPath(msg, url);
ASSERT(netMessageIsRequest(msg));
if(!netMessageIsRequest(msg)) {
return;
}
NetMessageRef res = netMessageCreate(msg, false);
if(res == nullptr) {
netMessageAbortConnection(msg);
return;
}
netMessageSetStatusCodeAndReasonString(res, 200, "OK");
netMessageSetHeaderField(res, "Connection", "close");
if(url == "/") {
std::string index_path = app().httpRootPath();
index_path += "/index.osp";
err = httpInterpretServerPage(res, url.c_str(), index_path.c_str(), &httpInterpretCallback, (void*)this, nullptr);
if(err < 0) {
//do nothing
netMessageAbortConnection(msg);
return;
}
} else {
std::string path = app().httpRootPath();
path += url;
if(access(path.c_str(), F_OK) == 0) {
//get mime type based on extension
const char *mime_type = get_file_mime_type(path.c_str());
if(mime_type == nullptr) {
//not supported
netMessageSetStatusCodeAndReasonString(res, 403, "Forbidden");
} else {
std::string ext = get_file_ext(path);
if(ext == "osp") {
err = httpInterpretServerPage(res, url.c_str(), path.c_str(), &httpInterpretCallback, (void*)this, nullptr);
if(err < 0) {
//do nothing
netMessageAbortConnection(msg);
return;
}
} else {
ssize_t ret = netMessageSendFile(res, path.c_str(), mime_type);
reply_now = false;
}
}
} else {
netMessageSetStatusCodeAndReasonString(res, 400, "Bad Request");
}
}
done:
if(reply_now) {
netMessagePost(res);
}
}
Script 처리 콜백¶
Oasis 웹 서버는 웹 문서 내에 <?=<key_name>?> 양식의 서버 스크립트용 요소를 정의할 수 있습니다.
httpInterpretServerPage 함수는 웹 문서에서 이 요소를 분류하여, <key_name>에 해당하는 실제 값을 http_interpret_callback_t 콜백 함수를 호출하여 얻습니다. 이렇게 동적으로 변경한 웹 문서 내용을 클라이언트에게 전달합니다.
- 0: 성공
- -1: 실패
아래는 http_interpret_callback_t 구현 예입니다.
<p>
제품명은 <?=printProductName?> 입니다.
</p>
<p>
접속한 서버의 IP 주소는 <?=printIPAddress?> 입니다.
</p>
static int32_t httpInterpretCallback(const char *url, void *callback_data, key_value_map_t *callback_arguments, const char *key_name, std::string &output)
{
std::string _url(url);
char value[256];
int32_t err;
output.clear();
if(strcmp(key_name, "printProductName") == 0) {
output = "Oasis";
} else if(strcmp(key_name, "printIPAddress") == 0) {
output = "127.0.0.1";
}
return 0;
}
웹 클라이언트 함수¶
연결에 필요한 key-value map은 아래와 같습니다.
- 0: 성공
- -1: 실패
- 0: 성공
- -1: 실패
- 1: 연결이 닫혀 있습니다.
- 0: 연결 중입니다.
예제¶
서버¶
아래 예제의 서버는 홈 페이지에 접속하면 index.osp 파일을 읽어서 Script 콜백 함수를 호출하여 index.osp 내에 있는 변수값을 실제 적용 값으로 치환하여 클라이언트에게 전송합니다. 그외 파일에 접근할 경우, 허용된 경우에 파일 내용을 전송합니다.
또한 /ws/echo 에 웹소켓 서버를 연결하여, 웹소켓 클라이언트가 /ws/echo URL에 접속할 경우, 웹소켓 통신을 하게 됩니다.
응용 프로그램 이름은 webserver 이고 사용법은 아래와 같습니다.
USAGE: ./webserver [-p <port number>] [-s] <documnet root dir>
$ ./webserver -p 8080 -s /mnt/sd
- p 옵션은 포트번호를 지정합니다.
- s 옵션은 보안 서버 (HTTPS)를 실행합니다. 인증서와 개인키는 각 /mnt/sd/webserver-cert.pem 와 /mnt/sd/webserver-key.pem 에 있다고 가정합니다.
- documnet root dir 필수 인자로서 HTTP 문서 파일들의 루트 디렉토리 경로입니다.
코드를 설명합니다.
서버에서 스크립트 콜백 함수를 정의합니다.
static int32_t httpInterpretCallback(const char *url, void *callback_data, key_value_map_t *callback_arguments, const char *key_name, std::string &output)
{
std::string _url(url);
char value[256];
int32_t err;
output.clear();
if(strcmp(key_name, "printModelName") == 0) {
output = "OASIS";
}
return 0;
}
HTTP 서버용 HttpUpStreamDelegate 에서 유도된 사용자 정의 Delegate 클래스를 정의합니다.
class MyHttpUpStreamDelegate : public HttpUpStreamDelegate
{
public:
MyHttpUpStreamDelegate() { }
virtual ~MyHttpUpStreamDelegate() { }
public:
virtual void onRequest(const NetMessageRef &msg) {
std::string url;
netMessageUriGetPath(msg, url);
ASSERT(netMessageIsRequest(msg));
if(!netMessageIsRequest(msg)) {
return;
}
NetMessageRef res = netMessageCreate(msg, false);
if(res == nullptr) {
netMessageAbortConnection(msg);
return;
}
fprintf(stdout, "onRequest: %s\n", url.c_str());
netMessageSetStatusCodeAndReasonString(res, 200, "OK");
//netMessageSetHeaderField(res, "Connection", "close");
if(url == "/") {
// HOME url이면, HTTP 문서의 루트 디렉토리에 index.osp 파일을 읽어와 스크립트를 실행 후 전송합니다.
std::string index_path = http_root_dir_path + "/index.osp";
int32_t err = httpInterpretServerPage(res, url.c_str(), index_path.c_str(), &httpInterpretCallback, (void*)this, nullptr);
if(err < 0) {
netMessageAbortConnection(msg);
return;
} else {
netMessagePost(res);
}
} else {
std::string path = http_root_dir_path + url;
if(access(path.c_str(), F_OK) == 0) {
//get mime type based on extension
const char *mime_type = get_file_mime_type(path.c_str());
if(mime_type == nullptr) {
//not supported
netMessageSetStatusCodeAndReasonString(res, 403, "Forbidden");
netMessagePost(res);
} else {
std::string ext = get_file_ext(path);
if(ext == "osp") {
//oasis server script file extension
int32_t err = httpInterpretServerPage(res, url.c_str(), path.c_str(), &httpInterpretCallback, (void*)this, nullptr);
if(err < 0) {
netMessageAbortConnection(msg);
return;
}
} else {
netMessageSendFile(res, path.c_str(), mime_type);
}
}
} else {
netMessageSetStatusCodeAndReasonString(res, 400, "Bad Request");
netMessagePost(res);
}
}
}
virtual void onResponse(const NetMessageRef &msg) {
}
};
index.osp 문서는 아래와 같습니다.
<html>
<head>
<meta charset="utf-8">
<title>Oasis Web Server</title>
</head>
<body>
<h1> Hello, World!</h1>
<h2> My model is <?=printModelName?>.</h2>
</body>
</html>
웹소켓 서버용 WsEventDelegate 에서 유도된 사용자 정의 Delegate 클래스를 정의합니다.
단순 받은 메세지를 그대로 응답하는 기능입니다.
class EchoWsEventDelegate : public WsEventDelegate
{
public:
EchoWsEventDelegate() {}
virtual ~EchoWsEventDelegate() {}
public:
virtual void onOpen(const WsEventRef &evt) {
fprintf(stdout, "Echo: Open\n");
}
virtual void onMessage(const WsEventRef &evt) {
fprintf(stdout, "Echo: Message\n");
if(wsEventMessageType(evt) == kWsMessageText) {
std::vector<uint8_t> data;
if(wsEventData(evt, data) > 0) {
std::string str = "Echo: ";
str += std::string(data.data(), data.data()+data.size());
wsSendText(evt, str.c_str());
}
}
}
virtual void onError(const WsEventRef &evt) {
fprintf(stdout, "Echo: Error\n");
}
virtual void onClose(const WsEventRef &evt) {
fprintf(stdout, "Echo: Close\n");
}
};
Oasis를 초기화 합니다. Oasis 파일 시스템을 사용하지 않습니다.
key_value_map_t parameters;
parameters["offs-disable"] = "1";
if(oasis::initialize(parameters) < 0) {
fprintf(stderr, "Oasis init failed\n");
return -1;
}
HTTP 서버 객체를 생성하고 시작합니다.
parameters.clear();
parameters["port"] = std::to_string(http_port);
parameters["root-dir"] = http_root_dir_path.c_str();
parameters["content-length-max"] = std::to_string(3*1024*1024);
parameters["tls-disable-certificate-validation"] = "1";
if(is_secure) {
parameters["tls-enabled"] = "1";
parameters["tls-version"] = "1.2";
parameters["cert-file-path"] = "/mnt/sd/webserver-cert.pem";
parameters["pkey-file-path"] = "/mnt/sd/webserver-key.pem";
}
http_delegate = std::make_shared<MyHttpUpStreamDelegate>();
http_server = oasis::createHttpServer(parameters, http_delegate);
if (http_server) {
err = oasis::startHttpServer(http_server);
ASSERT(err == 0);
if(err < 0) {
fprintf(stderr, "HTTPS server failed\n");
goto done;
}
fprintf(stdout, "HTTP%s running at %d...\n", is_secure?"S":"", http_port);
// 웹소켓 서비스를 /ws/echo URL을 통해 접근하도록 합니다.
parameters.clear();
std::shared_ptr<EchoWsEventDelegate> echo_delegate = std::make_shared<EchoWsEventDelegate>();
if(createHttpWsService(http_server, "/ws/echo", parameters, echo_delegate) >= 0) {
}
}
HTTP 서버를 해제합니다.
if(http_server) {
oasis::destroyHttpServer(http_server);
}
전체 코드는 아래와 같습니다.
#include "OasisAPI.h"
#include "OasisWeb.h"
#include "OasisLog.h"
#include "OasisUtil.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <signal.h>
#include <thread>
#include <mutex>
#include <memory>
#include <condition_variable>
#define DLOG_THIS 0x00080000
#undef DLOG_FLAGS
#define DLOG_FLAGS (DLOG_FLAGS_DEFAULT|DLOG_THIS/**/)
#undef DLOG_TAG
#define DLOG_TAG "HTTP-SERVER"
using namespace oasis;
static bool isDirectory(const char *path)
{
struct stat path_stat;
stat(path, &path_stat);
return S_ISDIR(path_stat.st_mode);
}
static std::string http_root_dir_path;
static int32_t httpInterpretCallback(const char *url, void *callback_data, key_value_map_t *callback_arguments, const char *key_name, std::string &output)
{
std::string _url(url);
char value[256];
int32_t err;
output.clear();
if(strcmp(key_name, "printModelName") == 0) {
output = "OASIS";
}
return 0;
}
class MyHttpUpStreamDelegate : public HttpUpStreamDelegate
{
public:
MyHttpUpStreamDelegate() { }
virtual ~MyHttpUpStreamDelegate() { }
public:
virtual void onRequest(const NetMessageRef &msg) {
std::string url;
netMessageUriGetPath(msg, url);
ASSERT(netMessageIsRequest(msg));
if(!netMessageIsRequest(msg)) {
return;
}
NetMessageRef res = netMessageCreate(msg, false);
if(res == nullptr) {
netMessageAbortConnection(msg);
return;
}
fprintf(stdout, "onRequest: %s\n", url.c_str());
netMessageSetStatusCodeAndReasonString(res, 200, "OK");
//netMessageSetHeaderField(res, "Connection", "close");
if(url == "/") {
std::string index_path = http_root_dir_path + "/index.osp";
int32_t err = httpInterpretServerPage(res, url.c_str(), index_path.c_str(), &httpInterpretCallback, (void*)this, nullptr);
if(err < 0) {
netMessageAbortConnection(msg);
return;
} else {
netMessagePost(res);
}
} else {
std::string path = http_root_dir_path + url;
if(access(path.c_str(), F_OK) == 0) {
//get mime type based on extension
const char *mime_type = get_file_mime_type(path.c_str());
if(mime_type == nullptr) {
//not supported
netMessageSetStatusCodeAndReasonString(res, 403, "Forbidden");
netMessagePost(res);
} else {
std::string ext = get_file_ext(path);
if(ext == "osp") {
//oasis server script file extension
int32_t err = httpInterpretServerPage(res, url.c_str(), path.c_str(), &httpInterpretCallback, (void*)this, nullptr);
if(err < 0) {
netMessageAbortConnection(msg);
return;
}
} else {
netMessageSendFile(res, path.c_str(), mime_type);
}
}
} else {
netMessageSetStatusCodeAndReasonString(res, 400, "Bad Request");
netMessagePost(res);
}
}
}
virtual void onResponse(const NetMessageRef &msg) {
}
};
class EchoWsEventDelegate : public WsEventDelegate
{
public:
EchoWsEventDelegate() {}
virtual ~EchoWsEventDelegate() {}
public:
virtual void onOpen(const WsEventRef &evt) {
fprintf(stdout, "Echo: Open\n");
}
virtual void onMessage(const WsEventRef &evt) {
fprintf(stdout, "Echo: Message\n");
if(wsEventMessageType(evt) == kWsMessageText) {
std::vector<uint8_t> data;
if(wsEventData(evt, data) > 0) {
std::string str = "Echo: ";
str += std::string(data.data(), data.data()+data.size());
wsSendText(evt, str.c_str());
}
}
}
virtual void onError(const WsEventRef &evt) {
fprintf(stdout, "Echo: Error\n");
}
virtual void onClose(const WsEventRef &evt) {
fprintf(stdout, "Echo: Close\n");
}
};
static bool continue_running = true;
void cancel_handler(int signum)
{
continue_running = false;
}
void printUsage(const char *pgname)
{
fprintf(stderr, "USAGE: %s [-p <port number>] [-s] <documnet root dir>\n", pgname);
}
using namespace oasis;
int main(int argc, char* argv[])
{
int32_t err;
int c;
int32_t http_port = 80;
bool is_secure = false;
std::shared_ptr<MyHttpUpStreamDelegate> http_delegate;
HttpServerRef http_server;
opterr = 0;
while ((c = getopt(argc, argv, "p:sh")) != -1) {
switch (c) {
case 'p':
if(optarg == nullptr) {
fprintf(stderr, "error: bad option argument '%c'\n", optopt);
return -1;
}
http_port = atoi(optarg);
if(http_port <= 0 || http_port > 65535) {
fprintf(stderr, "bad port number: %d\n", http_port);
return -1;
}
break;
case 's':
is_secure = true;
break;
case 'h':
default:
printUsage(argv[0]);
return -1;
}
}
if(argc-optind < 1) {
fprintf(stderr, "error: invalid or insufficient arguments (%d, %d)\n", argc, optind);
printUsage(argv[0]);
return -1;
}
http_root_dir_path = argv[optind];
if(!isDirectory(http_root_dir_path.c_str())) {
fprintf(stderr, "error: invalid root directory: %s\n", http_root_dir_path.c_str());
printUsage(argv[0]);
return -1;
}
key_value_map_t parameters;
parameters["offs-disable"] = "1";
if(oasis::initialize(parameters) < 0) {
fprintf(stderr, "Oasis init failed\n");
return -1;
}
parameters.clear();
parameters["port"] = std::to_string(http_port);
parameters["root-dir"] = http_root_dir_path.c_str();
parameters["content-length-max"] = std::to_string(3*1024*1024);
parameters["tls-disable-certificate-validation"] = "1";
if(is_secure) {
parameters["tls-enabled"] = "1";
parameters["tls-version"] = "1.2";
parameters["cert-file-path"] = "/mnt/sd/webserver-cert.pem";
parameters["pkey-file-path"] = "/mnt/sd/webserver-key.pem";
}
http_delegate = std::make_shared<MyHttpUpStreamDelegate>();
http_server = oasis::createHttpServer(parameters, http_delegate);
if (http_server) {
err = oasis::startHttpServer(http_server);
ASSERT(err == 0);
if(err < 0) {
fprintf(stderr, "HTTPS server failed\n");
goto done;
}
fprintf(stdout, "HTTP%s running at %d...\n", is_secure?"S":"", http_port);
//serverless websocket handler
parameters.clear();
std::shared_ptr<EchoWsEventDelegate> echo_delegate = std::make_shared<EchoWsEventDelegate>();
if(createHttpWsService(http_server, "/ws/echo", parameters, echo_delegate) >= 0) {
}
}
do {
usleep(100000);
} while(continue_running);
done:
if(http_server) {
oasis::destroyHttpServer(http_server);
}
oasis::finalize();
return 0;
}
클라이언트¶
아래는 웹 사이트에 접속해서 홈페이지 HTML 문서를 다운받는 클라이언트는 예입니다.
#include "OasisAPI.h"
#include "OasisWeb.h"
#include "OasisLog.h"
#include <thread>
#include <mutex>
#include <memory>
#include <condition_variable>
#define DLOG_THIS 0x00080000
#undef DLOG_FLAGS
#define DLOG_FLAGS (DLOG_FLAGS_DEFAULT|DLOG_THIS/**/)
#undef DLOG_TAG
#define DLOG_TAG "HTTPCLI"
using namespace oasis;
int main(int argc, char* argv[])
{
int32_t err;
int32_t status_code;
std::string reason_string;
std::string content_type;
std::vector<char> res_content;
size_t content_length;
const char *url = "https://google.com/";
HttpConnectionRef conn;
NetMessageRef msg, res_msg;
int32_t wait_count = 0;
key_value_map_t parameters, fields;
parameters["offs-disable"] = "1";
parameters["media-cache-size"] = "4096";
if(oasis::initialize(parameters) < 0) {
DLOG(DLOG_ERROR, "Oasis init failed\n");
return -1;
}
parameters.clear();
// 클라이언트의 인증서가 필요할 경우 설정합니다.
// parameters["ca-certs-file-path"] = "/data/certs/cacerts.pem\x0a/data/certs/RootCA.pem";
// parameters["cert-file-path"] = "/data/certs/client_fullchain.pem";
// parameters["pkey-file-path"] = "/data/certs/client_privkey.pem";
parameters["tls-disable-certificate-validation"] = "1";
conn = httpOpenConnectionWithParameters(url, 30000, parameters);
if(!conn) {
DLOG(DLOG_THIS, "connection failed!\n");
goto done;
}
msg = httpConnectionGetRequestMessage(conn);
ASSERT(msg);
if(!msg) {
return -1;
}
netMessageSetMethod(msg, "GET");
if(netMessagePost(msg) < 0) {
DLOG(DLOG_ERROR, "post error\n");
goto done;
}
wait_count = 0;
do {
res_msg = httpConnectionGetResponseMessage(conn, 1000);
wait_count++;
} while(res_msg == nullptr && wait_count*1000 < 30000); //30seconds
if(res_msg == nullptr) {
//either timeout or aborted
DLOG(DLOG_ERROR, "res timeout\n");
goto done;
}
status_code = netMessageGetStatusCodeAndReasonString(res_msg, reason_string);
DLOG(DLOG_THIS, "Response: %d %s\n", status_code, reason_string.c_str());
parameters.clear();
netMessageUriGetQueryParameters(res_msg, parameters);
if(parameters.empty() == false) {
DLOG(DLOG_THIS, "Parameters:\n");
for(auto p : parameters) {
DLOG(DLOG_THIS, " [%s] = %s\n", p.first.c_str(), p.second.c_str());
}
}
DLOG(DLOG_THIS, "Headers:\n");
fields.clear();
netMessageGetHeaderFields(res_msg, fields);
for(auto p : fields) {
DLOG(DLOG_THIS, " [%s] = %s\n", p.first.c_str(), p.second.c_str());
}
content_length = netMessageGetContentLength(res_msg);
DLOG(DLOG_THIS, "Content length: %d\n", content_length);
netMessageGetContentType(res_msg, content_type);
DLOG(DLOG_THIS, "Content type: %s\n", content_type.c_str());
netMessageGetContent(res_msg, res_content);
//DUMP("REG_RES", res_content.data(), res_content.size());
if(content_length > 0) {
char path[PATH_MAX];
sprintf(path, "/tmp/%u.html", (uint32_t)time(NULL));
FILE *fp = fopen(path, "w");
if(fp) {
fwrite(res_content.data(), 1, res_content.size(), fp);
fclose(fp);
DLOG(DLOG_THIS, "saved to %s, %d bytes\n", path, content_length);
}
}
httpCloseConnection(conn);
conn = nullptr;
done:
oasis::finalize();
return 0;
}