1 #include "first.h" 2 3 #include "array.h" 4 #include "buffer.h" 5 #include "log.h" 6 #include "request.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 unix_time64_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 /* process and validate config directives 120 * (init i to 0 if global context; to 1 to skip empty global context) */ 121 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { 122 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 123 for (; -1 != cpv->k_id; ++cpv) { 124 switch (cpv->k_id) { 125 case 0: /* userdir.path */ 126 case 1: /* userdir.exclude-user */ 127 case 2: /* userdir.include-user */ 128 break; 129 case 3: /* userdir.basepath */ 130 if (buffer_is_blank(cpv->v.b)) 131 cpv->v.b = NULL; 132 break; 133 case 4: /* userdir.letterhomes */ 134 case 5: /* userdir.active */ 135 break; 136 default:/* should not happen */ 137 break; 138 } 139 } 140 } 141 142 /* enabled by default for backward compatibility; 143 * if userdir.path isn't set userdir is disabled too, 144 * but you can't disable it by setting it to an empty string. */ 145 p->defaults.active = 1; 146 147 /* initialize p->defaults from global config context */ 148 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 149 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 150 if (-1 != cpv->k_id) 151 mod_userdir_merge_config(&p->defaults, cpv); 152 } 153 154 return HANDLER_GO_ON; 155 } 156 157 static int mod_userdir_in_vlist_nc(const array * const a, const char * const k, const size_t klen) { 158 for (uint32_t i = 0, used = a->used; i < used; ++i) { 159 const data_string * const ds = (const data_string *)a->data[i]; 160 if (buffer_eq_icase_slen(&ds->value, k, klen)) return 1; 161 } 162 return 0; 163 } 164 165 static int mod_userdir_in_vlist(const array * const a, const char * const k, const size_t klen) { 166 for (uint32_t i = 0, used = a->used; i < used; ++i) { 167 const data_string * const ds = (const data_string *)a->data[i]; 168 if (buffer_eq_slen(&ds->value, k, klen)) return 1; 169 } 170 return 0; 171 } 172 173 __attribute_noinline__ 174 static handler_t mod_userdir_docroot_construct(request_st * const r, plugin_data * const p, const char * const uptr, const size_t ulen) { 175 char u[256]; 176 if (ulen >= sizeof(u)) return HANDLER_GO_ON; 177 178 memcpy(u, uptr, ulen); 179 u[ulen] = '\0'; 180 181 /* we build the physical path */ 182 buffer * const b = r->tmp_buf; 183 184 if (!p->conf.basepath) { 185 #ifdef HAVE_PWD_H 186 /* getpwnam() lookup is expensive; first check 2-element cache */ 187 const unix_time64_t cur_ts = log_monotonic_secs; 188 int cached = -1; 189 const int cache_sz =(int)(sizeof(p->cache_user)/sizeof(*p->cache_user)); 190 for (int i = 0; i < cache_sz; ++i) { 191 if (cur_ts - p->cache_ts[i] < 60 && p->cache_user[i].used 192 && buffer_eq_slen(&p->cache_user[i], u, ulen)) { 193 cached = i; 194 break; 195 } 196 } 197 struct passwd *pwd; 198 if (cached >= 0) { 199 buffer_copy_path_len2(b, BUF_PTR_LEN(&p->cache_path[cached]), 200 BUF_PTR_LEN(p->conf.path)); 201 } 202 else if ((pwd = getpwnam(u))) { 203 const size_t plen = strlen(pwd->pw_dir); 204 buffer_copy_path_len2(b, pwd->pw_dir, plen, 205 BUF_PTR_LEN(p->conf.path)); 206 if (!stat_cache_path_isdir(b)) { 207 return HANDLER_GO_ON; 208 } 209 /* update cache, replacing oldest entry */ 210 cached = 0; 211 unix_time64_t cache_ts = p->cache_ts[0]; 212 for (int i = 1; i < cache_sz; ++i) { 213 if (cache_ts > p->cache_ts[i]) { 214 cache_ts = p->cache_ts[i]; 215 cached = i; 216 } 217 } 218 p->cache_ts[cached] = cur_ts; 219 buffer_copy_string_len(&p->cache_path[cached], b->ptr, plen); 220 buffer_copy_string_len(&p->cache_user[cached], u, ulen); 221 } 222 else /* user not found */ 223 #endif 224 return HANDLER_GO_ON; 225 } else { 226 /* check if the username is valid 227 * a request for /~../ should lead to a directory traversal 228 * limiting to [-_a-z0-9.] should fix it */ 229 if (ulen <= 2 && (u[0] == '.' && (1 == ulen || u[1] == '.'))) { 230 return HANDLER_GO_ON; 231 } 232 233 for (size_t i = 0; i < ulen; ++i) { 234 const int c = u[i]; 235 if (!(light_isalnum(c) || c == '-' || c == '_' || c == '.')) { 236 return HANDLER_GO_ON; 237 } 238 } 239 240 if (r->conf.force_lowercase_filenames) { 241 for (size_t i = 0; i < ulen; ++i) { 242 if (light_isupper(u[i])) u[i] |= 0x20; 243 } 244 } 245 246 buffer_copy_buffer(b, p->conf.basepath); 247 if (p->conf.letterhomes) { 248 if (u[0] == '.') return HANDLER_GO_ON; 249 buffer_append_path_len(b, u, 1); 250 } 251 buffer_append_path_len(b, u, ulen); 252 buffer_append_path_len(b, BUF_PTR_LEN(p->conf.path)); 253 } 254 255 buffer_copy_buffer(&r->physical.basedir, b); 256 buffer_copy_buffer(&r->physical.path, b); 257 258 /* the physical rel_path is basically the same as uri.path; 259 * but it is converted to lowercase in case of force_lowercase_filenames 260 * and some special handling for trailing '.', ' ' and '/' on windows 261 * we assume that no docroot/physical handler changed this 262 * (docroot should only set the docroot/server name, physical should only 263 * change the physical.path; 264 * the exception mod_secdownload doesn't work with userdir anyway) 265 */ 266 buffer_append_slash(&r->physical.path); 267 /* if no second '/' is found, we assume that it was stripped from the 268 * uri.path for the special handling on windows. we do not care about the 269 * trailing slash here on windows, as we already ensured it is a directory 270 * 271 * TODO: what to do with trailing dots in usernames on windows? 272 * they may result in the same directory as a username without them. 273 */ 274 char *rel_url; 275 if (NULL != (rel_url = strchr(r->physical.rel_path.ptr + 2, '/'))) { 276 buffer_append_string(&r->physical.path, rel_url + 1); /* skip the / */ 277 } 278 279 return HANDLER_GO_ON; 280 } 281 282 URIHANDLER_FUNC(mod_userdir_docroot_handler) { 283 /* /~user/foo.html -> /home/user/public_html/foo.html */ 284 285 #ifdef __COVERITY__ 286 if (buffer_is_blank(&r->uri.path)) return HANDLER_GO_ON; 287 #endif 288 289 if (r->uri.path.ptr[0] != '/' || 290 r->uri.path.ptr[1] != '~') return HANDLER_GO_ON; 291 292 plugin_data * const p = p_d; 293 mod_userdir_patch_config(r, p); 294 295 /* enforce the userdir.path to be set in the config, ugly fix for #1587; 296 * should be replaced with a clean .enabled option in 1.5 297 */ 298 if (!p->conf.active || !p->conf.path) return HANDLER_GO_ON; 299 300 const char * const uptr = r->uri.path.ptr + 2; 301 const char * const rel_url = strchr(uptr, '/'); 302 if (NULL == rel_url) { 303 if (!*uptr) return HANDLER_GO_ON; /* "/~" is not a valid userdir path */ 304 /* / is missing -> redirect to .../ as we are a user - DIRECTORY ! :) */ 305 http_response_redirect_to_directory(r, 301); 306 return HANDLER_FINISHED; 307 } 308 309 /* /~/ is a empty username, catch it directly */ 310 const size_t ulen = (size_t)(rel_url - uptr); 311 if (0 == ulen) return HANDLER_GO_ON; 312 313 /* vlists could be turned into sorted array at config time, 314 * but these lists are expected to be relatively short in most cases 315 * so there is not a huge benefit to doing so in the common case */ 316 317 if (p->conf.exclude_user) { 318 /* use case-insensitive comparison for exclude list 319 * if r->conf.force_lowercase_filenames */ 320 if (!r->conf.force_lowercase_filenames 321 ? mod_userdir_in_vlist(p->conf.exclude_user, uptr, ulen) 322 : mod_userdir_in_vlist_nc(p->conf.exclude_user, uptr, ulen)) 323 return HANDLER_GO_ON; /* user in exclude list */ 324 } 325 326 if (p->conf.include_user) { 327 if (!mod_userdir_in_vlist(p->conf.include_user, uptr, ulen)) 328 return HANDLER_GO_ON; /* user not in include list */ 329 } 330 331 return mod_userdir_docroot_construct(r, p, uptr, ulen); 332 } 333 334 335 int mod_userdir_plugin_init(plugin *p); 336 int mod_userdir_plugin_init(plugin *p) { 337 p->version = LIGHTTPD_VERSION_ID; 338 p->name = "userdir"; 339 340 p->init = mod_userdir_init; 341 p->cleanup = mod_userdir_free; 342 p->handle_physical = mod_userdir_docroot_handler; 343 p->set_defaults = mod_userdir_set_defaults; 344 345 return 0; 346 } 347