Skip to content

Instantly share code, notes, and snippets.

@rafaelmartins
Last active June 8, 2016 13:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rafaelmartins/9f8392a8909e62820ae0 to your computer and use it in GitHub Desktop.
Save rafaelmartins/9f8392a8909e62820ae0 to your computer and use it in GitHub Desktop.
chunda - A simple URL manager, that counts URL hits.
/*
* 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