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이 필요합니다.
tls-certificate-request-authority-names 키에 등록된 이름만 연결 허용합니다.createHttpServer로 생성한 HttpServer 객체입니다.
- 0: 성공
- -1: 실패
createHttpServer로 생성한 HttpServer 객체입니다.
- 0: 성공
- -1: 실패
createHttpServer로 생성한 HttpServer 객체입니다.
- 0: 성공
- -1: 실패
createHttpServer로 생성한 HttpServer 객체입니다.
- 0: 성공
- -1: 실패
아래는 웹소켓 서비스 생성에 필요한 key-value map 목록입니다.
HttpUpStreamDelegate::onRequest API에서 서버 스크립트 콜백을 수행할 때 호출합니다. 웹 페이지 파일 내에 <?=<key_name>?> 이러한 서버 스크립트 태깅이 있을 경우, <key_name> 값을 얻기 위해 http_interpret_callback_t로 정의된 사용자 함수가 호출됩니다. server_page_path 파일이 없으면 "400 Bad Request" 응답을 전송합니다. 성공할 경우, "200 OK" 응답 코드와 함께 파일의 내용을 전송합니다.
- 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 콜백 함수를 호출하여 얻습니다. 이렇게 동적으로 변경한 웹 문서 내용을 클라이언트에게 전달합니다.
httpInterpretCallback 호출시 매개변수로 전달한 사용자 정의 데이터입니다.
httpInterpretCallback 호출시 매개변수로 전달한 key-value map 포인터입니다.
- 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은 아래와 같습니다.
httpConnectionIsClosed로 연결 상태를 확인하고, 이미 연결된 상태면 httpCloseConnection을 호출한 다음, httpOpenConnection를 사용합니다.
- 0: 성공
- -1: 실패
true이면 소켓을 완전히 닫습니다.
- 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;
}