[BACK]Return to server_file.c CVS log [TXT][DIR] Up to [local] / src / usr.sbin / httpd

File: [local] / src / usr.sbin / httpd / server_file.c (download)

Revision 1.80, Mon Apr 29 16:17:46 2024 UTC (5 weeks, 5 days ago) by florian
Branch: MAIN
CVS Tags: HEAD
Changes since 1.79: +2 -1 lines

plug fd leak introduced in "avoid toctu" rewrite.

Issue reported by job
OK tb, job

/*	$OpenBSD: server_file.c,v 1.80 2024/04/29 16:17:46 florian Exp $	*/

/*
 * Copyright (c) 2006 - 2017 Reyk Floeter <reyk@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>

#include <limits.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <time.h>
#include <event.h>
#include <util.h>

#include "httpd.h"
#include "http.h"
#include "css.h"
#include "js.h"

#define MINIMUM(a, b)	(((a) < (b)) ? (a) : (b))
#define MAXIMUM(a, b)	(((a) > (b)) ? (a) : (b))

int		 server_file_access(struct httpd *, struct client *,
		    char *, size_t);
int		 server_file_request(struct httpd *, struct client *,
		    struct media_type *, int, const struct stat *);
int		 server_partial_file_request(struct httpd *, struct client *,
		    struct media_type *, int, const struct stat *,
		    char *);
int		 server_file_index(struct httpd *, struct client *, int,
		    struct stat *);
int		 server_file_modified_since(struct http_descriptor *,
		    const struct timespec *);
int		 server_file_method(struct client *);
int		 parse_range_spec(char *, size_t, struct range *);
int		 parse_ranges(struct client *, char *, size_t);
static int	 select_visible(const struct dirent *);

int
server_file_access(struct httpd *env, struct client *clt,
    char *path, size_t len)
{
	struct http_descriptor	*desc = clt->clt_descreq;
	struct server_config	*srv_conf = clt->clt_srv_conf;
	struct stat		 st;
	struct kv		*r, key;
	struct media_type	*media;
	char			*newpath, *encodedpath;
	int			 ret, fd;

	if ((fd = open(path, O_RDONLY)) == -1) {
		switch (errno) {
		case ENOENT:
		case ENOTDIR:
			return (404);
		case EACCES:
			return (403);
		default:
			return (500);
		}
	}
	if (fstat(fd, &st) == -1) {
		close(fd);
		return (500);
	}

	if (S_ISDIR(st.st_mode)) {
		/* Deny access if directory indexing is disabled */
		if (srv_conf->flags & SRVFLAG_NO_INDEX) {
			close(fd);
			return (403);
		}

		if (desc->http_path_alias != NULL) {
			/* Recursion - the index "file" is a directory? */
			close(fd);
			return (500);
		}

		/* Redirect to path with trailing "/" */
		if (path[strlen(path) - 1] != '/') {
			close(fd);
			if ((encodedpath = url_encode(desc->http_path)) == NULL)
				return (500);
			if (asprintf(&newpath, "%s/", encodedpath) == -1) {
				free(encodedpath);
				return (500);
			}
			free(encodedpath);

			/* Path alias will be used for the redirection */
			desc->http_path_alias = newpath;

			/* Indicate that the file has been moved */
			return (301);
		}

		/* Append the default index file to the location */
		if (asprintf(&newpath, "%s%s", desc->http_path,
		    srv_conf->index) == -1) {
			close(fd);
			return (500);
		}
		desc->http_path_alias = newpath;
		if (server_getlocation(clt, newpath) != srv_conf) {
			/* The location has changed */
			close(fd);
			return (server_file(env, clt));
		}

		/* Otherwise append the default index file to the path */
		if (strlcat(path, srv_conf->index, len) >= len) {
			close(fd);
			return (403);
		}

		ret = server_file_access(env, clt, path, len);
		if (ret == 404) {
			/*
			 * Index file not found; fail if auto-indexing is
			 * not enabled, otherwise return success but
			 * indicate directory with S_ISDIR of the previous
			 * stat.
			 */
			if ((srv_conf->flags & SRVFLAG_AUTO_INDEX) == 0) {
				close(fd);
				return (403);
			}

			return (server_file_index(env, clt, fd, &st));
		}
		close(fd);
		return (ret);
	} else if (!S_ISREG(st.st_mode)) {
		/* Don't follow symlinks and ignore special files */
		close(fd);
		return (403);
	}

	media = media_find_config(env, srv_conf, path);

	/* Only consider range requests for GET */
	if (desc->http_method == HTTP_METHOD_GET) {
		key.kv_key = "Range";
		r = kv_find(&desc->http_headers, &key);
		if (r != NULL)
			return (server_partial_file_request(env, clt, media,
			    fd, &st, r->kv_value));
	}

	/* change path to path.gz if necessary. */
	if (srv_conf->flags & SRVFLAG_GZIP_STATIC) {
		struct http_descriptor	*req = clt->clt_descreq;
		struct http_descriptor	*resp = clt->clt_descresp;
		struct stat		 gzst;
		int			 gzfd;
		char			 gzpath[PATH_MAX];

		/* check Accept-Encoding header */
		key.kv_key = "Accept-Encoding";
		r = kv_find(&req->http_headers, &key);

		if (r != NULL && strstr(r->kv_value, "gzip") != NULL) {
			/* append ".gz" to path and check existence */
			ret = snprintf(gzpath, sizeof(gzpath), "%s.gz", path);
			if (ret < 0 || (size_t)ret >= sizeof(gzpath)) {
				close(fd);
				return (500);
			}

			if ((gzfd = open(gzpath, O_RDONLY)) != -1) {
				/* .gz must be a file, and not older */
				if (fstat(gzfd, &gzst) != -1 &&
				    S_ISREG(gzst.st_mode) &&
				    timespeccmp(&gzst.st_mtim, &st.st_mtim,
				    >=)) {
					kv_add(&resp->http_headers,
					    "Content-Encoding", "gzip");
					/* Use original file timestamp */
					gzst.st_mtim = st.st_mtim;
					st = gzst;
					close(fd);
					fd = gzfd;
				} else {
					close(gzfd);
				}
			}
		}
	}

	return (server_file_request(env, clt, media, fd, &st));
}

int
server_file(struct httpd *env, struct client *clt)
{
	struct http_descriptor	*desc = clt->clt_descreq;
	struct server_config	*srv_conf = clt->clt_srv_conf;
	char			 path[PATH_MAX];
	const char		*stripped, *errstr = NULL;
	int			 ret = 500;

	if (srv_conf->flags & SRVFLAG_FCGI)
		return (server_fcgi(env, clt));

	/* Request path is already canonicalized */
	stripped = server_root_strip(
	    desc->http_path_alias != NULL ?
	    desc->http_path_alias : desc->http_path,
	    srv_conf->strip);
	if ((size_t)snprintf(path, sizeof(path), "%s%s",
	    srv_conf->root, stripped) >= sizeof(path)) {
		errstr = desc->http_path;
		goto abort;
	}

	/* Returns HTTP status code on error */
	if ((ret = server_file_access(env, clt, path, sizeof(path))) > 0) {
		errstr = desc->http_path_alias != NULL ?
		    desc->http_path_alias : desc->http_path;
		goto abort;
	}

	return (ret);

 abort:
	if (errstr == NULL)
		errstr = strerror(errno);
	server_abort_http(clt, ret, errstr);
	return (-1);
}

int
server_file_method(struct client *clt)
{
	struct http_descriptor	*desc = clt->clt_descreq;

	switch (desc->http_method) {
	case HTTP_METHOD_GET:
	case HTTP_METHOD_HEAD:
		return (0);
	default:
		/* Other methods are not allowed */
		errno = EACCES;
		return (405);
	}
	/* NOTREACHED */
}

int
server_file_request(struct httpd *env, struct client *clt, struct media_type
    *media, int fd, const struct stat *st)
{
	struct server_config	*srv_conf = clt->clt_srv_conf;
	const char		*errstr = NULL;
	int			 ret, code = 500;
	size_t			 bufsiz;

	if ((ret = server_file_method(clt)) != 0) {
		code = ret;
		goto abort;
	}

	if ((ret = server_file_modified_since(clt->clt_descreq, &st->st_mtim))
	    != -1) {
		/* send the header without a body */
		if ((ret = server_response_http(clt, ret, media, -1,
		    MINIMUM(time(NULL), st->st_mtim.tv_sec))) == -1)
			goto fail;
		close(fd);
		goto done;
	}

	ret = server_response_http(clt, 200, media, st->st_size,
	    MINIMUM(time(NULL), st->st_mtim.tv_sec));
	switch (ret) {
	case -1:
		goto fail;
	case 0:
		/* Connection is already finished */
		close(fd);
		goto done;
	default:
		break;
	}

	clt->clt_fd = fd;
	if (clt->clt_srvbev != NULL)
		bufferevent_free(clt->clt_srvbev);

	clt->clt_srvbev_throttled = 0;
	clt->clt_srvbev = bufferevent_new(clt->clt_fd, server_read,
	    server_write, server_file_error, clt);
	if (clt->clt_srvbev == NULL) {
		errstr = "failed to allocate file buffer event";
		goto fail;
	}

	/* Adjust read watermark to the optimal file io size */
	bufsiz = MAXIMUM(st->st_blksize, 64 * 1024);
	bufferevent_setwatermark(clt->clt_srvbev, EV_READ, 0,
	    bufsiz);

	bufferevent_settimeout(clt->clt_srvbev,
	    srv_conf->timeout.tv_sec, srv_conf->timeout.tv_sec);
	bufferevent_enable(clt->clt_srvbev, EV_READ);
	bufferevent_disable(clt->clt_bev, EV_READ);

 done:
	server_reset_http(clt);
	return (0);
 fail:
	bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE);
	bufferevent_free(clt->clt_bev);
	clt->clt_bev = NULL;
 abort:
	if (fd != -1)
		close(fd);
	if (errstr == NULL)
		errstr = strerror(errno);
	server_abort_http(clt, code, errstr);
	return (-1);
}

int
server_partial_file_request(struct httpd *env, struct client *clt,
    struct media_type *media, int fd, const struct stat *st, char *range_str)
{
	struct server_config	*srv_conf = clt->clt_srv_conf;
	struct http_descriptor	*resp = clt->clt_descresp;
	struct media_type	 multipart_media;
	struct range_data	*r = &clt->clt_ranges;
	struct range		*range;
	size_t			 content_length = 0, bufsiz;
	int			 code = 500, i, nranges, ret;
	char			 content_range[64];
	const char		*errstr = NULL;

	if ((nranges = parse_ranges(clt, range_str, st->st_size)) < 1) {
		code = 416;
		(void)snprintf(content_range, sizeof(content_range),
		    "bytes */%lld", st->st_size);
		errstr = content_range;
		goto abort;
	}

	r->range_media = media;

	if (nranges == 1) {
		range = &r->range[0];
		(void)snprintf(content_range, sizeof(content_range),
		    "bytes %lld-%lld/%lld", range->start, range->end,
		    st->st_size);
		if (kv_add(&resp->http_headers, "Content-Range",
		    content_range) == NULL)
			goto abort;

		range = &r->range[0];
		content_length += range->end - range->start + 1;
	} else {
		/* Add boundary, all parts will be handled by the callback */
		arc4random_buf(&clt->clt_boundary, sizeof(clt->clt_boundary));

		/* Calculate Content-Length of the complete multipart body */
		for (i = 0; i < nranges; i++) {
			range = &r->range[i];

			/* calculate Content-Length of the complete body */
			if ((ret = snprintf(NULL, 0,
			    "\r\n--%llu\r\n"
			    "Content-Type: %s/%s\r\n"
			    "Content-Range: bytes %lld-%lld/%lld\r\n\r\n",
			    clt->clt_boundary,
			    media->media_type, media->media_subtype,
			    range->start, range->end, st->st_size)) < 0)
				goto abort;

			/* Add data length */
			content_length += ret + range->end - range->start + 1;

		}
		if ((ret = snprintf(NULL, 0, "\r\n--%llu--\r\n",
		    clt->clt_boundary)) < 0)
			goto abort;
		content_length += ret;

		/* prepare multipart/byteranges media type */
		(void)strlcpy(multipart_media.media_type, "multipart",
		    sizeof(multipart_media.media_type));
		(void)snprintf(multipart_media.media_subtype,
		    sizeof(multipart_media.media_subtype),
		    "byteranges; boundary=%llu", clt->clt_boundary);
		media = &multipart_media;
	}

	/* Start with first range */
	r->range_toread = TOREAD_HTTP_RANGE;

	ret = server_response_http(clt, 206, media, content_length,
	    MINIMUM(time(NULL), st->st_mtim.tv_sec));
	switch (ret) {
	case -1:
		goto fail;
	case 0:
		/* Connection is already finished */
		close(fd);
		goto done;
	default:
		break;
	}

	clt->clt_fd = fd;
	if (clt->clt_srvbev != NULL)
		bufferevent_free(clt->clt_srvbev);

	clt->clt_srvbev_throttled = 0;
	clt->clt_srvbev = bufferevent_new(clt->clt_fd, server_read_httprange,
	    server_write, server_file_error, clt);
	if (clt->clt_srvbev == NULL) {
		errstr = "failed to allocate file buffer event";
		goto fail;
	}

	/* Adjust read watermark to the optimal file io size */
	bufsiz = MAXIMUM(st->st_blksize, 64 * 1024);
	bufferevent_setwatermark(clt->clt_srvbev, EV_READ, 0,
	    bufsiz);

	bufferevent_settimeout(clt->clt_srvbev,
	    srv_conf->timeout.tv_sec, srv_conf->timeout.tv_sec);
	bufferevent_enable(clt->clt_srvbev, EV_READ);
	bufferevent_disable(clt->clt_bev, EV_READ);

 done:
	server_reset_http(clt);
	return (0);
 fail:
	bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE);
	bufferevent_free(clt->clt_bev);
	clt->clt_bev = NULL;
 abort:
	if (fd != -1)
		close(fd);
	if (errstr == NULL)
		errstr = strerror(errno);
	server_abort_http(clt, code, errstr);
	return (-1);
}

/* ignore hidden files starting with a dot */
static int 
select_visible(const struct dirent *dp)
{
    if (dp->d_name[0] == '.' &&
	!(dp->d_name[1] == '.' && dp->d_name[2] == '\0'))
	    return 0;
    else
	    return 1;
}

int
server_file_index(struct httpd *env, struct client *clt, int fd,
    struct stat *st)
{
	char			  path[PATH_MAX];
	char			  tmstr[21];
	struct http_descriptor	 *desc = clt->clt_descreq;
	struct server_config	 *srv_conf = clt->clt_srv_conf;
	struct dirent		**namelist, *dp;
	int			  namesize, i, ret, skip;
	int			  code = 500;
	struct evbuffer		 *evb = NULL;
	struct media_type	 *media;
	const char		 *stripped;
	char			 *escapeduri, *escapedhtml, *escapedpath;
	struct tm		  tm;
	time_t			  t, dir_mtime;
	char 			  human_size[FMT_SCALED_STRSIZE];

	if ((ret = server_file_method(clt)) != 0) {
		code = ret;
		goto abort;
	}

	/* Request path is already canonicalized */
	stripped = server_root_strip(desc->http_path, srv_conf->strip);
	if ((size_t)snprintf(path, sizeof(path), "%s%s",
	    srv_conf->root, stripped) >= sizeof(path))
		goto abort;

	/* Save last modification time */
	dir_mtime = MINIMUM(time(NULL), st->st_mtim.tv_sec);

	if ((evb = evbuffer_new()) == NULL)
		goto abort;

	if ((escapedpath = escape_html(desc->http_path)) == NULL)
		goto abort;

	/* Generate simple HTML index document */
	if (evbuffer_add_printf(evb,
	    "<!DOCTYPE html>\n"
	    "<html lang=\"en\">\n"
	    "<head>\n"
	    "<meta charset=\"utf-8\">\n"
	    "<title>Index of %s</title>\n"
	    "<style><!--\n%s--></style>\n"
	    "</head>\n"
	    "<body>\n"
	    "<h1>Index of %s</h1>\n"
	    "<table><thead>\n"
	    "<tr class=\"sort\"><th class=\"sorted\">Name</th>\n"
	    "    <th>Date</th><th>Size</th></tr>\n"
	    "</thead><tbody>\n",
	    escapedpath, css, escapedpath) == -1) {
		free(escapedpath);
		goto abort;
	}

	free(escapedpath);

	if ((namesize = scandirat(fd, ".", &namelist, select_visible,
	    alphasort)) == -1)
		goto abort;

	/* Indicate failure but continue going through the list */
	skip = 0;

	for (i = 0; i < namesize; i++) {
		struct stat subst;

		dp = namelist[i];

		if (skip ||
		    fstatat(fd, dp->d_name, &subst, 0) == -1) {
			free(dp);
			continue;
		}

		t = subst.st_mtime;
		localtime_r(&t, &tm);
		strftime(tmstr, sizeof(tmstr), "%d-%h-%Y %R", &tm);

		if ((escapeduri = url_encode(dp->d_name)) == NULL) {
			skip = 1;
			free(dp);
			continue;
		}
		if ((escapedhtml = escape_html(dp->d_name)) == NULL) {
			skip = 1;
			free(escapeduri);
			free(dp);
			continue;
		}

		if (S_ISDIR(subst.st_mode)) {
			if (evbuffer_add_printf(evb,
			    "<tr class=\"dir\">"
			    "<td><a href=\"%s%s/\">%s/</a></td>\n"
			    "    <td data-o=\"%lld\">%s</td><td>%s</td></tr>\n",
			    strchr(escapeduri, ':') != NULL ? "./" : "",
			    escapeduri, escapedhtml, 
			    (long long)t, tmstr, "-") == -1)
				skip = 1;
		} else if (S_ISREG(subst.st_mode)) {
			if ((fmt_scaled(subst.st_size, human_size) != 0) ||
			   (evbuffer_add_printf(evb,
			    "<tr><td><a href=\"%s%s\">%s</a></td>\n"
			    "    <td data-o=\"%lld\">%s</td>"
			    "<td title=\"%llu\">%s</td></tr>\n",
			    strchr(escapeduri, ':') != NULL ? "./" : "",
			    escapeduri, escapedhtml,
			    (long long)t, tmstr, 
			    subst.st_size, human_size) == -1))
				skip = 1;
		}
		free(escapeduri);
		free(escapedhtml);
		free(dp);
	}
	free(namelist);

	if (skip ||
	    evbuffer_add_printf(evb,
	    "</tbody></table>\n<script>\n"
	    "%s\n"
	    "</script>\n</body>\n</html>\n", js) == -1)
		goto abort;

	close(fd);
	fd = -1;

	media = media_find_config(env, srv_conf, "index.html");
	ret = server_response_http(clt, 200, media, EVBUFFER_LENGTH(evb),
	    dir_mtime);
	switch (ret) {
	case -1:
		goto fail;
	case 0:
		/* Connection is already finished */
		evbuffer_free(evb);
		goto done;
	default:
		break;
	}

	if (server_bufferevent_write_buffer(clt, evb) == -1)
		goto fail;
	evbuffer_free(evb);
	evb = NULL;

	bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);
	if (clt->clt_persist)
		clt->clt_toread = TOREAD_HTTP_HEADER;
	else
		clt->clt_toread = TOREAD_HTTP_NONE;
	clt->clt_done = 0;

 done:
	server_reset_http(clt);
	return (0);
 fail:
	bufferevent_disable(clt->clt_bev, EV_READ|EV_WRITE);
	bufferevent_free(clt->clt_bev);
	clt->clt_bev = NULL;
 abort:
	if (fd != -1)
		close(fd);
	if (evb != NULL)
		evbuffer_free(evb);
	server_abort_http(clt, code, desc->http_path);
	return (-1);
}

void
server_file_error(struct bufferevent *bev, short error, void *arg)
{
	struct client		*clt = arg;
	struct evbuffer		*src, *dst;

	if (error & EVBUFFER_TIMEOUT) {
		server_close(clt, "buffer event timeout");
		return;
	}
	if (error & EVBUFFER_ERROR) {
		if (errno == EFBIG) {
			bufferevent_enable(bev, EV_READ);
			return;
		}
		server_close(clt, "buffer event error");
		return;
	}
	if (error & (EVBUFFER_READ|EVBUFFER_WRITE|EVBUFFER_EOF)) {
		bufferevent_disable(bev, EV_READ|EV_WRITE);

		clt->clt_done = 1;

		src = EVBUFFER_INPUT(clt->clt_bev);

		/* Close the connection if a previous pipeline is empty */
		if (clt->clt_pipelining && EVBUFFER_LENGTH(src) == 0)
			clt->clt_persist = 0;

		if (clt->clt_persist) {
			/* Close input file and wait for next HTTP request */
			if (clt->clt_fd != -1)
				close(clt->clt_fd);
			clt->clt_fd = -1;
			clt->clt_toread = TOREAD_HTTP_HEADER;
			server_reset_http(clt);
			bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);

			/* Start pipelining if the buffer is not empty */
			if (EVBUFFER_LENGTH(src)) {
				clt->clt_pipelining++;
				server_read_http(clt->clt_bev, arg);
			}
			return;
		}

		dst = EVBUFFER_OUTPUT(clt->clt_bev);
		if (EVBUFFER_LENGTH(dst)) {
			/* Finish writing all data first */
			bufferevent_enable(clt->clt_bev, EV_WRITE);
			return;
		}

		server_close(clt, "done");
		return;
	}
	server_close(clt, "unknown event error");
	return;
}

int
server_file_modified_since(struct http_descriptor *desc, const struct timespec
    *mtim)
{
	struct kv	 key, *since;
	struct tm	 tm;

	key.kv_key = "If-Modified-Since";
	if ((since = kv_find(&desc->http_headers, &key)) != NULL &&
	    since->kv_value != NULL) {
		memset(&tm, 0, sizeof(struct tm));

		/*
		 * Return "Not modified" if the file hasn't changed since
		 * the requested time.
		 */
		if (strptime(since->kv_value,
		    "%a, %d %h %Y %T %Z", &tm) != NULL &&
		    timegm(&tm) >= mtim->tv_sec)
			return (304);
	}

	return (-1);
}

int
parse_ranges(struct client *clt, char *str, size_t file_sz)
{
	int			 i = 0;
	char			*p, *q;
	struct range_data	*r = &clt->clt_ranges;

	memset(r, 0, sizeof(*r));

	/* Extract range unit */
	if ((p = strchr(str, '=')) == NULL)
		return (-1);

	*p++ = '\0';
	/* Check if it's a bytes range spec */
	if (strcmp(str, "bytes") != 0)
		return (-1);

	while ((q = strchr(p, ',')) != NULL) {
		*q++ = '\0';

		/* Extract start and end positions */
		if (parse_range_spec(p, file_sz, &r->range[i]) == 0)
			continue;

		i++;
		if (i == SERVER_MAX_RANGES)
			return (-1);

		p = q;
	}

	if (parse_range_spec(p, file_sz, &r->range[i]) != 0)
		i++;

	r->range_total = file_sz;
	r->range_count = i;
	return (i);
}

int
parse_range_spec(char *str, size_t size, struct range *r)
{
	size_t		 start_str_len, end_str_len;
	char		*p, *start_str, *end_str;
	const char	*errstr;

	if ((p = strchr(str, '-')) == NULL)
		return (0);

	*p++ = '\0';
	start_str = str;
	end_str = p;
	start_str_len = strlen(start_str);
	end_str_len = strlen(end_str);

	/* Either 'start' or 'end' is optional but not both */
	if ((start_str_len == 0) && (end_str_len == 0))
		return (0);

	if (end_str_len) {
		r->end = strtonum(end_str, 0, LLONG_MAX, &errstr);
		if (errstr)
			return (0);

		if ((size_t)r->end >= size)
			r->end = size - 1;
	} else
		r->end = size - 1;

	if (start_str_len) {
		r->start = strtonum(start_str, 0, LLONG_MAX, &errstr);
		if (errstr)
			return (0);

		if ((size_t)r->start >= size)
			return (0);
	} else {
		r->start = size - r->end;
		r->end = size - 1;
	}

	if (r->end < r->start)
		return (0);

	return (1);
}