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