-
-
Save rafaelmartins/9f8392a8909e62820ae0 to your computer and use it in GitHub Desktop.
chunda - A simple URL manager, that counts URL hits.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* chunda: A simple URL manager, that counts URL hits. | |
* Copyright (C) 2015 Rafael G. Martins <rafael@rafaelmartins.eng.br> | |
* | |
* This program can be distributed under the terms of the LGPL-2 License. | |
* See <http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html>. | |
*/ | |
/* | |
* How to use: | |
* | |
* This is a very simple URL manager. To build it, use the following command: | |
* | |
* $ gcc -std=c99 -o chunda $(pkg-config --libs --cflags balde hiredis) chunda.c | |
* | |
* To deploy it, run the binary like this: | |
* | |
* $ AUTH_USERNAME="admin username" AUTH_PASSWORD="admin password" ./chunda -c | |
* | |
* Then configure your web server to connect to SCGI server at 127.0.0.1:9000 | |
* | |
* With default settings, this app will look for a redis server in localhost | |
* and the default port (6379). It will use the DB 0 by default. | |
* | |
* You can change this behavior using environment variables: | |
* | |
* - REDIS_HOST | |
* - REDIS_PORT | |
* - REDIS_DB (must be an integer, to be used with redis' SELECT command) | |
* | |
* | |
* WARNING: | |
* | |
* The current implementation won't authenticate to redis server. Your redis | |
* server instance must not require authentication. | |
* | |
* The administration interface uses HTTP Basic auth, please use HTTPS and a | |
* STRONG password. | |
* | |
*/ | |
#ifdef HAVE_CONFIG_H | |
#include <config.h> | |
#endif /* HAVE_CONFIG_H */ | |
#include <stdlib.h> | |
#include <string.h> | |
#include <glib.h> | |
#include <balde.h> | |
#include <hiredis.h> | |
static gchar *template_base_header = | |
"<html>\n" | |
" <head>\n" | |
" <title>chunda - Administration</title>\n" | |
" </head>\n" | |
" <body>\n" | |
" <h1>chunda - Administration</h1>\n" | |
" <h2>Add new URL</h2>\n" | |
" <form method=\"POST\">\n" | |
" <label for=\"alias\">Alias:</label>\n" | |
" <input type=\"text\" name=\"alias\" />\n" | |
" <label for=\"url\">URL:</label>\n" | |
" <input type=\"text\" name=\"url\" />\n" | |
" <input type=\"submit\" value=\"Add!\" />\n" | |
" </form>\n" | |
" <hr />\n" | |
" <h2>URLs</h2>\n" | |
" <table border=\"1\">\n" | |
" <tr>\n" | |
" <th>Alias</th>\n" | |
" <th>URL</th>\n" | |
" <th>Hits</th>\n" | |
" </tr>\n"; | |
static gchar *template_base_footer = | |
" </table>\n" | |
" </body>\n" | |
"</html>\n"; | |
typedef struct { | |
gchar *url; | |
gchar *alias; | |
guint64 hits; | |
} url_t; | |
static void | |
redis_connect_hook(balde_app_t *app, balde_request_t *request) | |
{ | |
if (app->error != NULL) | |
return; | |
redisContext *ctx = balde_app_get_user_data(app); | |
if (ctx != NULL && !ctx->err) | |
return; | |
const gchar *host = balde_app_get_config(app, "redis_host"); | |
if (host == NULL) | |
host = "127.0.0.1"; | |
const gchar *port_ = balde_app_get_config(app, "redis_port"); | |
gint port = 6379; | |
if (port_ != NULL) | |
port = strtol(port_, NULL, 10); | |
const gchar *db_ = balde_app_get_config(app, "redis_db"); | |
gint db = -1; | |
if (db_ != NULL) | |
db = strtol(db_, NULL, 10); | |
ctx = redisConnect(host, port); | |
if (ctx == NULL) { | |
balde_abort_set_error_with_description(app, 500, | |
"Redis: Failed to create context\n"); | |
return; | |
} | |
if (ctx->err) { | |
gchar *tmp = g_strdup_printf("Redis: Connection error: %s\n", | |
ctx->errstr); | |
balde_abort_set_error_with_description(app, 500, tmp); | |
g_free(tmp); | |
return; | |
} | |
// TODO: implement auth | |
if (db >= 0) { | |
redisReply *r = redisCommand(ctx, "SELECT %d", db); | |
if (!((NULL != r) && | |
(REDIS_REPLY_STATUS == r->type) && | |
(0 == g_ascii_strcasecmp(r->str, "ok")))) | |
{ | |
balde_abort_set_error_with_description(app, 500, | |
"Redis: Failed to select database\n"); | |
freeReplyObject(r); | |
return; | |
} | |
freeReplyObject(r); | |
} | |
balde_app_set_user_data(app, ctx); | |
} | |
static balde_response_t* | |
redis_add_url(balde_app_t *app, const char *url, const char *alias) | |
{ | |
redisContext *ctx = balde_app_get_user_data(app); | |
redisReply *r = redisCommand(ctx, "SET url:%s %s", alias, url); | |
if (!((NULL != r) && | |
(REDIS_REPLY_STATUS == r->type) && | |
(0 == g_ascii_strcasecmp(r->str, "ok")))) | |
{ | |
freeReplyObject(r); | |
return balde_abort_with_description(app, 500, | |
"Redis: Failed to add url\n"); | |
} | |
freeReplyObject(r); | |
return NULL; | |
} | |
static gint | |
redis_compare_url(url_t *a, url_t *b) | |
{ | |
if (a == NULL && b == NULL) | |
return 0; | |
if (a == NULL) | |
return -1; | |
if (b == NULL) | |
return 1; | |
return g_strcmp0(a->alias, b->alias); | |
} | |
static GSList* | |
redis_list_urls(balde_app_t *app) | |
{ | |
redisContext *ctx = balde_app_get_user_data(app); | |
redisReply *r = redisCommand(ctx, "KEYS url:*"); | |
if (!((NULL != r) && | |
(REDIS_REPLY_ARRAY == r->type) && | |
(0 < r->elements))) | |
{ | |
return NULL; | |
} | |
GSList *rv = NULL; | |
for (guint i = 0; i < r->elements; i++) { | |
redisReply *r1 = r->element[i]; | |
if ((NULL != r1) && | |
(REDIS_REPLY_STRING == r1->type)) | |
{ | |
gchar *alias = r1->str + 4; | |
redisReply *r2 = redisCommand(ctx, "GET %s", r1->str); | |
if ((NULL != r2) && | |
(REDIS_REPLY_STRING == r2->type)) | |
{ | |
gchar *url = r2->str; | |
redisReply *r3 = redisCommand(ctx, "GET hits:%s", alias); | |
if (NULL != r3) { | |
guint64 hits; | |
if (REDIS_REPLY_STRING == r3->type) | |
hits = g_ascii_strtoll(r3->str, NULL, 10); | |
else | |
hits = 0; | |
url_t *u = g_new(url_t, 1); | |
u->url = g_strdup(url); | |
u->alias = g_strdup(alias); | |
u->hits = hits; | |
rv = g_slist_insert_sorted(rv, u, | |
(GCompareFunc) redis_compare_url); | |
} | |
freeReplyObject(r3); | |
} | |
freeReplyObject(r2); | |
} | |
} | |
freeReplyObject(r); | |
return rv; | |
} | |
static void | |
redis_free_url(url_t *url) | |
{ | |
if (url == NULL) | |
return; | |
g_free(url->url); | |
g_free(url->alias); | |
g_free(url); | |
} | |
static gchar* | |
redis_get_url(balde_app_t *app, const gchar *alias) | |
{ | |
if (alias == NULL) | |
return NULL; | |
redisContext *ctx = balde_app_get_user_data(app); | |
redisReply *r = redisCommand(ctx, "GET url:%s", alias); | |
if (!((NULL != r) && | |
(REDIS_REPLY_STRING == r->type))) | |
{ | |
freeReplyObject(r); | |
return NULL; | |
} | |
gchar *rv = g_strndup(r->str, r->len); | |
freeReplyObject(r); | |
return rv; | |
} | |
static void | |
redis_hit_url(balde_app_t *app, const gchar *alias) | |
{ | |
redisContext *ctx = balde_app_get_user_data(app); | |
redisReply *r = redisCommand(ctx, "INCR hits:%s", alias); | |
// here we just ignore the reply. if we can't count hits, it should not | |
// affect users' ability to get redirected. | |
freeReplyObject(r); | |
} | |
static balde_response_t* | |
admin_view(balde_app_t *app, balde_request_t *request) | |
{ | |
balde_response_t *rv; | |
if ((NULL == request->authorization) || | |
(0 != g_strcmp0(request->authorization->username, | |
balde_app_get_config(app, "auth_username"))) || | |
(0 != g_strcmp0(request->authorization->password, | |
balde_app_get_config(app, "auth_password")))) | |
{ | |
rv = balde_abort(app, 401); | |
balde_response_set_header(rv, "WWW-Authenticate", | |
"Basic realm=\"Authentication required\""); | |
return rv; | |
} | |
if (request->method == BALDE_HTTP_GET) { | |
rv = balde_make_response(template_base_header); | |
GSList *urls = redis_list_urls(app); | |
if (g_slist_length(urls) == 0) { | |
balde_response_append_body(rv, | |
"<tr><td colspan=\"3\">No URLs registered</td></tr>"); | |
} | |
else { | |
for (GSList *u = urls; u != NULL; u = u->next) { | |
url_t *url = u->data; | |
gchar *alias_url = balde_app_url_for(app, request, "redirect", | |
FALSE, url->alias); | |
gchar *tmp = g_strdup_printf( | |
"<tr>\n" | |
" <td><a href=\"%s\">%s</a></td>\n" | |
" <td><a href=\"%s\">%s</a></td>\n" | |
" <td>%" G_GUINT64_FORMAT "</td>\n" | |
"</tr>\n", alias_url, url->alias, url->url, url->url, | |
url->hits); | |
g_free(alias_url); | |
balde_response_append_body(rv, tmp); | |
g_free(tmp); | |
} | |
} | |
g_slist_free_full(urls, (GDestroyNotify) redis_free_url); | |
balde_response_append_body(rv, template_base_footer); | |
return rv; | |
} | |
else { | |
const gchar *url = balde_request_get_form(request, "url"); | |
if (url == NULL || 0 == strlen(url)) | |
goto redirect; | |
const gchar *alias = balde_request_get_form(request, "alias"); | |
if (alias == NULL || 0 == strlen(alias)) | |
goto redirect; | |
rv = redis_add_url(app, url, alias); | |
if (rv != NULL) | |
return rv; | |
redirect: | |
rv = balde_abort(app, 302); | |
gchar *tmp = balde_app_url_for(app, request, "admin", FALSE); | |
balde_response_set_header(rv, "location", tmp); | |
g_free(tmp); | |
return rv; | |
} | |
} | |
static balde_response_t* | |
redirect_view(balde_app_t *app, balde_request_t *request) | |
{ | |
const gchar *alias = balde_request_get_view_arg(request, "alias"); | |
if (alias == NULL) | |
alias = "default"; | |
gchar *url = redis_get_url(app, alias); | |
if (url == NULL) | |
return balde_abort(app, 404); | |
balde_response_t *rv = balde_abort(app, 302); | |
balde_response_set_header(rv, "location", url); | |
redis_hit_url(app, alias); | |
g_free(url); | |
return rv; | |
} | |
int | |
main(int argc, char **argv) | |
{ | |
balde_app_t *app = balde_app_init(); | |
// config | |
balde_app_set_config_from_envvar(app, "redis_host", "REDIS_HOST", TRUE); | |
balde_app_set_config_from_envvar(app, "redis_port", "REDIS_PORT", TRUE); | |
balde_app_set_config_from_envvar(app, "redis_db", "REDIS_DB", TRUE); | |
balde_app_set_config_from_envvar(app, "auth_username", "AUTH_USERNAME", | |
FALSE); | |
balde_app_set_config_from_envvar(app, "auth_password", "AUTH_PASSWORD", | |
FALSE); | |
// hooks | |
balde_app_set_user_data_destroy_func(app, (GDestroyNotify) redisFree); | |
balde_app_add_before_request(app, redis_connect_hook); | |
// views | |
balde_app_add_url_rule(app, "main", "/", BALDE_HTTP_GET, redirect_view); | |
balde_app_add_url_rule(app, "admin", "/admin", | |
BALDE_HTTP_GET | BALDE_HTTP_POST, admin_view); | |
balde_app_add_url_rule(app, "redirect", "/<alias>", BALDE_HTTP_GET, | |
redirect_view); | |
balde_app_run(app, argc, argv); | |
balde_app_free(app); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment