1 /*
2  * ProFTPD: mod_lang -- a module for handling the LANG command [RFC2640]
3  * Copyright (c) 2006-2017 The ProFTPD Project
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18  *
19  * As a special exemption, TJ Saunders and other respective copyright holders
20  * give permission to link this program with OpenSSL, and distribute the
21  * resulting executable, without including the source code for OpenSSL in the
22  * source distribution.
23  */
24 
25 #include "conf.h"
26 
27 #define MOD_LANG_VERSION		"mod_lang/1.1"
28 
29 #if PROFTPD_VERSION_NUMBER < 0x0001030101
30 # error "ProFTPD 1.3.1rc1 or later required"
31 #endif
32 
33 #if PR_USE_NLS
34 
35 #ifdef HAVE_LANGINFO_H
36 # include <langinfo.h>
37 #endif
38 
39 extern xaset_t *server_list;
40 
41 module lang_module;
42 
43 #define LANG_DEFAULT_LANG	"en_US"
44 
45 static const char *lang_curr = LANG_DEFAULT_LANG;
46 static const char *lang_default = LANG_DEFAULT_LANG;
47 static int lang_engine = TRUE;
48 static pool *lang_pool = NULL;
49 static array_header *lang_list = NULL;
50 static pr_table_t *lang_aliases = NULL;
51 static const char *lang_path = PR_LOCALE_DIR;
52 
53 static unsigned long lang_opts = 0UL;
54 #define LANG_OPT_PREFER_SERVER_ENCODING		0x0001
55 #define LANG_OPT_REQUIRE_VALID_ENCODING		0x0002
56 
57 static int lang_use_encoding = -1;
58 static const char *lang_local_charset = NULL, *lang_client_charset = NULL;
59 
60 /* Support routines
61  */
62 
lang_feat_add(pool * p)63 static void lang_feat_add(pool *p) {
64   char *feat_str = "";
65 
66   if (lang_list &&
67       lang_list->nelts > 0) {
68     register unsigned int i;
69     char **langs;
70     size_t feat_strlen = 0;
71 
72     langs = lang_list->elts;
73     for (i = 0; i < lang_list->nelts; i++) {
74       char *lang_dup, *tmp;
75 
76       /* Convert all locales in the list to RFC1766 form, i.e. hyphens instead
77        * of underscores.
78        */
79       lang_dup = pstrdup(p, langs[i]);
80       tmp = strchr(lang_dup, '_');
81       if (tmp) {
82         *tmp = '-';
83       }
84 
85       feat_str = pstrcat(p, feat_str, lang_dup, NULL);
86       if (strcasecmp(lang_curr, lang_dup) == 0 ||
87           strcasecmp(lang_curr, langs[i]) == 0) {
88         /* This is the currently selected language; mark it with an asterisk,
89          * as per RFC2640, Section 4.3.
90          */
91         feat_str = pstrcat(p, feat_str, "*", NULL);
92       }
93 
94       feat_str = pstrcat(p, feat_str, ";", NULL);
95     }
96 
97     feat_strlen = strlen(feat_str);
98 
99     /* Trim the trailing semicolon. */
100     if (feat_str[feat_strlen-1] == ';') {
101       feat_str[feat_strlen-1] = '\0';
102     }
103 
104     feat_str = pstrcat(p, "LANG ", feat_str, NULL);
105     pr_feat_add(feat_str);
106 
107   } else {
108     feat_str = pstrcat(p, "LANG ", lang_curr, NULL);
109     pr_feat_add(feat_str);
110   }
111 }
112 
lang_feat_remove(void)113 static void lang_feat_remove(void) {
114   const char *feat, *lang_feat = NULL;
115 
116   feat = pr_feat_get();
117   while (feat) {
118     pr_signals_handle();
119 
120     if (strncmp(feat, C_LANG, 4) == 0) {
121       lang_feat = feat;
122       break;
123     }
124 
125     feat = pr_feat_get_next();
126   }
127 
128   if (lang_feat)
129     pr_feat_remove(lang_feat);
130 }
131 
lang_bind_domain(void)132 static const char *lang_bind_domain(void) {
133   const char *res = NULL;
134 
135 #ifdef HAVE_LIBINTL_H
136   pr_log_debug(DEBUG9, MOD_LANG_VERSION
137     ": binding to text domain 'proftpd' using locale path '%s'", lang_path);
138   res = bindtextdomain("proftpd", lang_path);
139   if (res == NULL) {
140     pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
141       ": unable to bind to text domain 'proftpd' using locale path '%s': %s",
142       lang_path, strerror(errno));
143     return NULL;
144 
145   } else {
146     textdomain("proftpd");
147     pr_log_debug(DEBUG9, MOD_LANG_VERSION ": using locale files in '%s'", res);
148   }
149 
150 #else
151   pr_log_debug(DEBUG7, MOD_LANG_VERSION
152     ": unable to bind to text domain 'proftpd', lacking libintl support");
153   errno = ENOSYS;
154 #endif /* !HAVE_LIBINTL_H */
155 
156   return res;
157 }
158 
lang_set_lang(pool * p,const char * lang)159 static int lang_set_lang(pool *p, const char *lang) {
160   char *curr_lang;
161 
162   if (lang_aliases != NULL) {
163     const void *v;
164 
165     /* Check to see if the given lang has an alias that has been determined
166      * to be acceptable.
167      */
168 
169     v = pr_table_get(lang_aliases, lang, NULL);
170     if (v != NULL) {
171       pr_log_debug(DEBUG9, MOD_LANG_VERSION ": '%s' is an alias for '%s'",
172         lang, (const char *) v);
173       lang = v;
174     }
175   }
176 
177   curr_lang = pstrdup(p, setlocale(LC_MESSAGES, NULL));
178 
179   /* XXX Do we need to set LC_COLLATE (e.g. for sorted directory listings)
180    * and/or LC_CTYPE (for iconv conversion) here as well?
181    */
182 
183   if (setlocale(LC_MESSAGES, lang) == NULL) {
184     if (errno == ENOENT) {
185       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
186         ": unknown/unsupported language '%s', ignoring", lang);
187 
188     } else {
189       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
190         ": unable to set LC_MESSAGES to '%s': %s", lang, strerror(errno));
191       return -1;
192     }
193 
194   } else {
195     curr_lang = setlocale(LC_MESSAGES, NULL);
196     pr_log_debug(DEBUG4, MOD_LANG_VERSION ": using %s messages",
197       *lang ? lang : curr_lang);
198 
199 
200     /* Set LC_COLLATE for strcoll(3), for sorted directory listings. */
201     if (setlocale(LC_COLLATE, curr_lang) == NULL) {
202       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
203         ": unable to set LC_COLLATE to '%s': %s", curr_lang, strerror(errno));
204     }
205 
206     /* Set LC_CTYPE for conversion, case-sensitive comparisons, and regexes. */
207     if (setlocale(LC_CTYPE, curr_lang) == NULL) {
208       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
209         ": unable to set LC_CTYPE to '%s': %s", curr_lang, strerror(errno));
210     }
211 
212     /* Set LC_MONETARY, for handling e.g the Euro symbol. */
213     if (setlocale(LC_MONETARY, curr_lang) == NULL) {
214       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
215         ": unable to set LC_MONETARY to '%s': %s", curr_lang, strerror(errno));
216     }
217   }
218 
219   /* In order to make gettext lookups work properly on some platforms
220    * (i.e. FreeBSD), the LANG environment variable MUST be set.  Apparently
221    * on these platforms, bindtextdomain(3) is not enough.  Sigh.
222    *
223    * This post first tipped me off to the solution for this problem:
224    *
225    *  http://fixunix.com/unix/243882-freebsd-locales.html
226    */
227 
228   pr_env_unset(session.pool, "LANG");
229   pr_env_set(session.pool, "LANG", lang);
230 
231   return 0;
232 }
233 
234 /* Supports comparison of RFC1766 language tags (case-insensitive, using
235  * hyphens) from the client with the underscore-using locale names usually
236  * used by iconv and setlocale().
237  */
lang_supported(pool * p,const char * lang)238 static int lang_supported(pool *p, const char *lang) {
239   register unsigned int i;
240   size_t lang_len;
241   char *lang_dup, **langs;
242   int ok = FALSE;
243 
244   if (lang_list == NULL) {
245     errno = EPERM;
246     return -1;
247   }
248 
249   if (lang_aliases != NULL) {
250     const void *v;
251 
252     /* Check to see if the given lang has an alias that has been determined
253      * to be acceptable.
254      */
255 
256     v = pr_table_get(lang_aliases, lang, NULL);
257     if (v != NULL) {
258       pr_log_debug(DEBUG9, MOD_LANG_VERSION ": using '%s' as alias for '%s'",
259         (const char *) v, lang);
260       lang = v;
261     }
262   }
263 
264   lang_dup = pstrdup(p, lang);
265 
266   lang_len = strlen(lang_dup);
267   if (lang_len > 4) {
268 
269     /* Transform something like "en-US" into "en_US". */
270     if (lang_dup[2] == '-') {
271       lang_dup[2] = '_';
272     }
273   }
274 
275   langs = lang_list->elts;
276 
277   for (i = 0; i < lang_list->nelts; i++) {
278     if (strcasecmp(langs[i], lang_dup) == 0) {
279       ok = TRUE;
280       break;
281     }
282   }
283 
284   if (!ok) {
285     errno = ENOENT;
286     return -1;
287   }
288 
289   return 0;
290 }
291 
292 /* Lookup/handle any UseEncoding configuration. */
process_encoding_config(int * utf8_client_encoding)293 static int process_encoding_config(int *utf8_client_encoding) {
294   config_rec *c;
295   int strict_encoding;
296 
297   c = find_config(main_server->conf, CONF_PARAM, "UseEncoding", FALSE);
298   if (c == NULL) {
299     errno = ENOENT;
300     return -1;
301   }
302 
303   if (c->argc == 1) {
304     lang_use_encoding = *((int *) c->argv[0]);
305     if (lang_use_encoding == TRUE) {
306       pr_fs_use_encoding(TRUE);
307 
308     } else {
309       pr_encode_disable_encoding();
310       pr_fs_use_encoding(FALSE);
311     }
312 
313     return 0;
314   }
315 
316   lang_local_charset = c->argv[0];
317   lang_client_charset = c->argv[1];
318   strict_encoding = *((int *) c->argv[2]);
319 
320   if (strict_encoding == TRUE) {
321     /* Fold the UseEncoding "strict" keyword functionality into LangOptions;
322      * we want to move people that way anyway.
323      */
324     lang_opts |= LANG_OPT_PREFER_SERVER_ENCODING;
325   }
326 
327   if (strcasecmp(lang_client_charset, "UTF8") == 0 ||
328       strcasecmp(lang_client_charset, "UTF-8") == 0) {
329     if (utf8_client_encoding) {
330       *utf8_client_encoding = TRUE;
331     }
332   }
333 
334   if (pr_encode_set_charset_encoding(lang_local_charset,
335       lang_client_charset) < 0) {
336     pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
337       ": error setting local charset '%s', client charset '%s': %s",
338       lang_local_charset, lang_client_charset, strerror(errno));
339     pr_fs_use_encoding(FALSE);
340 
341   } else {
342     pr_log_debug(DEBUG3, MOD_LANG_VERSION ": using local charset '%s', "
343       "client charset '%s' for path encoding", lang_local_charset,
344       lang_client_charset);
345     pr_fs_use_encoding(TRUE);
346 
347     /* Make sure that gettext() uses the specified charset as well. */
348     if (bind_textdomain_codeset("proftpd", lang_client_charset) == NULL) {
349       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
350         ": error setting client charset '%s' for localised messages: %s",
351         lang_client_charset, strerror(errno));
352     }
353   }
354 
355   return 0;
356 }
357 
358 /* Configuration handlers
359  */
360 
361 /* usage: LangDefault lang */
set_langdefault(cmd_rec * cmd)362 MODRET set_langdefault(cmd_rec *cmd) {
363   CHECK_ARGS(cmd, 1);
364   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
365 
366   (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
367   return PR_HANDLED(cmd);
368 }
369 
370 /* usage: LangEngine on|off */
set_langengine(cmd_rec * cmd)371 MODRET set_langengine(cmd_rec *cmd) {
372   int b;
373   config_rec *c;
374 
375   CHECK_ARGS(cmd, 1);
376   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
377 
378   b = get_boolean(cmd, 1);
379   if (b == -1) {
380     CONF_ERROR(cmd, "expected Boolean parameter");
381   }
382 
383   c = add_config_param(cmd->argv[0], 1, NULL);
384   c->argv[0] = pcalloc(c->pool, sizeof(int));
385   *((int *) c->argv[0]) = b;
386 
387   return PR_HANDLED(cmd);
388 }
389 
390 /* usage: LangOptions opt1 opt2 ... */
set_langoptions(cmd_rec * cmd)391 MODRET set_langoptions(cmd_rec *cmd) {
392   config_rec *c = NULL;
393   register unsigned int i = 0;
394   unsigned long opts = 0UL;
395 
396   if (cmd->argc-1 == 0)
397     CONF_ERROR(cmd, "wrong number of parameters");
398 
399   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
400 
401   c = add_config_param(cmd->argv[0], 1, NULL);
402 
403   for (i = 1; i < cmd->argc; i++) {
404     if (strcmp(cmd->argv[i], "PreferServerEncoding") == 0) {
405       opts |= LANG_OPT_PREFER_SERVER_ENCODING;
406 
407     } else if (strcmp(cmd->argv[i], "RequireValidEncoding") == 0) {
408       opts |= LANG_OPT_REQUIRE_VALID_ENCODING;
409 
410     } else {
411       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown LangOption '",
412         cmd->argv[i], "'", NULL));
413     }
414   }
415 
416   c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
417   *((unsigned long *) c->argv[0]) = opts;
418 
419   return PR_HANDLED(cmd);
420 }
421 
422 /* usage: LangPath path */
set_langpath(cmd_rec * cmd)423 MODRET set_langpath(cmd_rec *cmd) {
424   CHECK_ARGS(cmd, 1);
425   CHECK_CONF(cmd, CONF_ROOT);
426 
427   if (pr_fs_valid_path(cmd->argv[1]) < 0) {
428     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use path '",
429       cmd->argv[1], "' for locale files", NULL));
430   }
431 
432   lang_path = pstrdup(permanent_pool, cmd->argv[1]);
433   return PR_HANDLED(cmd);
434 }
435 
436 /* usage: UseEncoding on|off|local-charset client-charset ["strict"]*/
set_useencoding(cmd_rec * cmd)437 MODRET set_useencoding(cmd_rec *cmd) {
438   config_rec *c;
439 
440   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
441 
442   if (cmd->argc == 2) {
443     int use_encoding = -1;
444 
445     use_encoding = get_boolean(cmd, 1);
446     if (use_encoding == -1) {
447       CONF_ERROR(cmd, "expected Boolean parameter");
448     }
449 
450     c = add_config_param(cmd->argv[0], 1, NULL);
451     c->argv[0] = pcalloc(c->pool, sizeof(int));
452     *((int *) c->argv[0]) = use_encoding;
453 
454   } else if (cmd->argc == 3 ||
455              cmd->argc == 4) {
456 
457     if (cmd->argc == 4) {
458       if (strcasecmp(cmd->argv[3], "strict") != 0) {
459         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown UseEncoding keyword '",
460           cmd->argv[3], "'", NULL));
461       }
462     }
463 
464     c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
465     c->argv[0] = pstrdup(c->pool, cmd->argv[1]);
466     c->argv[1] = pstrdup(c->pool, cmd->argv[2]);
467     c->argv[2] = palloc(c->pool, sizeof(int));
468 
469     if (cmd->argc == 4) {
470       /* UseEncoding strict keyword in effect. */
471       *((int *) c->argv[2]) = TRUE;
472 
473     } else {
474       /* UseEncoding strict keyword NOT in effect. */
475       *((int *) c->argv[2]) = FALSE;
476     }
477 
478   } else {
479     CONF_ERROR(cmd, "wrong number of parameters");
480   }
481 
482   return PR_HANDLED(cmd);
483 }
484 
485 /* Command handlers
486  */
487 
lang_lang(cmd_rec * cmd)488 MODRET lang_lang(cmd_rec *cmd) {
489   unsigned char *authenticated;
490 
491   if (!lang_engine)
492     return PR_DECLINED(cmd);
493 
494   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, session.cwd, NULL)) {
495     pr_log_debug(DEBUG4, MOD_LANG_VERSION ": LANG command denied by <Limit>");
496     pr_response_add_err(R_500, _("Unable to handle command"));
497 
498     pr_cmd_set_errno(cmd, EPERM);
499     errno = EPERM;
500     return PR_ERROR(cmd);
501   }
502 
503   /* If the user has already authenticated (and thus possibly chrooted),
504    * deny the command.  Once chrooted, we will not have access to the
505    * message catalog files anymore.
506    *
507    * True, the user may not have been chrooted, but if we allow non-chrooted
508    * users to issue LANG commands while chrooted users cannot, it can
509    * constitute an information leak.  Best to avoid that altogether.
510    */
511   authenticated = get_param_ptr(cmd->server->conf, "authenticated", FALSE);
512   if (authenticated &&
513       *authenticated == TRUE) {
514     pr_log_debug(DEBUG7, MOD_LANG_VERSION ": assuming language files are "
515       "unavailable after login, denying LANG command");
516     pr_response_add_err(R_500, _("Unable to handle command"));
517 
518     pr_cmd_set_errno(cmd, EPERM);
519     errno = EPERM;
520     return PR_ERROR(cmd);
521   }
522 
523   if (cmd->argc > 2) {
524     pr_response_add_err(R_501, _("Invalid number of parameters"));
525 
526     pr_cmd_set_errno(cmd, EINVAL);
527     errno = EINVAL;
528     return PR_ERROR(cmd);
529   }
530 
531   if (cmd->argc == 1) {
532     pr_log_debug(DEBUG7, MOD_LANG_VERSION
533       ": resetting to default language '%s'", lang_default);
534 
535     if (lang_set_lang(cmd->tmp_pool, lang_default) < 0) {
536       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
537         ": unable to use LangDefault '%s': %s", lang_default, strerror(errno));
538       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
539         ": using LC_ALL environment variable value instead");
540     }
541 
542     pr_response_add(R_200, _("Using default language %s"), lang_default);
543     return PR_HANDLED(cmd);
544   }
545 
546   if (lang_supported(cmd->tmp_pool, cmd->argv[1]) < 0) {
547     pr_log_debug(DEBUG3, MOD_LANG_VERSION ": language '%s' unsupported: %s",
548       (char *) cmd->argv[1], strerror(errno));
549     pr_response_add_err(R_504, _("Language %s not supported"),
550       (char *) cmd->argv[1]);
551 
552     pr_cmd_set_errno(cmd, EPERM);
553     errno = EPERM;
554     return PR_ERROR(cmd);
555   }
556 
557   pr_log_debug(DEBUG7, MOD_LANG_VERSION
558     ": setting to client-requested language '%s'", (char *) cmd->argv[1]);
559 
560   if (lang_set_lang(cmd->tmp_pool, cmd->argv[1]) < 0) {
561     pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
562       ": unable to use client-requested language '%s': %s",
563       (char *) cmd->argv[1], strerror(errno));
564     pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
565       ": using LangDefault '%s' instead", lang_default);
566 
567     if (lang_set_lang(cmd->tmp_pool, lang_default) < 0) {
568       pr_log_pri(PR_LOG_WARNING, MOD_LANG_VERSION
569         ": unable to use LangDefault '%s': %s", lang_default, strerror(errno));
570       pr_session_disconnect(&lang_module, PR_SESS_DISCONNECT_BAD_CONFIG, NULL);
571     }
572   }
573 
574   lang_curr = pstrdup(lang_pool, cmd->argv[1]);
575 
576   pr_log_debug(DEBUG5, MOD_LANG_VERSION
577     ": now using client-requested language '%s'", lang_curr);
578 
579   /* If successful, remove the previous FEAT line for LANG, and update it
580    * with a new one showing the currently selected language.
581    */
582 
583   lang_feat_remove();
584   lang_feat_add(cmd->tmp_pool);
585 
586   pr_response_add(R_200, _("Using language %s"), lang_curr);
587   return PR_HANDLED(cmd);
588 }
589 
lang_post_pass(cmd_rec * cmd)590 MODRET lang_post_pass(cmd_rec *cmd) {
591   (void) process_encoding_config(NULL);
592   return PR_DECLINED(cmd);
593 }
594 
lang_utf8(cmd_rec * cmd)595 MODRET lang_utf8(cmd_rec *cmd) {
596   register unsigned int i;
597   int use_utf8;
598   const char *curr_encoding;
599   char *method;
600 
601   method = pstrdup(cmd->tmp_pool, cmd->argv[0]);
602 
603   /* Convert underscores to spaces in the method name, for prettier
604    * logging.
605    */
606   for (i = 0; method[i]; i++) {
607     if (method[i] == '_') {
608       method[i] = ' ';
609     }
610   }
611 
612   if (cmd->argc != 2) {
613     pr_response_add_err(R_501, _("'%s' not understood"), method);
614 
615     pr_cmd_set_errno(cmd, EINVAL);
616     errno = EINVAL;
617     return PR_ERROR(cmd);
618   }
619 
620   use_utf8 = get_boolean(cmd, 1);
621   if (use_utf8 < 0) {
622     pr_response_add_err(R_501, _("'%s' not understood"), method);
623 
624     pr_cmd_set_errno(cmd, EINVAL);
625     errno = EINVAL;
626     return PR_ERROR(cmd);
627   }
628 
629   curr_encoding = pr_encode_get_encoding();
630   if (curr_encoding != NULL) {
631     pr_log_debug(DEBUG9, MOD_LANG_VERSION
632       ": Handling OPTS UTF8 %s (current encoding is '%s')",
633       (char *) cmd->argv[1], curr_encoding);
634 
635   } else {
636     pr_log_debug(DEBUG9, MOD_LANG_VERSION
637       ": Handling OPTS UTF8 %s (encoding currently disabled)",
638       (char *) cmd->argv[1]);
639   }
640 
641   if (pr_encode_is_utf8(curr_encoding) == TRUE) {
642     if (use_utf8) {
643       /* Client requested that we use UTF8, and we already are.  Nothing
644        * more needs to be done.
645        */
646       pr_response_add(R_200, _("UTF8 set to on"));
647 
648     } else {
649       /* Client requested that we NOT use UTF8 (i.e. "OPTS UTF8 off"), and
650        * we are.  Need to disable encoding, then, unless the
651        * LangOptions/UseEncoding settings dictate that we MUST use UTF8.
652        */
653 
654       if (lang_use_encoding == TRUE) {
655         /* We have explicit instructions; we cannot change the encoding use as
656          * requested by the client.
657          */
658         pr_log_debug(DEBUG5, MOD_LANG_VERSION
659           ": unable to accept 'OPTS UTF8 off' due to LangOptions/UseEncoding "
660           "directive in config file");
661         pr_response_add_err(R_451, _("Unable to accept %s"), method);
662 
663         pr_cmd_set_errno(cmd, EPERM);
664         errno = EPERM;
665         return PR_ERROR(cmd);
666 
667       } else if (lang_local_charset != NULL &&
668                  lang_client_charset != NULL) {
669 
670         /* UseEncoding configured with specific charsets, and the client
671          * requested that we turn UTF8 support off.  Easy enough; just
672          * (re)set the encoding to use the configured charsets.
673          */
674 
675         if (lang_opts & LANG_OPT_PREFER_SERVER_ENCODING) {
676           /* We have explicit instructions; we cannot change the encoding
677            * use as requested by the client.
678            */
679           pr_log_debug(DEBUG5, MOD_LANG_VERSION
680             ": unable to accept 'OPTS UTF8 off' due to "
681             "LangOptions/UseEncoding directive in config file");
682           pr_response_add_err(R_451, _("Unable to accept %s"), method);
683 
684           pr_cmd_set_errno(cmd, EPERM);
685           errno = EPERM;
686           return PR_ERROR(cmd);
687         }
688 
689         if (pr_encode_set_charset_encoding(lang_local_charset,
690             lang_client_charset) < 0) {
691 
692           pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
693             ": error setting local charset '%s', client charset '%s': %s",
694             lang_local_charset, lang_client_charset, strerror(errno));
695           pr_fs_use_encoding(FALSE);
696           pr_response_add_err(R_451, _("Unable to accept %s"), method);
697 
698           pr_cmd_set_errno(cmd, EPERM);
699           errno = EPERM;
700           return PR_ERROR(cmd);
701         }
702 
703         pr_log_debug(DEBUG3, MOD_LANG_VERSION ": using local charset '%s', "
704           "client charset '%s' for path encoding", lang_local_charset,
705           lang_client_charset);
706         pr_fs_use_encoding(TRUE);
707         pr_response_add(R_200, _("UTF8 set to off"));
708         return PR_HANDLED(cmd);
709       }
710 
711       pr_log_debug(DEBUG5, MOD_LANG_VERSION
712         ": disabling use of UTF8 encoding as per client's request");
713 
714       /* No explicit UseEncoding instructions; we can turn off encoding. */
715       pr_encode_disable_encoding();
716       pr_fs_use_encoding(FALSE);
717       pr_response_add(R_200, _("UTF8 set to off"));
718     }
719 
720   } else {
721     if (use_utf8) {
722       /* Client requested that we use UTF8 (i.e. "OPTS UTF8 on"), and we
723        * currently are not.  Enable UTF8 encoding, unless the
724        * LangOptions/UseEncoding setting dictates that we cannot.
725        */
726 
727       if (lang_use_encoding == FALSE) {
728         /* We have explicit instructions. */
729         pr_log_debug(DEBUG5, MOD_LANG_VERSION
730           ": unable to accept 'OPTS UTF8 on' due to LangOptions/UseEncoding "
731           "directive in config file");
732         pr_response_add_err(R_451, _("Unable to accept %s"), method);
733 
734         pr_cmd_set_errno(cmd, EPERM);
735         errno = EPERM;
736         return PR_ERROR(cmd);
737 
738       } else if (lang_opts & LANG_OPT_PREFER_SERVER_ENCODING) {
739         /* We have explicit instructions; we cannot change the encoding use
740          * as requested by the client.
741          */
742         pr_log_debug(DEBUG5, MOD_LANG_VERSION
743           ": unable to accept 'OPTS UTF8 on' due to LangOptions/UseEncoding "
744           "directive in config file");
745         pr_response_add_err(R_451, _("Unable to accept %s"), method);
746 
747         pr_cmd_set_errno(cmd, EPERM);
748         errno = EPERM;
749         return PR_ERROR(cmd);
750       }
751 
752       pr_log_debug(DEBUG5, MOD_LANG_VERSION
753         ": enabling use of UTF8 encoding as per client's request");
754 
755       /* No explicit UseEncoding instructions; we can turn on encoding. */
756       if (pr_encode_enable_encoding("UTF-8") < 0) {
757         pr_log_debug(DEBUG3, MOD_LANG_VERSION
758           ": error enabling UTF8 encoding: %s", strerror(errno));
759         pr_response_add_err(R_451, _("Unable to accept %s"), method);
760 
761         pr_cmd_set_errno(cmd, EPERM);
762         errno = EPERM;
763         return PR_ERROR(cmd);
764       }
765 
766       pr_fs_use_encoding(TRUE);
767       pr_response_add(R_200, _("UTF8 set to on"));
768 
769     } else {
770       /* Client requested that we not use UTF8, and we are not.  Nothing more
771        * needs to be done.
772        */
773       pr_response_add(R_200, _("UTF8 set to off"));
774     }
775   }
776 
777   return PR_HANDLED(cmd);
778 }
779 
780 /* Event handlers
781  */
782 
lang_postparse_ev(const void * event_data,void * user_data)783 static void lang_postparse_ev(const void *event_data, void *user_data) {
784   pool *tmp_pool;
785   config_rec *c;
786   DIR *dirh;
787   server_rec *s;
788   const char *locale_path = NULL;
789 
790   c = find_config(main_server->conf, CONF_PARAM, "LangEngine", FALSE);
791   if (c) {
792     int engine;
793 
794     engine = *((int *) c->argv[0]);
795     if (!engine)
796       return;
797   }
798 
799   /* Scan the LangPath for the .mo files to read in. */
800 
801   locale_path = lang_bind_domain();
802   if (locale_path == NULL)
803     return;
804 
805   /* Scan locale_path using readdir(), to get the list of available
806    * translations/langs.  Make sure to check for presence of 'proftpd.mo'
807    * in the directories:
808    *
809    *  $lang/LC_MESSAGES/proftpd.mo
810    *
811    * In addition, make sure the directory name is a locale acceptable to
812    * setlocale(3).
813    */
814 
815   tmp_pool = make_sub_pool(lang_pool);
816 
817   dirh = opendir(locale_path);
818   if (dirh != NULL) {
819     register unsigned int i;
820     struct dirent *dent;
821     char *curr_locale, *langs_str = "", **langs = NULL;
822 
823     if (!lang_list) {
824       lang_list = make_array(lang_pool, 3, sizeof(char *));
825     }
826 
827     curr_locale = pstrdup(tmp_pool, setlocale(LC_MESSAGES, NULL));
828 
829     while ((dent = readdir(dirh)) != NULL) {
830       char *mo;
831       struct stat st;
832 
833       pr_signals_handle();
834 
835       if (strncmp(dent->d_name, ".", 2) == 0 ||
836           strncmp(dent->d_name, "..", 3) == 0) {
837         continue;
838       }
839 
840       mo = pdircat(tmp_pool, locale_path, dent->d_name, "LC_MESSAGES",
841         "proftpd.mo", NULL);
842 
843       if (stat(mo, &st) == 0) {
844         register unsigned int j;
845         char *locale_name;
846 
847         /* Check that dent->d_name is a valid language name according to
848          * setlocale(3) before adding it to the list.
849          *
850          * Note that proftpd's .po files do not include the optional codeset
851          * modifier in the file name, i.e.:
852          *
853          *  lang[_territory[.codeset[@modifier]]]
854          *
855          * Thus if setlocale() returns ENOENT, we will automatically try
856          * appending a ".UTF-8" to see if setlocale() accepts that.
857          */
858 
859         locale_name = dent->d_name;
860 
861         for (j = 0; j < 2; j++) {
862           if (setlocale(LC_MESSAGES, locale_name) != NULL) {
863             *((char **) push_array(lang_list)) = pstrdup(lang_pool,
864               locale_name);
865 
866             /* If this is not the first setlocale() attempt, then we have
867              * automatically appending ".UTF-8" (or ".utf8") to the file name.
868              * In which case we want to allow the non-".UTF-8"/non-".utf8"
869              * locale name as an acceptable alias.
870              */
871             if (j > 0) {
872               if (lang_aliases == NULL) {
873                 lang_aliases = pr_table_alloc(lang_pool, 0);
874               }
875 
876               pr_table_add(lang_aliases, pstrdup(lang_pool, dent->d_name),
877                 pstrdup(lang_pool, locale_name), 0);
878 
879               /* Make sure the original name up in our "supported languages"
880                * list as well.
881                */
882               *((char **) push_array(lang_list)) = pstrdup(lang_pool,
883                 dent->d_name);
884             }
885 
886             break;
887           }
888 
889           if (errno == ENOENT) {
890             if (j == 0) {
891               locale_name = pstrcat(tmp_pool, dent->d_name, ".UTF-8", NULL);
892               continue;
893 
894             } else {
895               pr_log_debug(DEBUG5, MOD_LANG_VERSION
896                 ": skipping possible language '%s': not supported by "
897                 "setlocale(3); see `locale -a'", dent->d_name);
898             }
899 
900           } else {
901             pr_log_debug(DEBUG5, MOD_LANG_VERSION
902               ": skipping possible language '%s': %s", dent->d_name,
903               strerror(errno));
904           }
905         }
906       }
907     }
908 
909     /* Restore the current locale. */
910     setlocale(LC_MESSAGES, curr_locale);
911 
912     closedir(dirh);
913 
914     langs = lang_list->elts;
915     for (i = 0; i < lang_list->nelts; i++) {
916       langs_str = pstrcat(tmp_pool, langs_str, *langs_str ? ", " : "",
917         langs[i], NULL);
918     }
919 
920     if (lang_list->nelts > 0) {
921       pr_log_debug(DEBUG8, MOD_LANG_VERSION
922       ": added the following supported languages: %s", langs_str);
923     }
924 
925   } else {
926     pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
927       ": unable to scan the localised files in '%s': %s", locale_path,
928       strerror(errno));
929   }
930 
931   /* Iterate through the server list, checking each for a configured
932    * LangDefault.  If configured, make sure that the specified language is
933    * supported.
934    */
935 
936   for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
937     c = find_config(s->conf, CONF_PARAM, "LangDefault", FALSE);
938     if (c) {
939       char *lang = c->argv[0];
940 
941       if (lang_supported(tmp_pool, lang) < 0) {
942         pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
943           ": LangDefault '%s', configured for server '%s', is not a supported "
944           "language, removing", lang, s->ServerName);
945         pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
946           ": Perhaps proftpd has not yet been translated into '%s'", lang);
947         remove_config(s->conf, "LangDefault", FALSE);
948       }
949     }
950   }
951 
952   if (tmp_pool)
953     destroy_pool(tmp_pool);
954 }
955 
lang_restart_ev(const void * event_data,void * user_data)956 static void lang_restart_ev(const void *event_data, void *user_data) {
957   destroy_pool(lang_pool);
958   lang_curr = LANG_DEFAULT_LANG;
959   lang_list = NULL;
960   lang_aliases = NULL;
961 
962   lang_pool = make_sub_pool(permanent_pool);
963   pr_pool_tag(lang_pool, MOD_LANG_VERSION);
964 }
965 
966 /* Initialization functions
967  */
968 
lang_init(void)969 static int lang_init(void) {
970   lang_pool = make_sub_pool(permanent_pool);
971   pr_pool_tag(lang_pool, MOD_LANG_VERSION);
972 
973   pr_event_register(&lang_module, "core.postparse", lang_postparse_ev, NULL);
974   pr_event_register(&lang_module, "core.restart", lang_restart_ev, NULL);
975 
976   return 0;
977 }
978 
lang_sess_init(void)979 static int lang_sess_init(void) {
980   config_rec *c;
981   int res, utf8_client_encoding = FALSE;
982 
983   c = find_config(main_server->conf, CONF_PARAM, "LangEngine", FALSE);
984   if (c != NULL) {
985     lang_engine = *((int *) c->argv[0]);
986   }
987 
988   if (lang_engine == FALSE) {
989     return 0;
990   }
991 
992   c = find_config(main_server->conf, CONF_PARAM, "LangDefault", FALSE);
993   if (c != NULL) {
994     char *lang;
995 
996     lang = c->argv[0];
997 
998     if (lang_set_lang(lang_pool, lang) < 0) {
999       pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
1000         ": unable to use LangDefault '%s': %s", lang, strerror(errno));
1001     }
1002 
1003     pr_log_debug(DEBUG9, MOD_LANG_VERSION ": using LangDefault '%s'", lang);
1004     lang_curr = lang_default = lang;
1005 
1006   } else {
1007     /* No explicit default language configured; rely on the environment
1008      * variables (which will already have been picked up).
1009      */
1010 
1011     lang_curr = pstrdup(lang_pool, setlocale(LC_MESSAGES, NULL));
1012     if (strcasecmp(lang_curr, "C") == 0) {
1013       lang_curr = LANG_DEFAULT_LANG;
1014     }
1015 
1016     lang_default = lang_curr;
1017 
1018     /* If a list of languages is empty (perhaps because the message catalogs
1019      * could not be found for some reason), we should still have an entry for
1020      * the current language.
1021      */
1022     if (lang_list == NULL) {
1023       lang_list = make_array(lang_pool, 1, sizeof(char *));
1024     }
1025 
1026     if (lang_list->nelts == 0) {
1027       *((char **) push_array(lang_list)) = pstrdup(lang_pool, lang_curr);
1028     }
1029   }
1030 
1031   c = find_config(main_server->conf, CONF_PARAM, "LangOptions", FALSE);
1032   while (c != NULL) {
1033     unsigned long opts = 0;
1034 
1035     pr_signals_handle();
1036 
1037     opts = *((unsigned long *) c->argv[0]);
1038     lang_opts |= opts;
1039 
1040     c = find_config_next(c, c->next, CONF_PARAM, "LangOptions", FALSE);
1041   }
1042 
1043   if (lang_opts & LANG_OPT_REQUIRE_VALID_ENCODING) {
1044     unsigned long encoding_policy;
1045 
1046     encoding_policy = pr_encode_get_policy();
1047     encoding_policy |= PR_ENCODE_POLICY_FL_REQUIRE_VALID_ENCODING;
1048 
1049     pr_encode_set_policy(encoding_policy);
1050   }
1051 
1052   res = process_encoding_config(&utf8_client_encoding);
1053   if (res < 0 &&
1054       errno == ENOENT) {
1055     /* Default is to use UTF8. */
1056     pr_fs_use_encoding(TRUE);
1057   }
1058 
1059   /* If the PreferServerEncoding LangOption is not set, OR if the encoding
1060    * configured explicitly requests UTF8 from the client, then we can list
1061    * UTF8 in the FEAT response.
1062    */
1063   if (!(lang_opts & LANG_OPT_PREFER_SERVER_ENCODING) ||
1064       utf8_client_encoding == TRUE) {
1065     pr_feat_add("UTF8");
1066   }
1067 
1068   /* Configure a proper FEAT line, for our supported languages and our
1069    * default language.
1070    */
1071   lang_feat_add(main_server->pool);
1072 
1073   return 0;
1074 }
1075 
1076 /* Module API tables
1077  */
1078 
1079 static conftable lang_conftab[] = {
1080   { "LangDefault",	set_langdefault,	NULL },
1081   { "LangEngine",	set_langengine,		NULL },
1082   { "LangOptions",	set_langoptions,	NULL },
1083   { "LangPath",		set_langpath,		NULL },
1084   { "UseEncoding",	set_useencoding,	NULL },
1085   { NULL }
1086 };
1087 
1088 static cmdtable lang_cmdtab[] = {
1089   { CMD,	C_LANG,			G_NONE,	lang_lang,	FALSE,	FALSE },
1090   { CMD,	C_OPTS "_UTF8",		G_NONE,	lang_utf8,	FALSE,	FALSE },
1091   { POST_CMD,	C_PASS,			G_NONE,	lang_post_pass,	FALSE,	FALSE },
1092   { 0, NULL }
1093 };
1094 
1095 module lang_module = {
1096   NULL, NULL,
1097 
1098   /* Module API version 2.0 */
1099   0x20,
1100 
1101   /* Module name */
1102   "lang",
1103 
1104   /* Module configuration handler table */
1105   lang_conftab,
1106 
1107   /* Module command handler table */
1108   lang_cmdtab,
1109 
1110   /* Module authentication handler table */
1111   NULL,
1112 
1113   /* Module initialization function */
1114   lang_init,
1115 
1116   /* Session initialization function */
1117   lang_sess_init,
1118 
1119   /* Module version */
1120   MOD_LANG_VERSION
1121 };
1122 
1123 #endif /* PR_USE_NLS */
1124