view comment.c @ 1125:9293874827bf

post: don't leak parsed tag and cat names Sadly, the recent commit (37044617c35deabfe8337a049d2da635bb14075a) did not fix all the reference leaks surrounding the tag and category name s-expression processing. Signed-off-by: Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
author Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
date Sat, 09 Jun 2018 20:06:30 -0400
parents d13f51a2d239
children
line wrap: on
line source

/*
 * Copyright (c) 2009-2018 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/file.h>
#include <unistd.h>
#include <fcntl.h>

#include <jeffpc/error.h>
#include <jeffpc/atomic.h>
#include <jeffpc/sexpr.h>
#include <jeffpc/io.h>
#include <jeffpc/qstring.h>

#include "req.h"
#include "sidebar.h"
#include "render.h"
#include "utils.h"
#include "config.h"
#include "post.h"
#include "debug.h"

#define INTERNAL_ERR		"Ouch!  Encountered an internal error.  " \
				"Please contact me to resolve this issue."
#define GENERIC_ERR_STR		"Somehow, your post is getting rejected. " \
				"If your issues persist, contact me to " \
				"resolve this issue."
#define _REQ_FIELDS		"The required fields are: name, email " \
				"address, the math field, and of course "\
				"the content."
#define USERAGENT_MISSING	"You need to send a user-agent."
#define CAPTCHA_FAIL		"You need to get better at math."
#define MISSING_EMAIL		"You need to supply a valid email address. " \
				_REQ_FIELDS
#define MISSING_NAME		"You do have a name, right? " _REQ_FIELDS
#define MISSING_CONTENT		"No content? " _REQ_FIELDS

#define SHORT_BUF_LEN		128
#define MEDIUM_BUF_LEN		256
#define LONG_BUF_LEN		(64*1024)

/*
 * We use one-letter field names because of historic reasons.  The short
 * names help a bit with transfer sizes, but mostly serve to trivially
 * obfuscate the form from spammers.
 *
 * These defines give reasonable names to the field names.
 */
#define COMMENT_AUTHOR		"a"
#define COMMENT_EMAIL		"e"
#define COMMENT_URL		"u"
#define COMMENT_COMMENT		"c"
#define COMMENT_DATE		"d"
#define COMMENT_ID		"i"
#define COMMENT_CAPTCHA		"v"
#define COMMENT_EMPTY		"x"

static const struct nvl_convert_info comment_convert[] = {
	{ .name = COMMENT_DATE,		.tgt_type = VT_INT, },
	{ .name = COMMENT_ID,		.tgt_type = VT_INT, },
	{ .name = COMMENT_CAPTCHA,	.tgt_type = VT_INT, },
	{ .name = NULL, },
};

static struct str *prep_meta_sexpr(struct str *author, struct str *email,
				   struct str *curdate, struct str *ip,
				   struct str *url)
{
	struct val *lv;
	struct str *str;

	/*
	 * We're looking for a list looking something like:
	 *
	 * '((author . "<author>")
	 *   (email . "<email>")
	 *   (time . "<time>")
	 *   (ip . "<ip>")
	 *   (url . "<url>")
	 *   (moderated . #f))
	 */

	lv = sexpr_args_to_list(6,
				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("author")), str_cast_to_val(author)),
				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("email")), str_cast_to_val(email)),
				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("time")), str_cast_to_val(curdate)),
				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("ip")), str_cast_to_val(ip)),
				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("url")), str_cast_to_val(url)),
				VAL_ALLOC_CONS(str_cast_to_val(STATIC_STR("moderated")), VAL_ALLOC_BOOL(false)));

	str = sexpr_dump(lv, false);

	val_putref(lv);

	return str;
}

static const char *write_out_comment(struct req *req, int id,
				     struct str *email,
				     struct str *author,
				     struct str *url,
				     struct str *comment)
{
	static atomic_t nonce;

	const char *err;

	char basepath[FILENAME_MAX];
	char dirpath[FILENAME_MAX];
	char textpath[FILENAME_MAX];
	char lisppath[FILENAME_MAX];

	char curdate[32];
	int ret;
	struct str *meta;

	uint64_t now, now_nsec;
	time_t now_sec;
	struct tm *now_tm;

	struct nvlist *post;

	post = get_post(req, id, NULL, false);
	if (!post) {
		DBG("Gah! %d (postid=%d)", -1, id);
		err = GENERIC_ERR_STR;
		goto err;
	}
	nvl_putref(post);

	now = gettime();
	now_sec  = now / 1000000000UL;
	now_nsec = now % 1000000000UL;
	now_tm = gmtime(&now_sec);
	if (!now_tm) {
		DBG("Ow, gmtime() returned NULL");
		err = INTERNAL_ERR;
		goto err;
	}

	strftime(curdate, 31, "%Y-%m-%d %H:%M", now_tm);

	snprintf(basepath, FILENAME_MAX, "%s/pending-comments/%d-%08lx.%08"PRIx64".%05x",
		 str_cstr(config.data_dir), id, now_sec, now_nsec,
		 atomic_inc(&nonce));

	snprintf(dirpath,  FILENAME_MAX, "%sW", basepath);
	snprintf(textpath, FILENAME_MAX, "%s/text.txt", dirpath);
	snprintf(lisppath, FILENAME_MAX, "%s/meta.lisp", dirpath);

	ASSERT3U(strlen(dirpath),  <, FILENAME_MAX - 1);
	ASSERT3U(strlen(textpath), <, FILENAME_MAX - 1);
	ASSERT3U(strlen(lisppath), <, FILENAME_MAX - 1);

	ret = xmkdir(dirpath, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
	if (ret) {
		DBG("Ow, could not create directory: %d (%s) '%s'", ret,
		    xstrerror(ret), dirpath);
		err = INTERNAL_ERR;
		goto err;
	}

	ret = write_file(textpath, str_cstr(comment), str_len(comment));
	if (ret) {
		DBG("Couldn't write file ... :( %d (%s) '%s'",
		    ret, xstrerror(ret), textpath);
		err = INTERNAL_ERR;
		goto err;
	}

	meta = prep_meta_sexpr(author, email, STR_DUP(curdate),
			       nvl_lookup_str(req->scgi->request.headers,
					      SCGI_REMOTE_ADDR),
			       url);
	if (!meta) {
		DBG("failed to prep lisp meta data");
		err = INTERNAL_ERR;
		goto err;
	}

	ret = write_file(lisppath, str_cstr(meta), str_len(meta));

	str_putref(meta);

	if (ret) {
		DBG("Couldn't write file ... :( %d (%s) '%s'",
		    ret, xstrerror(ret), lisppath);
		err = INTERNAL_ERR;
		goto err;
	}

	ret = xrename(dirpath, basepath);
	if (ret) {
		DBG("Could not rename '%s' to '%s' %d (%s)",
		    dirpath, basepath, ret, xstrerror(ret));
		err = INTERNAL_ERR;
		goto err;
	}

	return NULL;

err:
	str_putref(author);
	str_putref(email);
	str_putref(url);
	str_putref(comment);

	return err;
}

static const char *get_postid(struct nvlist *qs, int *id_r)
{
	uint64_t id;
	int ret;

	ret = nvl_lookup_int(qs, COMMENT_ID, &id);
	if (ret) {
		DBG("failed to look up id: %s", xstrerror(ret));
		return GENERIC_ERR_STR;
	}

	if (id > INT_MAX) {
		DBG("postid:%"PRIu64" > INT_MAX", id);
		return GENERIC_ERR_STR;
	}

	*id_r = id;

	return NULL;
}

static const char *spam_check(int id, struct nvlist *qs)
{
	uint64_t now, date;
	uint64_t captcha;
	uint64_t deltat;
	struct str *str;
	bool nonempty;
	int ret;

	/*
	 * Check empty field.
	 */
	str = nvl_lookup_str(qs, COMMENT_EMPTY);
	if (IS_ERR(str)) {
		DBG("failed to look up captcha (postid:%u): %s", id,
		    xstrerror(PTR_ERR(str)));
		return GENERIC_ERR_STR;
	}

	nonempty = str_len(str) != 0;
	str_putref(str);

	if (nonempty) {
		DBG("User filled out supposedly empty field... postid:%u", id);
		return GENERIC_ERR_STR;
	}

	/*
	 * Check think time.
	 */
	ret = nvl_lookup_int(qs, COMMENT_DATE, &date);
	if (ret) {
		DBG("failed to look up date (postid:%u): %s", id,
		    xstrerror(ret));
		return GENERIC_ERR_STR;
	}

	now = gettime();
	deltat = now - date;

	if ((deltat > 1000000000ull * config.comment_max_think) ||
	    (deltat < 1000000000ull * config.comment_min_think)) {
		DBG("Flash-gordon or geriatric was here... load:%"PRIu64
		    " comment:%"PRIu64" delta:%"PRIu64" postid:%u",
		    date, now, deltat, id);
		return GENERIC_ERR_STR;
	}

	/*
	 * Check captcha.
	 */
	ret = nvl_lookup_int(qs, COMMENT_CAPTCHA, &captcha);
	if (ret) {
		DBG("failed to look up captcha (postid:%u): %s", id,
		    xstrerror(ret));
		return GENERIC_ERR_STR;
	}

	if (captcha != (config.comment_captcha_a + config.comment_captcha_b)) {
		DBG("Math illiterate was here... got:%"PRIu64
		    " expected:%"PRIu64" postid:%u", captcha,
		    config.comment_captcha_a + config.comment_captcha_b,
		    id);
		return CAPTCHA_FAIL;
	}

	return NULL;
}

static const char *get_str(int id, struct nvlist *nvl, const char *name,
			   const char *dbg_name, const char *err,
			   struct str **str_r)
{
	struct str *str;

	str = nvl_lookup_str(nvl, name);
	if (IS_ERR(str)) {
		DBG("%s: failed to look up %s (postid=%u): %s", __func__,
		    dbg_name, id, xstrerror(PTR_ERR(str)));
		return GENERIC_ERR_STR;
	}

	if (str_len(str) == 0) {
		if (err) {
			DBG("%s: must fill in %s (postid=%u)", __func__, dbg_name,
			    id);
			str_putref(str);
			return err;
		}

		/* turn empty strings to NULL */
		str_putref(str);
		str = NULL;
	}

	*str_r = str;

	return NULL;
}

static const char *get_strings(int id,
			       struct nvlist *qs,
			       struct str **email_r,
			       struct str **author_r,
			       struct str **comment_r,
			       struct str **url_r)
{
	struct str *author;
	struct str *email;
	struct str *comment;
	struct str *url;
	const char *err;

	err = get_str(id, qs, COMMENT_EMAIL, "email", MISSING_EMAIL, &email);
	if (err)
		goto err;

	err = get_str(id, qs, COMMENT_AUTHOR, "author", MISSING_NAME, &author);
	if (err)
		goto err_email;

	err = get_str(id, qs, COMMENT_COMMENT, "comment", MISSING_CONTENT,
		      &comment);
	if (err)
		goto err_author;

	err = get_str(id, qs, COMMENT_URL, "url", NULL, &url);
	if (err)
		goto err_comment;

	*email_r = email;
	*author_r = author;
	*comment_r = comment;
	*url_r = url;

	return NULL;

err_comment:
	str_putref(comment);

err_author:
	str_putref(author);

err_email:
	str_putref(email);

err:
	return err;
}

static const char *save_comment(struct req *req)
{
	struct nvlist *qs;
	struct str *author;
	struct str *email;
	struct str *url;
	struct str *comment;
	const char *err;
	int ret;
	int id;

	if (nvl_exists_type(req->scgi->request.headers, SCGI_HTTP_USER_AGENT,
			    VT_STR)) {
		DBG("Missing user agent...");
		return USERAGENT_MISSING;
	}

	if (nvl_exists_type(req->scgi->request.headers, SCGI_REMOTE_ADDR,
			    VT_STR)) {
		DBG("Missing remote addr...");
		return INTERNAL_ERR;
	}

	if (!req->scgi->request.body) {
		DBG("missing req. body");
		return INTERNAL_ERR;
	}

	qs = nvl_alloc();
	if (!qs) {
		DBG("failed to allocate nvlist");
		return INTERNAL_ERR;
	}

	err = GENERIC_ERR_STR;

	ret = qstring_parse(qs, req->scgi->request.body);
	if (ret) {
		DBG("failed to parse comment: %s", xstrerror(ret));
		goto err;
	}

	ret = nvl_convert(qs, comment_convert, false);
	if (ret) {
		DBG("Failed to convert nvlist types: %s", xstrerror(ret));
		goto err;
	}

	err = get_postid(qs, &id);
	if (err)
		goto err;

	err = spam_check(id, qs);
	if (err)
		goto err;

	err = get_strings(id, qs, &email, &author, &comment, &url);
	if (err)
		goto err;

	/* consumes all the string refs */
	err = write_out_comment(req, id, email, author, url, comment);

err:
	nvl_putref(qs);
	return err;
}

int blahg_comment(struct req *req)
{
	const char *errmsg;
	char *tmpl;

	req_head(req, "Content-Type", "text/html");

	sidebar(req);

	vars_scope_push(&req->vars);

	errmsg = save_comment(req);
	if (errmsg) {
		tmpl = "{comment_error}";
		vars_set_str(&req->vars, "comment_error_str", STR_DUP(errmsg));
		// FIXME: __store_title(&req->vars, "Error");
	} else {
		tmpl = "{comment_saved}";
	}

	req->scgi->response.body = render_page(req, tmpl);

	return 0;
}