1 #include "first.h" 2 3 #include "base.h" 4 #include "array.h" 5 #include "buffer.h" 6 #include "log.h" 7 #include "response.h" 8 #include "stat_cache.h" 9 10 #include "plugin.h" 11 12 #include <sys/types.h> 13 14 #include <stdlib.h> 15 #include <string.h> 16 17 #ifdef HAVE_PWD_H 18 # include <pwd.h> 19 #endif 20 21 typedef struct { 22 const array *exclude_user; 23 const array *include_user; 24 const buffer *path; 25 const buffer *basepath; 26 unsigned short letterhomes; 27 unsigned short active; 28 } plugin_config; 29 30 typedef struct { 31 PLUGIN_DATA; 32 plugin_config defaults; 33 plugin_config conf; 34 time_t cache_ts[2]; 35 buffer cache_user[2]; 36 buffer cache_path[2]; 37 } plugin_data; 38 39 INIT_FUNC(mod_userdir_init) { 40 return calloc(1, sizeof(plugin_data)); 41 } 42 43 FREE_FUNC(mod_userdir_free) { 44 plugin_data * const p = p_d; 45 free(p->cache_user[0].ptr); 46 free(p->cache_user[1].ptr); 47 free(p->cache_path[0].ptr); 48 free(p->cache_path[1].ptr); 49 } 50 51 static void mod_userdir_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { 52 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 53 case 0: /* userdir.path */ 54 pconf->path = cpv->v.b; 55 break; 56 case 1: /* userdir.exclude-user */ 57 pconf->exclude_user = cpv->v.a; 58 break; 59 case 2: /* userdir.include-user */ 60 pconf->include_user = cpv->v.a; 61 break; 62 case 3: /* userdir.basepath */ 63 pconf->basepath = cpv->v.b; 64 break; 65 case 4: /* userdir.letterhomes */ 66 pconf->letterhomes = cpv->v.u; 67 break; 68 case 5: /* userdir.active */ 69 pconf->active = cpv->v.u; 70 break; 71 default:/* should not happen */ 72 return; 73 } 74 } 75 76 static void mod_userdir_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { 77 do { 78 mod_userdir_merge_config_cpv(pconf, cpv); 79 } while ((++cpv)->k_id != -1); 80 } 81 82 static void mod_userdir_patch_config(request_st * const r, plugin_data * const p) { 83 memcpy(&p->conf, &p->defaults, sizeof(plugin_config)); 84 for (int i = 1, used = p->nconfig; i < used; ++i) { 85 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 86 mod_userdir_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]); 87 } 88 } 89 90 SETDEFAULTS_FUNC(mod_userdir_set_defaults) { 91 static const config_plugin_keys_t cpk[] = { 92 { CONST_STR_LEN("userdir.path"), 93 T_CONFIG_STRING, 94 T_CONFIG_SCOPE_CONNECTION } 95 ,{ CONST_STR_LEN("userdir.exclude-user"), 96 T_CONFIG_ARRAY_VLIST, 97 T_CONFIG_SCOPE_CONNECTION } 98 ,{ CONST_STR_LEN("userdir.include-user"), 99 T_CONFIG_ARRAY_VLIST, 100 T_CONFIG_SCOPE_CONNECTION } 101 ,{ CONST_STR_LEN("userdir.basepath"), 102 T_CONFIG_STRING, 103 T_CONFIG_SCOPE_CONNECTION } 104 ,{ CONST_STR_LEN("userdir.letterhomes"), 105 T_CONFIG_BOOL, 106 T_CONFIG_SCOPE_CONNECTION } 107 ,{ CONST_STR_LEN("userdir.active"), 108 T_CONFIG_BOOL, 109 T_CONFIG_SCOPE_CONNECTION } 110 ,{ NULL, 0, 111 T_CONFIG_UNSET, 112 T_CONFIG_SCOPE_UNSET } 113 }; 114 115 plugin_data * const p = p_d; 116 if (!config_plugin_values_init(srv, p, cpk, "mod_userdir")) 117 return HANDLER_ERROR; 118 119 /* enabled by default for backward compatibility; 120 * if userdir.path isn't set userdir is disabled too, 121 * but you can't disable it by setting it to an empty string. */ 122 p->defaults.active = 1; 123 124 /* initialize p->defaults from global config context */ 125 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 126 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 127 if (-1 != cpv->k_id) 128 mod_userdir_merge_config(&p->defaults, cpv); 129 } 130 131 return HANDLER_GO_ON; 132 } 133 134 static int mod_userdir_in_vlist_nc(const array * const a, const char * const k, const size_t klen) { 135 for (uint32_t i = 0, used = a->used; i < used; ++i) { 136 const data_string * const ds = (const data_string *)a->data[i]; 137 if (buffer_eq_icase_slen(&ds->value, k, klen)) return 1; 138 } 139 return 0; 140 } 141 142 static int mod_userdir_in_vlist(const array * const a, const char * const k, const size_t klen) { 143 for (uint32_t i = 0, used = a->used; i < used; ++i) { 144 const data_string * const ds = (const data_string *)a->data[i]; 145 if (buffer_eq_slen(&ds->value, k, klen)) return 1; 146 } 147 return 0; 148 } 149 150 __attribute_noinline__ 151 static handler_t mod_userdir_docroot_construct(request_st * const r, plugin_data * const p, const char * const uptr, const size_t ulen) { 152 char u[256]; 153 if (ulen >= sizeof(u)) return HANDLER_GO_ON; 154 155 memcpy(u, uptr, ulen); 156 u[ulen] = '\0'; 157 158 /* we build the physical path */ 159 buffer * const b = r->tmp_buf; 160 161 if (buffer_string_is_empty(p->conf.basepath)) { 162 #ifdef HAVE_PWD_H 163 /* getpwnam() lookup is expensive; first check 2-element cache */ 164 const time_t cur_ts = log_monotonic_secs; 165 int cached = -1; 166 const int cache_sz =(int)(sizeof(p->cache_user)/sizeof(*p->cache_user)); 167 for (int i = 0; i < cache_sz; ++i) { 168 if (cur_ts - p->cache_ts[i] < 60 && p->cache_user[i].used 169 && buffer_eq_slen(&p->cache_user[i], u, ulen)) { 170 cached = i; 171 break; 172 } 173 } 174 struct passwd *pwd; 175 if (cached >= 0) { 176 buffer_copy_path_len2(b, CONST_BUF_LEN(&p->cache_path[cached]), 177 CONST_BUF_LEN(p->conf.path)); 178 } 179 else if ((pwd = getpwnam(u))) { 180 const size_t plen = strlen(pwd->pw_dir); 181 buffer_copy_path_len2(b, pwd->pw_dir, plen, 182 CONST_BUF_LEN(p->conf.path)); 183 if (!stat_cache_path_isdir(b)) { 184 return HANDLER_GO_ON; 185 } 186 /* update cache, replacing oldest entry */ 187 cached = 0; 188 time_t cache_ts = p->cache_ts[0]; 189 for (int i = 1; i < cache_sz; ++i) { 190 if (cache_ts > p->cache_ts[i]) { 191 cache_ts = p->cache_ts[i]; 192 cached = i; 193 } 194 } 195 p->cache_ts[cached] = cur_ts; 196 buffer_copy_string_len(&p->cache_path[cached], b->ptr, plen); 197 buffer_copy_string_len(&p->cache_user[cached], u, ulen); 198 } 199 else /* user not found */ 200 #endif 201 return HANDLER_GO_ON; 202 } else { 203 /* check if the username is valid 204 * a request for /~../ should lead to a directory traversal 205 * limiting to [-_a-z0-9.] should fix it */ 206 if (ulen <= 2 && (u[0] == '.' && (1 == ulen || u[1] == '.'))) { 207 return HANDLER_GO_ON; 208 } 209 210 for (size_t i = 0; i < ulen; ++i) { 211 const int c = u[i]; 212 if (!(light_isalnum(c) || c == '-' || c == '_' || c == '.')) { 213 return HANDLER_GO_ON; 214 } 215 } 216 217 if (r->conf.force_lowercase_filenames) { 218 for (size_t i = 0; i < ulen; ++i) { 219 if (light_isupper(u[i])) u[i] |= 0x20; 220 } 221 } 222 223 buffer_copy_buffer(b, p->conf.basepath); 224 if (p->conf.letterhomes) { 225 if (u[0] == '.') return HANDLER_GO_ON; 226 buffer_append_path_len(b, u, 1); 227 } 228 buffer_append_path_len(b, u, ulen); 229 buffer_append_path_len(b, CONST_BUF_LEN(p->conf.path)); 230 } 231 232 buffer_copy_buffer(&r->physical.basedir, b); 233 buffer_copy_buffer(&r->physical.path, b); 234 235 /* the physical rel_path is basically the same as uri.path; 236 * but it is converted to lowercase in case of force_lowercase_filenames 237 * and some special handling for trailing '.', ' ' and '/' on windows 238 * we assume that no docroot/physical handler changed this 239 * (docroot should only set the docroot/server name, physical should only 240 * change the physical.path; 241 * the exception mod_secdownload doesn't work with userdir anyway) 242 */ 243 buffer_append_slash(&r->physical.path); 244 /* if no second '/' is found, we assume that it was stripped from the 245 * uri.path for the special handling on windows. we do not care about the 246 * trailing slash here on windows, as we already ensured it is a directory 247 * 248 * TODO: what to do with trailing dots in usernames on windows? 249 * they may result in the same directory as a username without them. 250 */ 251 char *rel_url; 252 if (NULL != (rel_url = strchr(r->physical.rel_path.ptr + 2, '/'))) { 253 buffer_append_string(&r->physical.path, rel_url + 1); /* skip the / */ 254 } 255 256 return HANDLER_GO_ON; 257 } 258 259 URIHANDLER_FUNC(mod_userdir_docroot_handler) { 260 /* /~user/foo.html -> /home/user/public_html/foo.html */ 261 262 #ifdef __COVERITY__ 263 if (buffer_is_empty(&r->uri.path)) return HANDLER_GO_ON; 264 #endif 265 266 if (r->uri.path.ptr[0] != '/' || 267 r->uri.path.ptr[1] != '~') return HANDLER_GO_ON; 268 269 plugin_data * const p = p_d; 270 mod_userdir_patch_config(r, p); 271 272 /* enforce the userdir.path to be set in the config, ugly fix for #1587; 273 * should be replaced with a clean .enabled option in 1.5 274 */ 275 if (!p->conf.active || buffer_is_empty(p->conf.path)) return HANDLER_GO_ON; 276 277 const char * const uptr = r->uri.path.ptr + 2; 278 const char * const rel_url = strchr(uptr, '/'); 279 if (NULL == rel_url) { 280 if (!*uptr) return HANDLER_GO_ON; /* "/~" is not a valid userdir path */ 281 /* / is missing -> redirect to .../ as we are a user - DIRECTORY ! :) */ 282 http_response_redirect_to_directory(r, 301); 283 return HANDLER_FINISHED; 284 } 285 286 /* /~/ is a empty username, catch it directly */ 287 const size_t ulen = (size_t)(rel_url - uptr); 288 if (0 == ulen) return HANDLER_GO_ON; 289 290 /* vlists could be turned into sorted array at config time, 291 * but these lists are expected to be relatively short in most cases 292 * so there is not a huge benefit to doing so in the common case */ 293 294 if (p->conf.exclude_user) { 295 /* use case-insensitive comparison for exclude list 296 * if r->conf.force_lowercase_filenames */ 297 if (!r->conf.force_lowercase_filenames 298 ? mod_userdir_in_vlist(p->conf.exclude_user, uptr, ulen) 299 : mod_userdir_in_vlist_nc(p->conf.exclude_user, uptr, ulen)) 300 return HANDLER_GO_ON; /* user in exclude list */ 301 } 302 303 if (p->conf.include_user) { 304 if (!mod_userdir_in_vlist(p->conf.include_user, uptr, ulen)) 305 return HANDLER_GO_ON; /* user not in include list */ 306 } 307 308 return mod_userdir_docroot_construct(r, p, uptr, ulen); 309 } 310 311 312 int mod_userdir_plugin_init(plugin *p); 313 int mod_userdir_plugin_init(plugin *p) { 314 p->version = LIGHTTPD_VERSION_ID; 315 p->name = "userdir"; 316 317 p->init = mod_userdir_init; 318 p->cleanup = mod_userdir_free; 319 p->handle_physical = mod_userdir_docroot_handler; 320 p->set_defaults = mod_userdir_set_defaults; 321 322 return 0; 323 } 324