1/* Licensed to the Apache Software Foundation (ASF) under one or more 2 * contributor license agreements. See the NOTICE file distributed with 3 * this work for additional information regarding copyright ownership. 4 * The ASF licenses this file to You under the Apache License, Version 2.0 5 * (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17/* 18 * mod_userdir... implement the UserDir command. Broken away from the 19 * Alias stuff for a couple of good and not-so-good reasons: 20 * 21 * 1) It shows a real minimal working example of how to do something like 22 * this. 23 * 2) I know people who are actually interested in changing this *particular* 24 * aspect of server functionality without changing the rest of it. That's 25 * what this whole modular arrangement is supposed to be good at... 26 * 27 * Modified by Alexei Kosut to support the following constructs 28 * (server running at www.foo.com, request for /~bar/one/two.html) 29 * 30 * UserDir public_html -> ~bar/public_html/one/two.html 31 * UserDir /usr/web -> /usr/web/bar/one/two.html 32 * UserDir /home/ * /www -> /home/bar/www/one/two.html 33 * NOTE: theses ^ ^ space only added allow it to work in a comment, ignore 34 * UserDir http://x/users -> (302) http://x/users/bar/one/two.html 35 * UserDir http://x/ * /y -> (302) http://x/bar/y/one/two.html 36 * NOTE: here also ^ ^ 37 * 38 * In addition, you can use multiple entries, to specify alternate 39 * user directories (a la Directory Index). For example: 40 * 41 * UserDir public_html /usr/web http://www.xyz.com/users 42 * 43 * Modified by Ken Coar to provide for the following: 44 * 45 * UserDir disable[d] username ... 46 * UserDir enable[d] username ... 47 * 48 * If "disabled" has no other arguments, *all* ~<username> references are 49 * disabled, except those explicitly turned on with the "enabled" keyword. 50 */ 51 52#include "apr_strings.h" 53#include "apr_user.h" 54 55#define APR_WANT_STRFUNC 56#include "apr_want.h" 57 58#if APR_HAVE_UNISTD_H 59#include <unistd.h> 60#endif 61 62#include "ap_config.h" 63#include "httpd.h" 64#include "http_config.h" 65#include "http_request.h" 66 67#if !defined(WIN32) && !defined(OS2) && !defined(NETWARE) 68#define HAVE_UNIX_SUEXEC 69#endif 70 71#ifdef HAVE_UNIX_SUEXEC 72#include "unixd.h" /* Contains the suexec_identity hook used on Unix */ 73#endif 74 75 76/* 77 * The default directory in user's home dir 78 * In the default install, the module is disabled 79 */ 80#ifndef DEFAULT_USER_DIR 81#define DEFAULT_USER_DIR NULL 82#endif 83 84#define O_DEFAULT 0 85#define O_ENABLE 1 86#define O_DISABLE 2 87 88module AP_MODULE_DECLARE_DATA userdir_module; 89 90typedef struct { 91 int globally_disabled; 92 char *userdir; 93 apr_table_t *enabled_users; 94 apr_table_t *disabled_users; 95} userdir_config; 96 97/* 98 * Server config for this module: global disablement flag, a list of usernames 99 * ineligible for UserDir access, a list of those immune to global (but not 100 * explicit) disablement, and the replacement string for all others. 101 */ 102 103static void *create_userdir_config(apr_pool_t *p, server_rec *s) 104{ 105 userdir_config *newcfg = apr_pcalloc(p, sizeof(*newcfg)); 106 107 newcfg->globally_disabled = O_DEFAULT; 108 newcfg->userdir = DEFAULT_USER_DIR; 109 newcfg->enabled_users = apr_table_make(p, 4); 110 newcfg->disabled_users = apr_table_make(p, 4); 111 112 return newcfg; 113} 114 115static void *merge_userdir_config(apr_pool_t *p, void *basev, void *overridesv) 116{ 117 userdir_config *cfg = apr_pcalloc(p, sizeof(userdir_config)); 118 userdir_config *base = basev, *overrides = overridesv; 119 120 cfg->globally_disabled = (overrides->globally_disabled != O_DEFAULT) ? 121 overrides->globally_disabled : 122 base->globally_disabled; 123 cfg->userdir = (overrides->userdir != DEFAULT_USER_DIR) ? 124 overrides->userdir : base->userdir; 125 126 /* not merged */ 127 cfg->enabled_users = overrides->enabled_users; 128 cfg->disabled_users = overrides->disabled_users; 129 130 return cfg; 131} 132 133 134static const char *set_user_dir(cmd_parms *cmd, void *dummy, const char *arg) 135{ 136 userdir_config *s_cfg = ap_get_module_config(cmd->server->module_config, 137 &userdir_module); 138 char *username; 139 const char *usernames = arg; 140 char *kw = ap_getword_conf(cmd->pool, &usernames); 141 apr_table_t *usertable; 142 143 /* Since we are a raw argument, it is possible for us to be called with 144 * zero arguments. So that we aren't ambiguous, flat out reject this. 145 */ 146 if (*kw == '\0') { 147 return "UserDir requires an argument."; 148 } 149 150 /* 151 * Let's do the comparisons once. 152 */ 153 if ((!strcasecmp(kw, "disable")) || (!strcasecmp(kw, "disabled"))) { 154 /* 155 * If there are no usernames specified, this is a global disable - we 156 * need do no more at this point than record the fact. 157 */ 158 if (!*usernames) { 159 s_cfg->globally_disabled = O_DISABLE; 160 return NULL; 161 } 162 usertable = s_cfg->disabled_users; 163 } 164 else if ((!strcasecmp(kw, "enable")) || (!strcasecmp(kw, "enabled"))) { 165 if (!*usernames) { 166 s_cfg->globally_disabled = O_ENABLE; 167 return NULL; 168 } 169 usertable = s_cfg->enabled_users; 170 } 171 else { 172 /* 173 * If the first (only?) value isn't one of our keywords, just copy 174 * the string to the userdir string. 175 */ 176 s_cfg->userdir = apr_pstrdup(cmd->pool, arg); 177 return NULL; 178 } 179 /* 180 * Now we just take each word in turn from the command line and add it to 181 * the appropriate table. 182 */ 183 while (*usernames) { 184 username = ap_getword_conf(cmd->pool, &usernames); 185 apr_table_setn(usertable, username, kw); 186 } 187 return NULL; 188} 189 190static const command_rec userdir_cmds[] = { 191 AP_INIT_RAW_ARGS("UserDir", set_user_dir, NULL, RSRC_CONF, 192 "the public subdirectory in users' home directories, or " 193 "'disabled', or 'disabled username username...', or " 194 "'enabled username username...'"), 195 {NULL} 196}; 197 198static int translate_userdir(request_rec *r) 199{ 200 ap_conf_vector_t *server_conf; 201 const userdir_config *s_cfg; 202 const char *userdirs; 203 const char *user, *dname; 204 char *redirect; 205 apr_finfo_t statbuf; 206 207 /* 208 * If the URI doesn't match our basic pattern, we've nothing to do with 209 * it. 210 */ 211 if (r->uri[0] != '/' || r->uri[1] != '~') { 212 return DECLINED; 213 } 214 server_conf = r->server->module_config; 215 s_cfg = ap_get_module_config(server_conf, &userdir_module); 216 userdirs = s_cfg->userdir; 217 if (userdirs == NULL) { 218 return DECLINED; 219 } 220 221 dname = r->uri + 2; 222 user = ap_getword(r->pool, &dname, '/'); 223 224 /* 225 * The 'dname' funny business involves backing it up to capture the '/' 226 * delimiting the "/~user" part from the rest of the URL, in case there 227 * was one (the case where there wasn't being just "GET /~user HTTP/1.0", 228 * for which we don't want to tack on a '/' onto the filename). 229 */ 230 231 if (dname[-1] == '/') { 232 --dname; 233 } 234 235 /* 236 * If there's no username, it's not for us. Ignore . and .. as well. 237 */ 238 if (user[0] == '\0' || 239 (user[1] == '.' && (user[2] == '\0' || 240 (user[2] == '.' && user[3] == '\0')))) { 241 return DECLINED; 242 } 243 /* 244 * Nor if there's an username but it's in the disabled list. 245 */ 246 if (apr_table_get(s_cfg->disabled_users, user) != NULL) { 247 return DECLINED; 248 } 249 /* 250 * If there's a global interdiction on UserDirs, check to see if this 251 * name is one of the Blessed. 252 */ 253 if (s_cfg->globally_disabled == O_DISABLE 254 && apr_table_get(s_cfg->enabled_users, user) == NULL) { 255 return DECLINED; 256 } 257 258 /* 259 * Special cases all checked, onward to normal substitution processing. 260 */ 261 262 while (*userdirs) { 263 const char *userdir = ap_getword_conf(r->pool, &userdirs); 264 char *filename = NULL, *prefix = NULL; 265 apr_status_t rv; 266 int is_absolute = ap_os_is_path_absolute(r->pool, userdir); 267 268 if (ap_strchr_c(userdir, '*')) 269 prefix = ap_getword(r->pool, &userdir, '*'); 270 271 if (userdir[0] == '\0' || is_absolute) { 272 if (prefix) { 273#ifdef HAVE_DRIVE_LETTERS 274 /* 275 * Crummy hack. Need to figure out whether we have been 276 * redirected to a URL or to a file on some drive. Since I 277 * know of no protocols that are a single letter, ignore 278 * a : as the first or second character, and assume a file 279 * was specified 280 */ 281 if (strchr(prefix + 2, ':')) 282#else 283 if (strchr(prefix, ':') && !is_absolute) 284#endif /* HAVE_DRIVE_LETTERS */ 285 { 286 redirect = apr_pstrcat(r->pool, prefix, user, userdir, 287 dname, NULL); 288 apr_table_setn(r->headers_out, "Location", redirect); 289 return HTTP_MOVED_TEMPORARILY; 290 } 291 else 292 filename = apr_pstrcat(r->pool, prefix, user, userdir, 293 NULL); 294 } 295 else 296 filename = apr_pstrcat(r->pool, userdir, "/", user, NULL); 297 } 298 else if (prefix && ap_strchr_c(prefix, ':')) { 299 redirect = apr_pstrcat(r->pool, prefix, user, dname, NULL); 300 apr_table_setn(r->headers_out, "Location", redirect); 301 return HTTP_MOVED_TEMPORARILY; 302 } 303 else { 304#if APR_HAS_USER 305 char *homedir; 306 307 if (apr_uid_homepath_get(&homedir, user, r->pool) == APR_SUCCESS) { 308 filename = apr_pstrcat(r->pool, homedir, "/", userdir, NULL); 309 } 310#else 311 return DECLINED; 312#endif 313 } 314 315 /* 316 * Now see if it exists, or we're at the last entry. If we are at the 317 * last entry, then use the filename generated (if there is one) 318 * anyway, in the hope that some handler might handle it. This can be 319 * used, for example, to run a CGI script for the user. 320 */ 321 if (filename && (!*userdirs 322 || ((rv = apr_stat(&statbuf, filename, APR_FINFO_MIN, 323 r->pool)) == APR_SUCCESS 324 || rv == APR_INCOMPLETE))) { 325 r->filename = apr_pstrcat(r->pool, filename, dname, NULL); 326 ap_set_context_info(r, apr_pstrmemdup(r->pool, r->uri, 327 dname - r->uri), 328 filename); 329 /* XXX: Does this walk us around FollowSymLink rules? 330 * When statbuf contains info on r->filename we can save a syscall 331 * by copying it to r->finfo 332 */ 333 if (*userdirs && dname[0] == 0) 334 r->finfo = statbuf; 335 336 /* For use in the get_suexec_identity phase */ 337 apr_table_setn(r->notes, "mod_userdir_user", user); 338 339 return OK; 340 } 341 } 342 343 return DECLINED; 344} 345 346#ifdef HAVE_UNIX_SUEXEC 347static ap_unix_identity_t *get_suexec_id_doer(const request_rec *r) 348{ 349 ap_unix_identity_t *ugid = NULL; 350#if APR_HAS_USER 351 const char *username = apr_table_get(r->notes, "mod_userdir_user"); 352 353 if (username == NULL) { 354 return NULL; 355 } 356 357 if ((ugid = apr_palloc(r->pool, sizeof(*ugid))) == NULL) { 358 return NULL; 359 } 360 361 if (apr_uid_get(&ugid->uid, &ugid->gid, username, r->pool) != APR_SUCCESS) { 362 return NULL; 363 } 364 365 ugid->userdir = 1; 366#endif 367 return ugid; 368} 369#endif /* HAVE_UNIX_SUEXEC */ 370 371static void register_hooks(apr_pool_t *p) 372{ 373 static const char * const aszPre[]={ "mod_alias.c",NULL }; 374 static const char * const aszSucc[]={ "mod_vhost_alias.c",NULL }; 375 376 ap_hook_translate_name(translate_userdir,aszPre,aszSucc,APR_HOOK_MIDDLE); 377#ifdef HAVE_UNIX_SUEXEC 378 ap_hook_get_suexec_identity(get_suexec_id_doer,NULL,NULL,APR_HOOK_FIRST); 379#endif 380} 381 382AP_DECLARE_MODULE(userdir) = { 383 STANDARD20_MODULE_STUFF, 384 NULL, /* dir config creater */ 385 NULL, /* dir merger --- default is to override */ 386 create_userdir_config, /* server config */ 387 merge_userdir_config, /* merge server config */ 388 userdir_cmds, /* command apr_table_t */ 389 register_hooks /* register hooks */ 390}; 391