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