documentation server and /doc route

Signed-off-by: ngn <ngn@ngn.tf>
This commit is contained in:
ngn
2025-01-16 07:46:27 +03:00
parent 5fb3c03e40
commit e87764a4c2
40 changed files with 1313 additions and 301 deletions

View File

@ -1,34 +1,46 @@
#include <ctorm/log.h>
#include <ctorm/ctorm.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include "config.h"
#include "util.h"
option_t options[] = {
{"host", "0.0.0.0:7003", true }, // host the server should listen on
{NULL, NULL, false},
{"host", "0.0.0.0:7003", true }, // host the server should listen on
{"docs_dir", "./docs", true }, // documentation directory
{"", NULL, false},
};
int32_t config_load(config_t *conf) {
bool config_load(config_t *conf) {
bzero(conf, sizeof(*conf));
char *value = NULL;
char name_env[OPT_NAME_MAX + 5], name_copy[OPT_NAME_MAX], *value = NULL;
conf->options = options;
for (option_t *opt = conf->options; opt->name != NULL; opt++) {
if ((value = getenv(opt->name)) != NULL)
for (option_t *opt = conf->options; opt->value != NULL; opt++, conf->count++) {
strcpy(name_copy, opt->name);
util_toupper(name_copy);
snprintf(name_env, sizeof(name_env), "DOC_%s", name_copy);
if ((value = getenv(name_env)) != NULL)
opt->value = value;
if (opt->required && *opt->value == 0) {
error("please specify a value for the required config option %s", opt->name);
errno = EFAULT;
return -1;
}
if (*opt->value == 0)
opt->value = NULL;
conf->count++;
if (!opt->required || NULL != opt->value)
continue;
ctorm_fail("please specify a value for the required config option: %s (%s)", opt->name, name_env);
errno = EFAULT;
return false;
}
return 0;
return true;
}
char *config_get(config_t *conf, const char *name) {

View File

@ -1,25 +1,41 @@
#include <ctorm/all.h>
#include <ctorm/ctorm.h>
#include <stdlib.h>
#include "routes.h"
#include "config.h"
#include "routes.h"
int main() {
config_t conf;
app_config_t config;
ctorm_app_t *app = NULL;
ctorm_config_t app_config;
if (config_load(&conf) < 0)
config_t conf;
char *host = NULL;
if (!config_load(&conf))
return EXIT_FAILURE;
host = config_get(&conf, "host");
app_config_new(&config);
config.disable_logging = true;
ctorm_config_new(&app_config);
app_config.disable_logging = true;
app = ctorm_app_new(&app_config);
app_t *app = app_new(&config);
GET(app, "/read", GET_read);
// middlewares
MIDDLEWARE_ALL(app, "/*", route_cors);
MIDDLEWARE_ALL(app, "/*/*", route_cors);
MIDDLEWARE_ALL(app, "/*/*/*", route_cors);
if (!app_run(app, config_get(&conf, "host")))
error("failed to start the app: %s", app_geterror());
// routes
GET(app, "/list", route_list);
GET(app, "/get/:name", route_get);
app_free(app);
ctorm_app_all(app, route_notfound);
ctorm_app_local(app, "config", &conf);
ctorm_info("starting the web server on %s", host);
if (!ctorm_app_run(app, host))
ctorm_fail("failed to start the app: %s", ctorm_geterror());
ctorm_app_free(app);
return EXIT_SUCCESS;
}

9
doc/src/routes/cors.c Normal file
View File

@ -0,0 +1,9 @@
#include <ctorm/ctorm.h>
void route_cors(ctorm_req_t *req, ctorm_res_t *res) {
RES_SET("Access-Control-Allow-Origin", "*");
RES_SET("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, "
"X-Requested-With");
RES_SET("Access-Control-Allow-Methods", "PUT, DELETE, GET");
}

78
doc/src/routes/get.c Normal file
View File

@ -0,0 +1,78 @@
#include <linux/limits.h>
#include <ctorm/ctorm.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "routes.h"
#include "config.h"
#include "util.h"
void route_get(ctorm_req_t *req, ctorm_res_t *res) {
config_t *conf = REQ_LOCAL("config");
char *docs_dir = config_get(conf, "docs_dir");
const char *name = REQ_PARAM("name");
if (NULL == name) {
ctorm_fail("doc name is not specified");
return util_send(res, 500, NULL);
}
if (strlen(name) > NAME_MAX - 6)
return util_send(res, 404, NULL);
char full_path[PATH_MAX + 1], full_name[NAME_MAX + 1];
util_file_t *doc_file = NULL, *json_file = NULL;
cJSON *doc_json = NULL;
// read the doc markdown
snprintf(full_name, sizeof(full_name), "%s.md", name);
snprintf(full_path, sizeof(full_path), "%s/%s", docs_dir, full_name);
if (!util_dir_contains(docs_dir, full_name)) {
util_send(res, 404, NULL);
goto end;
}
if (NULL == (doc_file = util_file_load(full_path))) {
ctorm_fail("failed to load file: %s", full_path);
util_send(res, 500, NULL);
goto end;
}
// read the doc JSON
snprintf(full_name, sizeof(full_name), "%s.json", name);
snprintf(full_path, sizeof(full_path), "%s/%s", docs_dir, full_name);
if (!util_dir_contains(docs_dir, full_name)) {
util_send(res, 404, NULL);
goto end;
}
if (NULL == (json_file = util_file_load(full_path))) {
ctorm_fail("failed to load file: %s", full_path);
util_send(res, 500, NULL);
goto end;
}
// parse the doc JSON
if (NULL == (doc_json = cJSON_Parse(json_file->content))) {
ctorm_fail("failed to parse file: %s", full_path);
util_send(res, 500, NULL);
goto end;
}
cJSON_AddStringToObject(doc_json, "content", doc_file->content);
util_send(res, 200, doc_json);
end:
if (NULL != doc_file)
util_file_free(doc_file);
if (NULL != json_file)
util_file_free(json_file);
if (NULL != doc_json)
cJSON_free(doc_json);
}

72
doc/src/routes/list.c Normal file
View File

@ -0,0 +1,72 @@
#include <linux/limits.h>
#include <ctorm/ctorm.h>
#include <dirent.h>
#include <string.h>
#include <stdio.h>
#include "util.h"
#include "config.h"
void route_list(ctorm_req_t *req, ctorm_res_t *res) {
config_t *conf = REQ_LOCAL("config");
char *docs_dir = config_get(conf, "docs_dir");
cJSON *array = NULL, *json = NULL;
DIR *docs_dir_fd = NULL;
if (NULL == (array = cJSON_CreateArray())) {
ctorm_fail("failed to create cJSON array");
return util_send(res, 500, NULL);
}
if (NULL == (docs_dir_fd = opendir(docs_dir))) {
ctorm_fail("failed to open the docs dir (%s): %s", docs_dir, ctorm_geterror());
return util_send(res, 500, NULL);
}
char doc_path[PATH_MAX + 1], doc_name[NAME_MAX + 1];
util_file_t *doc_file = NULL;
struct dirent *doc = NULL;
uint64_t ext_indx = 0;
while (NULL != (doc = readdir(docs_dir_fd))) {
if ((ext_indx = util_endswith(doc->d_name, ".json")) == 0)
continue;
snprintf(doc_path, sizeof(doc_path), "%s/%s", docs_dir, doc->d_name);
if (NULL == (doc_file = util_file_load(doc_path))) {
ctorm_fail("failed to load the JSON file: %s", doc_path);
goto next;
}
if (NULL == (json = cJSON_Parse(doc_file->content))) {
ctorm_fail("failed to parse the JSON file: %s", doc_path);
goto next;
}
strcpy(doc_name, doc->d_name);
util_tolower(doc_name);
doc_name[ext_indx] = 0;
cJSON_AddStringToObject(json, "name", doc_name);
cJSON_AddItemToArray(array, json);
next:
if (NULL != doc_file)
util_file_free(doc_file);
doc_file = NULL;
}
closedir(docs_dir_fd);
if (NULL == (json = cJSON_CreateObject())) {
ctorm_fail("failed to create cJSON object");
return util_send(res, 500, NULL);
}
cJSON_AddItemToObject(json, "list", array);
util_send(res, 200, json);
cJSON_free(json);
}

View File

@ -0,0 +1,6 @@
#include "routes.h"
#include "util.h"
void route_notfound(ctorm_req_t *req, ctorm_res_t *res) {
return util_send(res, 404, NULL);
}

View File

@ -1,118 +0,0 @@
#include <linux/limits.h>
#include <ctorm/all.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <cmark.h>
#include <fcntl.h>
#include "routes.h"
void GET_read(req_t *req, res_t *res) {
const char *lang = REQ_QUERY("lang");
const char *name = REQ_QUERY("name");
char fp[PATH_MAX + 1], md[NAME_MAX + 1];
struct dirent *dirent = NULL;
int32_t name_len = 0, ent_len = 0;
DIR *dir = NULL;
bzero(fp, sizeof(fp));
bzero(md, sizeof(md));
if (NULL == name)
return RES_REDIRECT("/");
name_len = strlen(name);
if (NULL == lang)
lang = "en";
if (NULL == (dir = opendir("docs"))) {
error("failed to open the docs dir: %s", strerror(errno));
RES_SENDFILE("html/internal.html");
res->code = 500;
return;
}
while ((dirent = readdir(dir)) != NULL) {
if (strncmp(dirent->d_name, lang, PATH_MAX) == 0)
break;
}
closedir(dir);
if (NULL == dirent) {
RES_SENDFILE("html/notfound.html");
return;
}
snprintf(fp, sizeof(fp), "docs/%s", lang);
if (NULL == (dir = opendir(fp))) {
error("failed to open the language dir: %s", strerror(errno));
RES_SENDFILE("html/internal.html");
res->code = 500;
return;
}
while ((dirent = readdir(dir)) != NULL) {
if ((ent_len = strlen(dirent->d_name) - 3) != name_len)
continue;
if (strncmp(dirent->d_name, name, name_len) == 0) {
memcpy(md, dirent->d_name, ent_len + 3);
break;
}
}
closedir(dir);
if (NULL == dirent) {
RES_SENDFILE("html/notfound.html");
return;
}
char *md_content = NULL, md_fp[PATH_MAX + 1];
struct stat md_st;
int md_fd = 0;
snprintf(md_fp, sizeof(fp), "%s/%s", fp, md);
if ((md_fd = open(md_fp, O_RDONLY)) < 0) {
error("failed to open %s: %s", fp, strerror(errno));
goto err_internal_close;
}
if (fstat(md_fd, &md_st) < 0) {
error("failed to fstat %s: %s", fp, strerror(errno));
goto err_internal_close;
}
if ((md_content = mmap(0, md_st.st_size, PROT_READ, MAP_PRIVATE, md_fd, 0)) == NULL) {
error("failed to mmap %s: %s", fp, strerror(errno));
goto err_internal_close;
}
char *parsed = cmark_markdown_to_html(md_content, md_st.st_size, CMARK_OPT_DEFAULT);
RES_SEND(parsed);
free(parsed);
munmap(md_content, md_st.st_size);
close(md_fd);
return;
err_internal_close:
close(md_fd);
RES_SENDFILE("html/internal.html");
return;
}

125
doc/src/util.c Normal file
View File

@ -0,0 +1,125 @@
#include <linux/limits.h>
#include <ctorm/ctorm.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdint.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <errno.h>
#include "util.h"
uint64_t util_endswith(char *str, char *suf) {
if (NULL == str || NULL == suf) {
errno = EINVAL;
return false;
}
uint64_t sufl = strlen(suf);
uint64_t strl = strlen(str);
if (sufl > strl)
return false;
uint64_t indx = strl - sufl;
return strncmp(str + indx, suf, sufl) == 0 ? indx : 0;
}
void util_send(ctorm_res_t *res, uint16_t code, cJSON *json) {
if (NULL == json)
json = cJSON_CreateObject();
const char *error = "";
switch (code) {
case 404:
error = "not found";
break;
case 400:
error = "bad request";
break;
case 500:
error = "internal server error";
break;
}
if (*error != 0)
cJSON_AddStringToObject(json, "error", error);
RES_JSON(json);
}
bool util_dir_contains(char *dir, const char *file) {
if (NULL == dir || NULL == file) {
errno = EINVAL;
return false;
}
struct dirent *dirent = NULL;
DIR *dirfd = NULL;
if (NULL == (dirfd = opendir(dir)))
return false; // errno set by opendir
while ((dirent = readdir(dirfd)) != NULL) {
if (util_compare_name(dirent->d_name, ".") || util_compare_name(dirent->d_name, ".."))
continue;
if (util_compare_name(dirent->d_name, file))
break;
}
closedir(dirfd);
return NULL != dirent;
}
util_file_t *util_file_load(char *path) {
if (NULL == path) {
errno = EINVAL;
return NULL;
}
util_file_t *file = NULL;
struct stat buf;
int fd = -1;
if (NULL == (file = malloc(sizeof(util_file_t))))
goto end; // errno set by malloc
if ((fd = open(path, O_RDONLY)) < 0)
goto end; // errno set by open
if (fstat(fd, &buf) < 0)
goto end; // errno set by fstat
if (NULL == (file->content = mmap(0, (file->size = buf.st_size), PROT_READ, MAP_PRIVATE, fd, 0)))
goto end; // errno set by mmap
end:
if (fd != -1)
close(fd);
if (NULL == file->content) {
free(file);
return NULL;
}
return file;
}
void util_file_free(util_file_t *file) {
if (NULL == file)
return;
munmap(file->content, file->size);
free(file);
}