lighttpd 1.4.x https://www.lighttpd.net/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

822 lines
18 KiB

#include "first.h"
#include "log.h"
#include "stat_cache.h"
#include "fdevent.h"
#include "etag.h"
#include "splaytree.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#ifdef HAVE_ATTR_ATTRIBUTES_H
# include <attr/attributes.h>
#endif
#ifdef HAVE_SYS_EXTATTR_H
# include <sys/extattr.h>
#endif
#ifdef HAVE_FAM_H
# include <fam.h>
#endif
#ifndef HAVE_LSTAT
# define lstat stat
#endif
#if 0
/* enables debug code for testing if all nodes in the stat-cache as accessable */
#define DEBUG_STAT_CACHE
#endif
/*
* stat-cache
*
* we cache the stat() calls in our own storage
* the directories are cached in FAM
*
* if we get a change-event from FAM, we increment the version in the FAM->dir mapping
*
* if the stat()-cache is queried we check if the version id for the directory is the
* same and return immediatly.
*
*
* What we need:
*
* - for each stat-cache entry we need a fast indirect lookup on the directory name
* - for each FAMRequest we have to find the version in the directory cache (index as userdata)
*
* stat <<-> directory <-> FAMRequest
*
* if file is deleted, directory is dirty, file is rechecked ...
* if directory is deleted, directory mapping is removed
*
* */
#ifdef HAVE_FAM_H
typedef struct {
FAMRequest *req;
buffer *name;
int version;
} fam_dir_entry;
#endif
/* the directory name is too long to always compare on it
* - we need a hash
* - the hash-key is used as sorting criteria for a tree
* - a splay-tree is used as we can use the caching effect of it
*/
/* we want to cleanup the stat-cache every few seconds, let's say 10
*
* - remove entries which are outdated since 30s
* - remove entries which are fresh but havn't been used since 60s
* - if we don't have a stat-cache entry for a directory, release it from the monitor
*/
#ifdef DEBUG_STAT_CACHE
typedef struct {
int *ptr;
size_t used;
size_t size;
} fake_keys;
static fake_keys ctrl;
#endif
typedef struct stat_cache {
splay_tree *files; /* the nodes of the tree are stat_cache_entry's */
buffer *dir_name; /* for building the dirname from the filename */
#ifdef HAVE_FAM_H
splay_tree *dirs; /* the nodes of the tree are fam_dir_entry */
FAMConnection fam;
int fam_fcce_ndx;
#endif
buffer *hash_key; /* temp-store for the hash-key */
} stat_cache;
static handler_t stat_cache_handle_fdevent(server *srv, void *_fce, int revent);
stat_cache *stat_cache_init(server *srv) {
stat_cache *sc = NULL;
sc = calloc(1, sizeof(*sc));
force_assert(NULL != sc);
sc->dir_name = buffer_init();
sc->hash_key = buffer_init();
#ifdef HAVE_FAM_H
sc->fam_fcce_ndx = -1;
#endif
#ifdef DEBUG_STAT_CACHE
ctrl.size = 0;
#endif
#ifdef HAVE_FAM_H
/* setup FAM */
if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_FAM) {
if (0 != FAMOpen2(&sc->fam, "lighttpd")) {
log_error_write(srv, __FILE__, __LINE__, "s",
"could not open a fam connection, dieing.");
free(sc);
return NULL;
}
#ifdef HAVE_FAMNOEXISTS
FAMNoExists(&sc->fam);
#endif
fd_close_on_exec(FAMCONNECTION_GETFD(&sc->fam));
fdevent_register(srv->ev, FAMCONNECTION_GETFD(&sc->fam), stat_cache_handle_fdevent, NULL);
fdevent_event_set(srv->ev, &(sc->fam_fcce_ndx), FAMCONNECTION_GETFD(&sc->fam), FDEVENT_IN);
}
#endif
return sc;
}
static stat_cache_entry * stat_cache_entry_init(void) {
stat_cache_entry *sce = NULL;
sce = calloc(1, sizeof(*sce));
force_assert(NULL != sce);
sce->name = buffer_init();
sce->etag = buffer_init();
sce->content_type = buffer_init();
return sce;
}
static void stat_cache_entry_free(void *data) {
stat_cache_entry *sce = data;
if (!sce) return;
buffer_free(sce->etag);
buffer_free(sce->name);
buffer_free(sce->content_type);
free(sce);
}
#ifdef HAVE_FAM_H
static fam_dir_entry * fam_dir_entry_init(void) {
fam_dir_entry *fam_dir = NULL;
fam_dir = calloc(1, sizeof(*fam_dir));
force_assert(NULL != fam_dir);
fam_dir->name = buffer_init();
return fam_dir;
}
static void fam_dir_entry_free(FAMConnection *fc, void *data) {
fam_dir_entry *fam_dir = data;
if (!fam_dir) return;
FAMCancelMonitor(fc, fam_dir->req);
buffer_free(fam_dir->name);
free(fam_dir->req);
free(fam_dir);
}
#endif
void stat_cache_free(stat_cache *sc) {
while (sc->files) {
int osize;
splay_tree *node = sc->files;
osize = sc->files->size;
stat_cache_entry_free(node->data);
sc->files = splaytree_delete(sc->files, node->key);
force_assert(osize - 1 == splaytree_size(sc->files));
}
buffer_free(sc->dir_name);
buffer_free(sc->hash_key);
#ifdef HAVE_FAM_H
while (sc->dirs) {
int osize;
splay_tree *node = sc->dirs;
osize = sc->dirs->size;
fam_dir_entry_free(&sc->fam, node->data);
sc->dirs = splaytree_delete(sc->dirs, node->key);
if (osize == 1) {
force_assert(NULL == sc->dirs);
} else {
force_assert(osize == (sc->dirs->size + 1));
}
}
if (-1 != sc->fam_fcce_ndx) {
/* fd events already gone */
sc->fam_fcce_ndx = -1;
FAMClose(&sc->fam);
}
#endif
free(sc);
}
#if defined(HAVE_XATTR)
static int stat_cache_attr_get(buffer *buf, char *name, char *xattrname) {
int attrlen;
int ret;
buffer_string_prepare_copy(buf, 1023);
fix buffer, chunk and http_chunk API * remove unused structs and functions (buffer_array, read_buffer) * change return type from int to void for many functions, as the return value (indicating error/success) was never checked, and the function would only fail on programming errors and not on invalid input; changed functions to use force_assert instead of returning an error. * all "len" parameters now are the real size of the memory to be read. the length of strings is given always without the terminating 0. * the "buffer" struct still counts the terminating 0 in ->used, provide buffer_string_length() to get the length of a string in a buffer. unset config "strings" have used == 0, which is used in some places to distinguish unset values from "" (empty string) values. * most buffer usages should now use it as string container. * optimise some buffer copying by "moving" data to other buffers * use (u)intmax_t for generic int-to-string functions * remove unused enum values: UNUSED_CHUNK, ENCODING_UNSET * converted BUFFER_APPEND_SLASH to inline function (no macro feature needed) * refactor: create chunkqueue_steal: moving (partial) chunks into another queue * http_chunk: added separate function to terminate chunked body instead of magic handling in http_chunk_append_mem(). http_chunk_append_* now handle empty chunks, and never terminate the chunked body. From: Stefan Bühler <stbuehler@web.de> git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2975 152afb58-edef-0310-8abb-c4023f1b3aa9
8 years ago
attrlen = buf->size - 1;
if(0 == (ret = attr_get(name, xattrname, buf->ptr, &attrlen, 0))) {
buffer_commit(buf, attrlen);
}
return ret;
}
#elif defined(HAVE_EXTATTR)
static int stat_cache_attr_get(buffer *buf, char *name, char *xattrname) {
ssize_t attrlen;
buffer_string_prepare_copy(buf, 1023);
if (-1 != (attrlen = extattr_get_file(name, EXTATTR_NAMESPACE_USER, xattrname, buf->ptr, buf->size - 1))) {
buf->used = attrlen + 1;
buf->ptr[attrlen] = '\0';
return 0;
}
return -1;
}
#endif
/* the famous DJB hash function for strings */
static uint32_t hashme(buffer *str) {
uint32_t hash = 5381;
const char *s;
for (s = str->ptr; *s; s++) {
hash = ((hash << 5) + hash) + *s;
}
hash &= ~(((uint32_t)1) << 31); /* strip the highest bit */
return hash;
}
#ifdef HAVE_FAM_H
static handler_t stat_cache_handle_fdevent(server *srv, void *_fce, int revent) {
size_t i;
stat_cache *sc = srv->stat_cache;
size_t events;
UNUSED(_fce);
/* */
if (revent & FDEVENT_IN) {
events = FAMPending(&sc->fam);
for (i = 0; i < events; i++) {
FAMEvent fe;
fam_dir_entry *fam_dir;
splay_tree *node;
int ndx, j;
FAMNextEvent(&sc->fam, &fe);
/* handle event */
switch(fe.code) {
case FAMChanged:
case FAMDeleted:
case FAMMoved:
/* if the filename is a directory remove the entry */
fam_dir = fe.userdata;
fam_dir->version++;
/* file/dir is still here */
if (fe.code == FAMChanged) break;
/* we have 2 versions, follow and no-follow-symlink */
for (j = 0; j < 2; j++) {
buffer_copy_string(sc->hash_key, fe.filename);
fix buffer, chunk and http_chunk API * remove unused structs and functions (buffer_array, read_buffer) * change return type from int to void for many functions, as the return value (indicating error/success) was never checked, and the function would only fail on programming errors and not on invalid input; changed functions to use force_assert instead of returning an error. * all "len" parameters now are the real size of the memory to be read. the length of strings is given always without the terminating 0. * the "buffer" struct still counts the terminating 0 in ->used, provide buffer_string_length() to get the length of a string in a buffer. unset config "strings" have used == 0, which is used in some places to distinguish unset values from "" (empty string) values. * most buffer usages should now use it as string container. * optimise some buffer copying by "moving" data to other buffers * use (u)intmax_t for generic int-to-string functions * remove unused enum values: UNUSED_CHUNK, ENCODING_UNSET * converted BUFFER_APPEND_SLASH to inline function (no macro feature needed) * refactor: create chunkqueue_steal: moving (partial) chunks into another queue * http_chunk: added separate function to terminate chunked body instead of magic handling in http_chunk_append_mem(). http_chunk_append_* now handle empty chunks, and never terminate the chunked body. From: Stefan Bühler <stbuehler@web.de> git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2975 152afb58-edef-0310-8abb-c4023f1b3aa9
8 years ago
buffer_append_int(sc->hash_key, j);
ndx = hashme(sc->hash_key);
sc->dirs = splaytree_splay(sc->dirs, ndx);
node = sc->dirs;
if (node && (node->key == ndx)) {
int osize = splaytree_size(sc->dirs);
fam_dir_entry_free(&sc->fam, node->data);
sc->dirs = splaytree_delete(sc->dirs, ndx);
force_assert(osize - 1 == splaytree_size(sc->dirs));
}
}
break;
default:
break;
}
}
}
if (revent & FDEVENT_HUP) {
/* fam closed the connection */
fdevent_event_del(srv->ev, &(sc->fam_fcce_ndx), FAMCONNECTION_GETFD(&sc->fam));
fdevent_unregister(srv->ev, FAMCONNECTION_GETFD(&sc->fam));
FAMClose(&sc->fam);
}
return HANDLER_GO_ON;
}
static int buffer_copy_dirname(buffer *dst, buffer *file) {
size_t i;
fix buffer, chunk and http_chunk API * remove unused structs and functions (buffer_array, read_buffer) * change return type from int to void for many functions, as the return value (indicating error/success) was never checked, and the function would only fail on programming errors and not on invalid input; changed functions to use force_assert instead of returning an error. * all "len" parameters now are the real size of the memory to be read. the length of strings is given always without the terminating 0. * the "buffer" struct still counts the terminating 0 in ->used, provide buffer_string_length() to get the length of a string in a buffer. unset config "strings" have used == 0, which is used in some places to distinguish unset values from "" (empty string) values. * most buffer usages should now use it as string container. * optimise some buffer copying by "moving" data to other buffers * use (u)intmax_t for generic int-to-string functions * remove unused enum values: UNUSED_CHUNK, ENCODING_UNSET * converted BUFFER_APPEND_SLASH to inline function (no macro feature needed) * refactor: create chunkqueue_steal: moving (partial) chunks into another queue * http_chunk: added separate function to terminate chunked body instead of magic handling in http_chunk_append_mem(). http_chunk_append_* now handle empty chunks, and never terminate the chunked body. From: Stefan Bühler <stbuehler@web.de> git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2975 152afb58-edef-0310-8abb-c4023f1b3aa9
8 years ago
if (buffer_string_is_empty(file)) return -1;
for (i = buffer_string_length(file); i > 0; i--) {
if (file->ptr[i] == '/') {
buffer_copy_string_len(dst, file->ptr, i);
return 0;
}
}
return -1;
}
#endif
#ifdef HAVE_LSTAT
static int stat_cache_lstat(server *srv, buffer *dname, struct stat *lst) {
if (lstat(dname->ptr, lst) == 0) {
return S_ISLNK(lst->st_mode) ? 0 : 1;
}
else {
log_error_write(srv, __FILE__, __LINE__, "sbs",
"lstat failed for:",
dname, strerror(errno));
};
return -1;
}
#endif
/***
*
*
*
* returns:
* - HANDLER_FINISHED on cache-miss (don't forget to reopen the file)
* - HANDLER_ERROR on stat() failed -> see errno for problem
*/
handler_t stat_cache_get_entry(server *srv, connection *con, buffer *name, stat_cache_entry **ret_sce) {
#ifdef HAVE_FAM_H
fam_dir_entry *fam_dir = NULL;
int dir_ndx = -1;
#endif
stat_cache_entry *sce = NULL;
stat_cache *sc;
struct stat st;
size_t k;
int fd;
struct stat lst;
#ifdef DEBUG_STAT_CACHE
size_t i;
#endif
int file_ndx;
*ret_sce = NULL;
/*
* check if the directory for this file has changed
*/
sc = srv->stat_cache;
fix buffer, chunk and http_chunk API * remove unused structs and functions (buffer_array, read_buffer) * change return type from int to void for many functions, as the return value (indicating error/success) was never checked, and the function would only fail on programming errors and not on invalid input; changed functions to use force_assert instead of returning an error. * all "len" parameters now are the real size of the memory to be read. the length of strings is given always without the terminating 0. * the "buffer" struct still counts the terminating 0 in ->used, provide buffer_string_length() to get the length of a string in a buffer. unset config "strings" have used == 0, which is used in some places to distinguish unset values from "" (empty string) values. * most buffer usages should now use it as string container. * optimise some buffer copying by "moving" data to other buffers * use (u)intmax_t for generic int-to-string functions * remove unused enum values: UNUSED_CHUNK, ENCODING_UNSET * converted BUFFER_APPEND_SLASH to inline function (no macro feature needed) * refactor: create chunkqueue_steal: moving (partial) chunks into another queue * http_chunk: added separate function to terminate chunked body instead of magic handling in http_chunk_append_mem(). http_chunk_append_* now handle empty chunks, and never terminate the chunked body. From: Stefan Bühler <stbuehler@web.de> git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2975 152afb58-edef-0310-8abb-c4023f1b3aa9
8 years ago
buffer_copy_buffer(sc->hash_key, name);
buffer_append_int(sc->hash_key, con->conf.follow_symlink);
file_ndx = hashme(sc->hash_key);
sc->files = splaytree_splay(sc->files, file_ndx);
#ifdef DEBUG_STAT_CACHE
for (i = 0; i < ctrl.used; i++) {
if (ctrl.ptr[i] == file_ndx) break;
}
#endif
if (sc->files && (sc->files->key == file_ndx)) {
#ifdef DEBUG_STAT_CACHE
/* it was in the cache */
force_assert(i < ctrl.used);
#endif
/* we have seen this file already and
* don't stat() it again in the same second */
sce = sc->files->data;
/* check if the name is the same, we might have a collision */
if (buffer_is_equal(name, sce->name)) {
if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_SIMPLE) {
[security] disable stat_cache if !follow-symlink (fixes #2724) disable stat_cache if server.follow-symlink = "disable" if server.stat-cache-engine = "simple". Caching is still enabled for server.stat-cache-engine = "fam" since the FAM notification is almost immediate, however there is still a small race condition. NOTE: server.follow-symlink = "disable" implementation still has time-of-check versus time-of-use (ToC-ToU) race conditions and its use is *not recommended* except to discourage symlinking. It *does not* prevent symlinking by a determined attacker with the ability to create files on the server. server.stat-cache-engine = "disable" can also be used to discourage symlinking, and also does not eliminate ToC-ToU race conditions. While more modern systems might use openat() and other *at() routines to eliminate the ToC-ToU race conditions, this is not currently implemented in lighttpd. Besides, for systems needing such protections against actors able to modify local files, it would be better to set up multiple lighttpd servers running in separate user contexts with filesystem permissions preventing access, rather than giving a single lighttpd server running under a single lighttpd user access to files across security boundaries, and trying to prevent access by lighttpd user if a file is a symlink. Note that there are performance implications to setting either of server.follow-symlink = "disable" server.stat-cache-engine = "disable" since stat cache normally reduces filesystem overhead for frequently-accessed files. x-ref: "security: stat cache *very large* race condition if caching when follow_symlink disabled" https://redmine.lighttpd.net/issues/2724
6 years ago
if (sce->stat_ts == srv->cur_ts && con->conf.follow_symlink) {
*ret_sce = sce;
return HANDLER_GO_ON;
}
}
} else {
/* collision, forget about the entry */
sce = NULL;
}
} else {
#ifdef DEBUG_STAT_CACHE
if (i != ctrl.used) {
log_error_write(srv, __FILE__, __LINE__, "xSB",
file_ndx, "was already inserted but not found in cache, ", name);
}
force_assert(i == ctrl.used);
#endif
}
#ifdef HAVE_FAM_H
/* dir-check */
if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_FAM) {
if (0 != buffer_copy_dirname(sc->dir_name, name)) {
log_error_write(srv, __FILE__, __LINE__, "sb",
"no '/' found in filename:", name);
return HANDLER_ERROR;
}
fix buffer, chunk and http_chunk API * remove unused structs and functions (buffer_array, read_buffer) * change return type from int to void for many functions, as the return value (indicating error/success) was never checked, and the function would only fail on programming errors and not on invalid input; changed functions to use force_assert instead of returning an error. * all "len" parameters now are the real size of the memory to be read. the length of strings is given always without the terminating 0. * the "buffer" struct still counts the terminating 0 in ->used, provide buffer_string_length() to get the length of a string in a buffer. unset config "strings" have used == 0, which is used in some places to distinguish unset values from "" (empty string) values. * most buffer usages should now use it as string container. * optimise some buffer copying by "moving" data to other buffers * use (u)intmax_t for generic int-to-string functions * remove unused enum values: UNUSED_CHUNK, ENCODING_UNSET * converted BUFFER_APPEND_SLASH to inline function (no macro feature needed) * refactor: create chunkqueue_steal: moving (partial) chunks into another queue * http_chunk: added separate function to terminate chunked body instead of magic handling in http_chunk_append_mem(). http_chunk_append_* now handle empty chunks, and never terminate the chunked body. From: Stefan Bühler <stbuehler@web.de> git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2975 152afb58-edef-0310-8abb-c4023f1b3aa9
8 years ago
buffer_copy_buffer(sc->hash_key, sc->dir_name);
buffer_append_int(sc->hash_key, con->conf.follow_symlink);
dir_ndx = hashme(sc->hash_key);
sc->dirs = splaytree_splay(sc->dirs, dir_ndx);
if ((NULL != sc->dirs) && (sc->dirs->key == dir_ndx)) {
fam_dir = sc->dirs->data;
/* check whether we got a collision */
if (buffer_is_equal(sc->dir_name, fam_dir->name)) {
/* test whether a found file cache entry is still ok */
if ((NULL != sce) && (fam_dir->version == sce->dir_version)) {
/* the stat()-cache entry is still ok */
*ret_sce = sce;
return HANDLER_GO_ON;
}
} else {
/* hash collision, forget about the entry */
fam_dir = NULL;
}
}
}
#endif
/*
* *lol*
* - open() + fstat() on a named-pipe results in a (intended) hang.
* - stat() if regular file + open() to see if we can read from it is better
*
* */
if (-1 == stat(name->ptr, &st)) {
return HANDLER_ERROR;
}
if (S_ISREG(st.st_mode)) {
/* fix broken stat/open for symlinks to reg files with appended slash on freebsd,osx */
if (name->ptr[buffer_string_length(name) - 1] == '/') {
errno = ENOTDIR;
return HANDLER_ERROR;
}
/* try to open the file to check if we can read it */
if (-1 == (fd = open(name->ptr, O_RDONLY))) {
return HANDLER_ERROR;
}
close(fd);
}
if (NULL == sce) {
sce = stat_cache_entry_init();
fix buffer, chunk and http_chunk API * remove unused structs and functions (buffer_array, read_buffer) * change return type from int to void for many functions, as the return value (indicating error/success) was never checked, and the function would only fail on programming errors and not on invalid input; changed functions to use force_assert instead of returning an error. * all "len" parameters now are the real size of the memory to be read. the length of strings is given always without the terminating 0. * the "buffer" struct still counts the terminating 0 in ->used, provide buffer_string_length() to get the length of a string in a buffer. unset config "strings" have used == 0, which is used in some places to distinguish unset values from "" (empty string) values. * most buffer usages should now use it as string container. * optimise some buffer copying by "moving" data to other buffers * use (u)intmax_t for generic int-to-string functions * remove unused enum values: UNUSED_CHUNK, ENCODING_UNSET * converted BUFFER_APPEND_SLASH to inline function (no macro feature needed) * refactor: create chunkqueue_steal: moving (partial) chunks into another queue * http_chunk: added separate function to terminate chunked body instead of magic handling in http_chunk_append_mem(). http_chunk_append_* now handle empty chunks, and never terminate the chunked body. From: Stefan Bühler <stbuehler@web.de> git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.4.x@2975 152afb58-edef-0310-8abb-c4023f1b3aa9
8 years ago
buffer_copy_buffer(sce->name, name);
/* already splayed file_ndx */
if ((NULL != sc->files) && (sc->files->key == file_ndx)) {
/* hash collision: replace old entry */
stat_cache_entry_free(sc->files->data);
sc->files->data = sce;
} else {
int osize = splaytree_size(sc->files);
sc->files = splaytree_insert(sc->files, file_ndx, sce);
force_assert(osize + 1 == splaytree_size(sc->files));
#ifdef DEBUG_STAT_CACHE
if (ctrl.size == 0) {
ctrl.size = 16;
ctrl.used = 0;
ctrl.ptr = malloc(ctrl.size * sizeof(*ctrl.ptr));
force_assert(NULL != ctrl.ptr);
} else if (ctrl.size == ctrl.used) {
ctrl.size += 16;
ctrl.ptr = realloc(ctrl.ptr, ctrl.size * sizeof(*ctrl.ptr));
force_assert(NULL != ctrl.ptr);
}
ctrl.ptr[ctrl.used++] = file_ndx;
#endif
}
force_assert(sc->files);
force_assert(sc->files->data == sce);
}
sce->st = st;
sce->stat_ts = srv->cur_ts;
/* catch the obvious symlinks
*
* this is not a secure check as we still have a race-condition between
* the stat() and the open. We can only solve this by
* 1. open() the file
* 2. fstat() the fd
*
* and keeping the file open for the rest of the time. But this can
* only be done at network level.
*
* per default it is not a symlink
* */
#ifdef HAVE_LSTAT
sce->is_symlink = 0;
/* we want to only check for symlinks if we should block symlinks.
*/
if (!con->conf.follow_symlink) {
if (stat_cache_lstat(srv, name, &lst) == 0) {
#ifdef DEBUG_STAT_CACHE
log_error_write(srv, __FILE__, __LINE__, "sb",
"found symlink", name);
#endif
sce->is_symlink = 1;
}
/*
* we assume "/" can not be symlink, so
* skip the symlink stuff if our path is /
**/
else if (buffer_string_length(name) > 1) {
buffer *dname;
char *s_cur;