~panda-roux/MoonGem

917d3392b47714661c55f0aba66fe01f6020d118 — panda-roux 6 months ago 9b9df04
implementing pre-, post-, and error-script middleware functionality
M CMakeLists.txt => CMakeLists.txt +16 -2
@@ 73,9 73,23 @@ target_include_directories(${MOONGEM_EXE} PRIVATE
  ${EVT_SSL_INCLUDE_DIRS}
  ${PCRE_INCLUDE_DIRS})

install(TARGETS ${MOONGEM_EXE} DESTINATION bin)

# optionally disable logging to reduce the binary size
if(DISABLE_LOGGING)
  target_compile_definitions(${MOONGEM_EXE} PRIVATE MOONGEM_DISABLE_LOGGING)
endif()

install(TARGETS ${MOONGEM_EXE} DESTINATION bin)

# set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/test")
# set(TEST_CERT_FILES "${TEST_DIR}/cert.pem" "${TEST_DIR}/key.pem")
# add_custom_command(
#   OUTPUT TEST_CERT_FILES
#   COMMAND "openssl req -x509 -newkey rsa:4096 -keyout ${TEST_DIR}/key.pem -out ${TEST_DIR}/cert.pem -days 3650 -nodes -subj \"/CN=localhost\""
#   COMMENT "Generating certificates for use during testing")
# add_custom_target(test
#   DEPENDS TEST_CERT_FILES ${MOONGEM_EXE}
#   WORKING_DIRECTORY ${TEST_DIR}
#   COMMAND "${TEST_DIR}/run-tests.sh ${CMAKE_BINARY_DIR}/${MOONGEM_EXE} ${TEST_DIR}/cert.pem ${TEST_DIR}/key.pem ${TEST_DIR}/root"
#   COMMENT "Running tests")



M README.md => README.md +19 -3
@@ 78,6 78,17 @@ The start and end of script sections are indicated with a double curly-braces.

All of the MoonGem-defined functionality is contained within a table called `mg`.

### Pre-Request

These methods are only accessible from pre-request scripts.

- `mg.set_path(<new-path>)
    - Sets the value of the incoming request path, overriding the initial value
    - This can be useful for implementing virtual directories and other URL-reinterpretation features
- `mg.interrupt()`
    - Instructs MoonGem to bypass the rest of the requet-handling pipeline and use the current response state
    - Unless otherwise set, the default response status code will be 20 (OK)

### Body

These methods modify the body of the Gemini page.


@@ 89,8 100,8 @@ These methods modify the body of the Gemini page.
    - Writes `text` to the page
- `mg.line([text])`
    - Writes `text` to the page followed by a line break
- `mg.link(<uri>, [text])`
    - Writes a link to `uri` on the page, and optionally includes the alt-text `text`
- `mg.link(<url>, [text])`
    - Writes a link to `url` on the page, and optionally includes the alt-text `text`
- `mg.head(<text>, [level])`
    - Writes a header line containing `text` to the page, with an optional header level
    - The default header level is 1 (i.e. a single '#' character)


@@ 112,6 123,9 @@ If a method is called which modifies the response's status code (which all but t

- `mg.set_language(<language>)`
    - Sets the `lang` portion of the response header, indicating the language(s) that the page is written in
- `mg.success()`
    - Sets the response status code to 20 (OK)
    - Only really useful in pre- and post-request scripts
- `mg.temp_redirect(<url>)`
    - Responds with a code-30 temporary redirect to `url`
- `mg.redirect(<url>)`


@@ 135,8 149,10 @@ If a method is called which modifies the response's status code (which all but t

These methods are concerned with handling user-input.

- `mg.get_path()`
    - Returns the path portion of the requested URL
- `mg.get_input([meta])`
    - If an input argument was included in the request URI, this method returns that value
    - If an input argument was included in the request URL, this method returns that value
    - If no input was provided in the request, then the server responds with a code-10 status response and optional `meta` string
- `mg.get_sensitive_input([meta])`
    - Same as `mg.get_input`, but uses status code 11

M include/options.h => include/options.h +3 -0
@@ 9,6 9,9 @@ typedef struct cli_options_t {
  char* root;
  char* cert_path;
  char* key_path;
  char* pre_script_path;
  char* post_script_path;
  char* error_script_path;
} cli_options_t;

cli_options_t* parse_options(int argc, const char** argv);

M include/parse.h => include/parse.h +2 -1
@@ 21,7 21,8 @@ int init_parser_regex(void);

void cleanup_parser_regex(void);

parser_t* create_doc_parser(gemini_context_t* gemini, file_info_t* file);
parser_t* create_doc_parser(gemini_context_t* gemini, file_info_t* file,
                            script_ctx_t* script_ctx);

void parse_gemtext_doc(parser_t* parser, struct evbuffer* buffer);


M include/script.h => include/script.h +3 -0
@@ 23,4 23,7 @@ void destroy_script(script_ctx_t* ctx);
script_result_t exec_script(script_ctx_t* ctx, char* script, size_t script_len,
                            struct evbuffer* output);

script_result_t exec_script_file(script_ctx_t* ctx, const char* path,
                                 struct evbuffer* output);

#endif

M include/status.h => include/status.h +3 -0
@@ 21,6 21,9 @@

#define STATUS_DEFAULT STATUS_SUCCESS

#define STATUS_IS_ERROR(value) \
  (value >= STATUS_TEMPORARY_FAILURE && value <= STATUS_BAD_REQUEST)

#define META_INPUT "Input Required"
#define META_SENSITIVE_INPUT "Input Required"
#define META_SERVER_UNAVAILABLE "Server Unavailable"

M include/uri.h => include/uri.h +2 -0
@@ 3,6 3,8 @@

#include <stdbool.h>

#define URI_PATH_MAX 1024

typedef enum uri_type_t { URI_TYPE_GEMTEXT, URI_TYPE_FILE } uri_type_t;

typedef struct uri_t {

M src/api.c => src/api.c +34 -0
@@ 15,6 15,7 @@
#define BLOCK_TOKEN "```"
#define SPACE " "
#define NEWLINE "\n"
#define PATH_DEFAULT "/"

#define FLD_CERT_FINGERPRINT "fingerprint"
#define FLD_CERT_EXPIRATION "not_after"


@@ 26,6 27,32 @@ static void set_interrupt_response(response_t* response, int status,
  set_response_meta(response, meta);
}

int api_set_path(lua_State* L) {
  lua_settop(L, 1);

  lua_getfield(L, LUA_REGISTRYINDEX, FLD_REQUEST);
  request_t* request = (request_t*)lua_touserdata(L, -1);

  if (!lua_isnoneornil(L, 1)) {
    if (request->uri->path != NULL) {
      free(request->uri->path);
    }

    request->uri->path = strndup(lua_tostring(L, 1), URI_PATH_MAX);
  } else {
    request->uri->path = strdup(PATH_DEFAULT);
  }

  return 0;
}

int api_interrupt(lua_State* L) {
  lua_getfield(L, LUA_REGISTRYINDEX, FLD_RESPONSE);
  response_t* response = (response_t*)lua_touserdata(L, -1);
  response->interrupted = true;
  return 0;
}

int api_set_lang(lua_State* L) {
  lua_settop(L, 1);



@@ 106,6 133,13 @@ int api_get_path(lua_State* L) {
  return 1;
}

int api_success(lua_State* L) {
  lua_getfield(L, LUA_REGISTRYINDEX, FLD_RESPONSE);
  response_t* response = (response_t*)lua_touserdata(L, -1);
  set_response_status(response, STATUS_SUCCESS, NULL);
  return 0;
}

int api_temp_redirect(lua_State* L) {
  lua_settop(L, 1);


M src/gemini.c => src/gemini.c +72 -19
@@ 1,5 1,7 @@
#include "gemini.h"

#include "script.h"

#define GNU_SOURCE

#include <event2/buffer.h>


@@ 44,6 46,7 @@ typedef struct context_t {
  SSL* ssl;
  cli_options_t* options;
  struct magic_set* magic;
  script_ctx_t* script_ctx;
} context_t;

static client_cert_t* create_client_cert() {


@@ 90,6 93,30 @@ static void event_cb(struct bufferevent* bev, short evt, void* data) {
}

static void end_response_cb(struct bufferevent* bev, void* data) {
  context_t* ctx = (context_t*)data;
  cli_options_t* options = ctx->options;

  // run post-response or error scripts if applicable
  if (options->post_script_path != NULL &&
      !STATUS_IS_ERROR(ctx->gemini.response.status)) {
    if (ctx->script_ctx == NULL) {
      ctx->script_ctx = create_script_ctx(&ctx->gemini);
    }

    exec_script_file(ctx->script_ctx, options->post_script_path, ctx->out);
  } else if (options->post_script_path != NULL &&
             STATUS_IS_ERROR(ctx->gemini.response.status)) {
    if (ctx->script_ctx == NULL) {
      ctx->script_ctx = create_script_ctx(&ctx->gemini);
    }

    exec_script_file(ctx->script_ctx, options->error_script_path, ctx->out);
  }

  if (ctx->script_ctx != NULL) {
    destroy_script(ctx->script_ctx);
  }

  bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
  bufferevent_trigger_event(bev, BEV_EVENT_EOF | BEV_EVENT_WRITING, 0);
}


@@ 174,9 201,15 @@ static void send_script_response(context_t* ctx, struct bufferevent* bev) {
  LOG_DEBUG("Sending gemtext response");

  // parse the gemtext file and run any scripts found within
  parser_t* parser = create_doc_parser(&ctx->gemini, &ctx->file);
  parser_t* parser =
      create_doc_parser(&ctx->gemini, &ctx->file, ctx->script_ctx);
  struct evbuffer* rendered = evbuffer_new();
  parse_gemtext_doc(parser, rendered);

  // store a reference to the script context so that we can use it for running
  // post- or error-response scripts if needed
  ctx->script_ctx = parser->script_ctx;

  destroy_doc_parser(parser);

  if (res->interrupted || res->status != STATUS_DEFAULT) {


@@ 241,7 274,6 @@ static void read_cb(struct bufferevent* bev, void* data) {
  context_t* ctx = (context_t*)data;
  request_t* req = &ctx->gemini.request;
  response_t* res = &ctx->gemini.response;
  file_info_t* file = &ctx->file;

  // read the entire request into a buffer
  char request_buffer[MAX_URL_LENGTH + 2] = {0};


@@ 261,25 293,45 @@ static void read_cb(struct bufferevent* bev, void* data) {
      req->cert = cert;
    }

    // check whether the requested file exists
    struct stat st;
    if (stat(req->uri->path, &st) != 0 || !S_ISREG(st.st_mode)) {
      LOG_DEBUG("File %s does not exist", req->uri->path);
      set_response_status(res, STATUS_NOT_FOUND, META_NOT_FOUND);
    } else {
      LOG_DEBUG("File %s exists", req->uri->path);
      file->ptr = fopen(req->uri->path, "rb");
      if (file->ptr == NULL) {
        LOG_ERROR("Failed to open file at \"%s\"", req->uri->path);

        // if we can't open the file, it may be due to being denied permissions,
        // which is may have been intentional if the host doesn't want the files
        // to be served; tell the client they don't exist
    cli_options_t* options = ctx->options;

    // try to run a pre-request script if applicable
    bool pre_script_failed = false;
    if (options->pre_script_path != NULL) {
      ctx->script_ctx = create_script_ctx(&ctx->gemini);
      if (exec_script_file(ctx->script_ctx, options->pre_script_path,
                           ctx->out) != SCRIPT_OK) {
        pre_script_failed = true;
        set_response_status(res, STATUS_CGI_ERROR, META_CGI_ERROR);
        LOG_ERROR(
            "An error occurred during the pre-request script; bypassing the "
            "rest of the response");
      }
    }

    // if the pre-response script neither failed nor handled the request on its
    // own in some other way, continue handling the request
    if (!pre_script_failed && !res->interrupted) {
      struct stat st;
      if (stat(req->uri->path, &st) != 0 || !S_ISREG(st.st_mode)) {
        LOG_DEBUG("File %s does not exist", req->uri->path);
        set_response_status(res, STATUS_NOT_FOUND, META_NOT_FOUND);
      } else {
        // file exists and was opened successfully
        file->fd = fileno(file->ptr);
        file->size = st.st_size;
        LOG_DEBUG("File %s exists", req->uri->path);
        file_info_t* file = &ctx->file;
        file->ptr = fopen(req->uri->path, "rb");
        if (file->ptr == NULL) {
          LOG_ERROR("Failed to open file at \"%s\"", req->uri->path);

          // if we can't open the file, it may be due to being denied
          // permissions, which is may have been intentional if the host doesn't
          // want the files to be served; tell the client they don't exist
          set_response_status(res, STATUS_NOT_FOUND, META_NOT_FOUND);
        } else {
          // file exists and was opened successfully
          file->fd = fileno(file->ptr);
          file->size = st.st_size;
        }
      }
    }
  }


@@ 313,6 365,7 @@ static void listener_cb(struct evconnlistener* listener, evutil_socket_t fd,
  ctx->gemini.response.status = STATUS_DEFAULT;
  ctx->magic = gemini->magic;
  ctx->options = gemini->options;
  ctx->script_ctx = NULL;

  bufferevent_setcb(bev, read_cb, NULL, event_cb, ctx);
  bufferevent_enable(bev, EV_READ);

M src/options.c => src/options.c +40 -4
@@ 35,6 35,9 @@ cli_options_t* parse_options(int argc, const char** argv) {
  const char* root = NULL;
  const char* cert_path = NULL;
  const char* key_path = NULL;
  const char* pre_script_path = NULL;
  const char* post_script_path = NULL;
  const char* error_script_path = NULL;

  struct argparse_option options_config[] = {
      OPT_HELP(),


@@ 52,6 55,15 @@ cli_options_t* parse_options(int argc, const char** argv) {
      OPT_INTEGER('u', "chunk", &options->chunk_size,
                  "size in bytes of the chunks loaded into memory while "
                  "serving static files (default: 16384)"),
      OPT_GROUP("Middleware"),
      OPT_STRING('b', "before", &pre_script_path,
                 "script to be run before each request is handled"),
      OPT_STRING('a', "after", &post_script_path,
                 "script to be run after a request has "
                 "resulted in a success response code (20)"),
      OPT_STRING('e', "error", &error_script_path,
                 "script to be run after a request has resulted "
                 "in an error response code (40 thru 59)"),
      OPT_END(),
  };



@@ 68,7 80,7 @@ cli_options_t* parse_options(int argc, const char** argv) {

  options->cert_path = realpath(cert_path, NULL);
  if (options->cert_path == NULL) {
    perror("Invalid certificate path");
    LOG_ERROR("Invalid certificate path");
    goto failure;
  }



@@ 80,15 92,39 @@ cli_options_t* parse_options(int argc, const char** argv) {

  options->key_path = realpath(key_path, NULL);
  if (options->key_path == NULL) {
    perror("Invalid key path");
    LOG_ERROR("Invalid key path");
    goto failure;
  }

  if (root != NULL) {
    options->root = realpath(root, NULL);
    if (options->root == NULL) {
      perror("Invalid root path");
      return NULL;
      LOG_ERROR("Invalid root path");
      goto failure;
    }
  }

  if (pre_script_path != NULL) {
    options->pre_script_path = realpath(pre_script_path, NULL);
    if (options->pre_script_path == NULL) {
      LOG_ERROR("Invalid pre-request script path");
      goto failure;
    }
  }

  if (post_script_path != NULL) {
    options->post_script_path = realpath(post_script_path, NULL);
    if (options->post_script_path == NULL) {
      LOG_ERROR("Invalid post-request script path");
      goto failure;
    }
  }

  if (error_script_path != NULL) {
    options->error_script_path = realpath(error_script_path, NULL);
    if (options->error_script_path == NULL) {
      LOG_ERROR("Invalid error script path");
      goto failure;
    }
  }


M src/parse.c => src/parse.c +7 -3
@@ 57,11 57,13 @@ int init_parser_regex(void) {

void cleanup_parser_regex(void) { regfree(&parser_regexp); }

parser_t* create_doc_parser(gemini_context_t* gemini, file_info_t* file) {
parser_t* create_doc_parser(gemini_context_t* gemini, file_info_t* file,
                            script_ctx_t* script_ctx) {
  parser_t* parser = (parser_t*)malloc(sizeof(parser_t));
  parser->gemini = gemini;
  parser->file = file;
  parser->script_ctx = NULL;
  parser->script_ctx =
      script_ctx;  // may be NULL if no pre-request script was run

  return parser;
}


@@ 117,7 119,9 @@ done:

void destroy_doc_parser(parser_t* parser) {
  if (parser != NULL) {
    destroy_script(parser->script_ctx);
    // don't free the script context here; we may need to use it for post- or
    // error-response scripts

    free(parser);
  }
}

M src/script.c => src/script.c +51 -13
@@ 14,6 14,9 @@

#define LIBRARY_TABLE_NAME "mg"

#define FUNC_SET_PATH "set_path"
#define FUNC_INTERRUPT "interrupt"

#define FUNC_INPUT "get_input"
#define FUNC_INPUT_SENSITIVE "get_sensitive_input"
#define FUNC_HAS_INPUT "has_input"


@@ 23,6 26,7 @@
#define FUNC_HAS_CERT "has_cert"

#define FUNC_LANG "set_language"
#define FUNC_SUCCESS "success"
#define FUNC_TEMP_REDIRECT "temp_redirect"
#define FUNC_REDIRECT "redirect"
#define FUNC_TEMP_FAILURE "temp_failure"


@@ 58,6 62,10 @@ typedef struct script_ctx_t {
  response_t* response;
} script_ctx_t;

/* Pre-Request */
int api_set_path(lua_State* L);
int api_interrupt(lua_State* L);

/* Input */
int api_get_input(lua_State* L);
int api_get_input_sensitive(lua_State* L);


@@ 70,6 78,7 @@ int api_has_cert(lua_State* L);

/* Response */
int api_set_lang(lua_State* L);
int api_success(lua_State* L);
int api_temp_redirect(lua_State* L);
int api_perm_redirect(lua_State* L);
int api_temp_failure(lua_State* L);


@@ 97,7 106,10 @@ int api_beginblock(lua_State* L);
int api_endblock(lua_State* L);

static void set_api_methods(lua_State* L) {
  luaL_Reg methods[] = {{FUNC_INPUT, api_get_input},
  luaL_Reg methods[] = {{FUNC_SET_PATH, api_set_path},
                        {FUNC_INTERRUPT, api_interrupt},

                        {FUNC_INPUT, api_get_input},
                        {FUNC_INPUT_SENSITIVE, api_get_input_sensitive},
                        {FUNC_HAS_INPUT, api_has_input},
                        {FUNC_GET_PATH, api_get_path},


@@ 107,6 119,7 @@ static void set_api_methods(lua_State* L) {

                        {FUNC_LANG, api_set_lang},
                        {FUNC_REDIRECT, api_perm_redirect},
                        {FUNC_SUCCESS, api_success},
                        {FUNC_TEMP_REDIRECT, api_temp_redirect},
                        {FUNC_TEMP_FAILURE, api_temp_failure},
                        {FUNC_UNAVAILABLE, api_unavailable},


@@ 234,8 247,6 @@ script_result_t exec_script(script_ctx_t* ctx, char* script, size_t script_len,

  set_response_buffer(L, output);

  script_result_t result = SCRIPT_OK;

  // we have to copy the script body to a buffer so that we can null-terminate
  // it, as the Lua API doesn't provide a way to run a string with an explicit
  // length


@@ 244,18 255,45 @@ script_result_t exec_script(script_ctx_t* ctx, char* script, size_t script_len,

  lua_pop(L, lua_gettop(L));
  if (luaL_dostring(L, &global_script_buffer[0]) != LUA_OK) {
    LOG_ERROR("Error running Lua script: %s", lua_tostring(L, -1));
    result = SCRIPT_ERROR;
  } else {
    LOG_DEBUG("Ran a script of length %zu", script_len);
    if (lua_isstring(L, -1)) {
      LOG_ERROR("Error running script: %s", lua_tostring(L, -1));
    } else {
      LOG_ERROR("Error running script");
    }

    // if the script returned a string, write it to the buffer because that's
    // nice
    if (lua_gettop(L) > 0 && lua_isstring(L, -1)) {
      const char* text = lua_tostring(L, -1);
      evbuffer_add_printf(output, "%s", text);
    return SCRIPT_ERROR;
  }

  LOG_DEBUG("Ran a script of length %zu", script_len);

  // if the script returned a string, write it to the buffer because that's
  // nice
  if (lua_gettop(L) > 0 && lua_isstring(L, -1)) {
    const char* text = lua_tostring(L, -1);
    evbuffer_add_printf(output, "%s", text);
  }

  return SCRIPT_OK;
}

script_result_t exec_script_file(script_ctx_t* ctx, const char* path,
                                 struct evbuffer* output) {
  lua_State* L = ctx->L;

  set_response_buffer(L, output);

  if (luaL_dofile(ctx->L, path) != LUA_OK) {
    if (lua_isstring(L, -1)) {
      LOG_ERROR("Error running script file at %s: %s", path,
                lua_tostring(L, -1));
    } else {
      LOG_ERROR("Error running script file at %s", path);
    }

    return SCRIPT_ERROR;
  }

  return result;
  LOG_DEBUG("Ran script at %s", path);

  return SCRIPT_OK;
}