Coverage Report

Created: 2025-07-18 06:04

/src/proftpd/modules/mod_site.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * ProFTPD - FTP server daemon
3
 * Copyright (c) 1997, 1998 Public Flood Software
4
 * Copyright (c) 2001-2021 The ProFTPD Project team
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 2 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
19
 *
20
 * As a special exemption, Public Flood Software/MacGyver aka Habeeb J. Dihu
21
 * and other respective copyright holders give permission to link this program
22
 * with OpenSSL, and distribute the resulting executable, without including
23
 * the source code for OpenSSL in the source distribution.
24
 */
25
26
/* "SITE" commands module for ProFTPD. */
27
28
#include "conf.h"
29
30
modret_t *site_dispatch(cmd_rec *cmd);
31
32
static struct {
33
  char *cmd;
34
  char *syntax;
35
  int implemented;
36
} _help[] = {
37
  { "HELP", "[<sp> site-command]",      TRUE },
38
  { "CHGRP",  "<sp> group <sp> pathname",   TRUE },
39
  { "CHMOD",  "<sp> mode <sp> pathname",    TRUE },
40
  { NULL, NULL,         FALSE }
41
};
42
43
0
static char *full_cmd(cmd_rec *cmd) {
44
0
  register unsigned int i;
45
0
  char *res = "";
46
0
  size_t reslen = 0;
47
48
0
  for (i = 0; i < cmd->argc; i++) {
49
0
    res = pstrcat(cmd->tmp_pool, res, cmd->argv[i], " ", NULL);
50
0
  }
51
52
0
  reslen = strlen(res);
53
0
  while (reslen >= 1 &&
54
0
         res[reslen-1] == ' ') {
55
0
    res[reslen-1] = '\0';
56
0
    reslen--;
57
0
  }
58
59
0
  return res;
60
0
}
61
62
0
MODRET site_chgrp(cmd_rec *cmd) {
63
0
  int res;
64
0
  gid_t gid;
65
0
  char *path = NULL, *tmp = NULL, *arg = "";
66
0
  register unsigned int i = 0;
67
0
#ifdef PR_USE_REGEX
68
0
  pr_regex_t *pre;
69
0
#endif
70
71
0
  if (cmd->argc < 3) {
72
0
    pr_response_add_err(R_500, _("'SITE %s' not understood"), full_cmd(cmd));
73
0
    return NULL;
74
0
  }
75
76
  /* Construct the target file name by concatenating all the parameters after
77
   * the mode, separating them with spaces.
78
   */
79
0
  for (i = 2; i <= cmd->argc-1; i++) {
80
0
    char *decoded_path;
81
82
0
    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[i],
83
0
      FSIO_DECODE_FL_TELL_ERRORS);
84
0
    if (decoded_path == NULL) {
85
0
      int xerrno = errno;
86
87
0
      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
88
0
        (char *) cmd->argv[i], strerror(xerrno));
89
0
      pr_response_add_err(R_550,
90
0
        _("SITE %s: Illegal character sequence in command"),
91
0
        (char *) cmd->argv[1]);
92
93
0
      pr_cmd_set_errno(cmd, xerrno);
94
0
      errno = xerrno;
95
0
      return PR_ERROR(cmd);
96
0
    }
97
98
0
    arg = pstrcat(cmd->tmp_pool, arg, *arg ? " " : "", decoded_path, NULL);
99
0
  }
100
101
0
#ifdef PR_USE_REGEX
102
0
  pre = get_param_ptr(CURRENT_CONF, "PathAllowFilter", FALSE);
103
0
  if (pre != NULL &&
104
0
      pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) != 0) {
105
0
    pr_log_pri(PR_LOG_NOTICE, "'%s %s' denied by PathAllowFilter",
106
0
      (char *) cmd->argv[0], arg);
107
0
    pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
108
109
0
    pr_cmd_set_errno(cmd, EPERM);
110
0
    errno = EPERM;
111
0
    return PR_ERROR(cmd);
112
0
  }
113
114
0
  pre = get_param_ptr(CURRENT_CONF, "PathDenyFilter", FALSE);
115
0
  if (pre != NULL &&
116
0
      pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) == 0) {
117
0
    pr_log_pri(PR_LOG_NOTICE, "'%s %s' denied by PathDenyFilter",
118
0
      (char *) cmd->argv[0], arg);
119
0
    pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
120
121
0
    pr_cmd_set_errno(cmd, EPERM);
122
0
    errno = EPERM;
123
0
    return PR_ERROR(cmd);
124
0
  }
125
0
#endif
126
127
0
  pr_fs_clear_cache2(arg);
128
0
  path = dir_realpath(cmd->tmp_pool, arg);
129
0
  if (path == NULL) {
130
0
    int xerrno = errno;
131
132
0
    pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
133
134
0
    pr_cmd_set_errno(cmd, xerrno);
135
0
    errno = xerrno;
136
0
    return PR_ERROR(cmd);
137
0
  }
138
139
  /* Map the given group argument, if a string, to a GID.  If already a
140
   * number, pass through as is.
141
   */
142
0
  gid = strtoul(cmd->argv[1], &tmp, 10);
143
144
0
  if (tmp && *tmp) {
145
146
    /* Try the parameter as a group name. */
147
0
    gid = pr_auth_name2gid(cmd->tmp_pool, cmd->argv[1]);
148
0
    if (gid == (gid_t) -1) {
149
0
      int xerrno = EINVAL;
150
151
0
      pr_log_debug(DEBUG9,
152
0
        "SITE CHGRP: Unable to resolve group name '%s' to GID",
153
0
        (char *) cmd->argv[1]);
154
0
      pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
155
156
0
      pr_cmd_set_errno(cmd, xerrno);
157
0
      errno = xerrno;
158
0
      return PR_ERROR(cmd);
159
0
    }
160
0
  }
161
162
0
  res = core_chgrp(cmd, path, (uid_t) -1, gid);
163
0
  if (res < 0) {
164
0
    int xerrno = errno;
165
166
0
    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
167
0
      "error chown'ing '%s' to GID %s: %s", (char *) cmd->argv[0], session.user,
168
0
      pr_uid2str(cmd->tmp_pool, session.uid),
169
0
      pr_gid2str(cmd->tmp_pool, session.gid), path,
170
0
      pr_gid2str(cmd->tmp_pool, gid), strerror(xerrno));
171
172
0
    pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
173
174
0
    pr_cmd_set_errno(cmd, xerrno);
175
0
    errno = xerrno;
176
0
    return PR_ERROR(cmd);
177
0
  }
178
179
0
  pr_response_add(R_200, _("SITE %s command successful"),
180
0
    (char *) cmd->argv[0]);
181
0
  return PR_HANDLED(cmd);
182
0
}
183
184
0
MODRET site_chmod(cmd_rec *cmd) {
185
0
  int res;
186
0
  mode_t mode = 0;
187
0
  char *dir, *endp, *mode_str, *tmp, *arg = "";
188
0
  struct stat st;
189
0
  register unsigned int i = 0;
190
0
#ifdef PR_USE_REGEX
191
0
  pr_regex_t *pre;
192
0
#endif
193
194
0
  if (cmd->argc < 3) {
195
0
    pr_response_add_err(R_500, _("'SITE %s' not understood"), full_cmd(cmd));
196
0
    return NULL;
197
0
  }
198
199
  /* Construct the target file name by concatenating all the parameters after
200
   * the mode, separating them with spaces.
201
   */
202
0
  for (i = 2; i <= cmd->argc-1; i++) {
203
0
    char *decoded_path;
204
205
0
    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[i],
206
0
      FSIO_DECODE_FL_TELL_ERRORS);
207
0
    if (decoded_path == NULL) {
208
0
      int xerrno = errno;
209
210
0
      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
211
0
        (char *) cmd->argv[i], strerror(xerrno));
212
0
      pr_response_add_err(R_550,
213
0
        _("SITE %s: Illegal character sequence in command"),
214
0
        (char *) cmd->argv[1]);
215
216
0
      pr_cmd_set_errno(cmd, xerrno);
217
0
      errno = xerrno;
218
0
      return PR_ERROR(cmd);
219
0
    }
220
221
0
    arg = pstrcat(cmd->tmp_pool, arg, *arg ? " " : "", decoded_path, NULL);
222
0
  }
223
224
0
#ifdef PR_USE_REGEX
225
0
  pre = get_param_ptr(CURRENT_CONF, "PathAllowFilter", FALSE);
226
0
  if (pre != NULL &&
227
0
      pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) != 0) {
228
0
    pr_log_pri(PR_LOG_NOTICE, "'%s %s %s' denied by PathAllowFilter",
229
0
      (char *) cmd->argv[0], (char *) cmd->argv[1], arg);
230
0
    pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
231
232
0
    pr_cmd_set_errno(cmd, EPERM);
233
0
    errno = EPERM;
234
0
    return PR_ERROR(cmd);
235
0
  }
236
237
0
  pre = get_param_ptr(CURRENT_CONF, "PathDenyFilter", FALSE);
238
0
  if (pre != NULL &&
239
0
      pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) == 0) {
240
0
    pr_log_pri(PR_LOG_NOTICE, "'%s %s %s' denied by PathDenyFilter",
241
0
      (char *) cmd->argv[0], (char *) cmd->argv[1], arg);
242
0
    pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
243
244
0
    pr_cmd_set_errno(cmd, EPERM);
245
0
    errno = EPERM;
246
0
    return PR_ERROR(cmd);
247
0
  }
248
0
#endif
249
250
0
  pr_fs_clear_cache2(arg);
251
0
  dir = dir_realpath(cmd->tmp_pool, arg);
252
0
  if (dir == NULL) {
253
0
    int xerrno = errno;
254
255
0
    pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
256
257
0
    pr_cmd_set_errno(cmd, xerrno);
258
0
    errno = xerrno;
259
0
    return PR_ERROR(cmd);
260
0
  }
261
262
  /* If the first character isn't '0', prepend it and attempt conversion.
263
   * This will fail if the chmod is a symbolic, but takes care of the
264
   * case where an octal number is sent without the leading '0'.
265
   */
266
0
  mode_str = cmd->argv[1];
267
0
  if (mode_str[0] != '0') {
268
0
    tmp = pstrcat(cmd->tmp_pool, "0", mode_str, NULL);
269
270
0
  } else {
271
0
    tmp = mode_str;
272
0
  }
273
274
0
  mode = strtol(tmp, &endp, 0);
275
0
  if (endp && *endp) {
276
    /* It's not an absolute number, try symbolic */
277
0
    char *cp = mode_str;
278
0
    int mask = 0, mode_op = 0, curr_mode = 0, curr_umask = umask(0);
279
0
    int invalid = 0;
280
0
    char *who, *how, *what;
281
282
0
    umask(curr_umask);
283
0
    mode = 0;
284
285
0
    if (pr_fsio_stat(dir, &st) != -1) {
286
0
      curr_mode = st.st_mode;
287
0
    }
288
289
0
    while (TRUE) {
290
0
      pr_signals_handle();
291
292
0
      who = pstrdup(cmd->tmp_pool, cp);
293
294
0
      tmp = strpbrk(who, "+-=");
295
0
      if (tmp != NULL) {
296
0
        how = pstrdup(cmd->tmp_pool, tmp);
297
0
        if (*how != '=') {
298
0
          mode = curr_mode;
299
0
        }
300
301
0
        *tmp = '\0';
302
303
0
      } else {
304
0
        invalid++;
305
0
        break;
306
0
      }
307
308
0
      tmp = strpbrk(how, "rwxXstugo");
309
0
      if (tmp != NULL) {
310
0
        what = pstrdup(cmd->tmp_pool, tmp);
311
0
        *tmp = '\0';
312
313
0
      } else {
314
0
        invalid++;
315
0
        break;
316
0
      }
317
318
0
      cp = what;
319
0
      while (cp) {
320
0
        switch (*who) {
321
0
          case 'u':
322
0
            mask = 0077;
323
0
            break;
324
325
0
          case 'g':
326
0
            mask = 0707;
327
0
            break;
328
329
0
          case 'o':
330
0
            mask = 0770;
331
0
            break;
332
333
0
          case 'a':
334
0
            mask = 0000;
335
0
            break;
336
337
0
          case '\0':
338
0
            mask = curr_umask;
339
0
            break;
340
341
0
          default:
342
0
            invalid++;
343
0
            break;
344
0
        }
345
346
0
        if (invalid) {
347
0
          break;
348
0
        }
349
350
0
        switch (*how) {
351
0
          case '+':
352
0
          case '-':
353
0
          case '=':
354
0
            break;
355
356
0
          default:
357
0
            invalid++;
358
0
        }
359
360
0
        if (invalid) {
361
0
          break;
362
0
        }
363
364
0
        switch (*cp) {
365
0
          case 'r':
366
0
            mode_op |= (S_IRUSR|S_IRGRP|S_IROTH);
367
0
            break;
368
369
0
          case 'w':
370
0
            mode_op |= (S_IWUSR|S_IWGRP|S_IWOTH);
371
0
            break;
372
373
0
          case 'x':
374
0
            mode_op |= (S_IXUSR|S_IXGRP|S_IXOTH);
375
0
            break;
376
377
          /* 'X' not implemented */
378
0
          case 's':
379
            /* setuid */
380
0
            mode_op |= S_ISUID;
381
0
            break;
382
383
0
          case 't':
384
            /* sticky */
385
0
            mode_op |= S_ISVTX;
386
0
            break;
387
388
0
          case 'o':
389
0
            mode_op |= (curr_mode & S_IRWXO);
390
0
            mode_op |= ((curr_mode & S_IRWXO) << 3);
391
0
            mode_op |= ((curr_mode & S_IRWXO) << 6);
392
0
            break;
393
394
0
          case 'g':
395
0
            mode_op |= ((curr_mode & S_IRWXG) >> 3);
396
0
            mode_op |= (curr_mode & S_IRWXG);
397
0
            mode_op |= ((curr_mode & S_IRWXG) << 3);
398
0
            break;
399
400
0
          case 'u':
401
0
            mode_op |= ((curr_mode & S_IRWXU) >> 6);
402
0
            mode_op |= ((curr_mode & S_IRWXU) >> 3);
403
0
            mode_op |= (curr_mode & S_IRWXU);
404
0
            break;
405
406
0
          case '\0':
407
            /* Apply the mode and move on */
408
0
            switch (*how) {
409
0
              case '+':
410
0
              case '=':
411
0
                mode |= (mode_op & ~mask);
412
0
                break;
413
414
0
              case '-':
415
0
                mode &= ~(mode_op & ~mask);
416
0
                break;
417
0
            }
418
419
0
            mode_op = 0;
420
0
            if (*who && *(who+1)) {
421
0
              who++;
422
0
              cp = what;
423
0
              continue;
424
0
            }
425
426
0
            cp = NULL;
427
0
            break;
428
429
0
          default:
430
0
            invalid++;
431
0
        }
432
433
0
        if (invalid) {
434
0
          break;
435
0
        }
436
437
0
        if (cp) {
438
0
          cp++;
439
0
        }
440
0
      }
441
0
      break;
442
0
    }
443
444
0
    if (invalid) {
445
0
      pr_response_add_err(R_550, _("'%s': invalid mode"), (char *) cmd->argv[1]);
446
447
0
      pr_cmd_set_errno(cmd, EINVAL);
448
0
      errno = EINVAL;
449
0
      return PR_ERROR(cmd);
450
0
    }
451
0
  }
452
453
0
  res = core_chmod(cmd, dir, mode);
454
0
  if (res < 0) {
455
0
    int xerrno = errno;
456
457
0
    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
458
0
      "error chmod'ing '%s' to %04o: %s", (char *) cmd->argv[0], session.user,
459
0
      pr_uid2str(cmd->tmp_pool, session.uid),
460
0
      pr_gid2str(cmd->tmp_pool, session.gid), dir, (unsigned int) mode,
461
0
      strerror(xerrno));
462
463
0
    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
464
465
0
    pr_cmd_set_errno(cmd, xerrno);
466
0
    errno = xerrno;
467
0
    return PR_ERROR(cmd);
468
0
  }
469
470
0
  pr_response_add(R_200, _("SITE %s command successful"),
471
0
    (char *) cmd->argv[0]);
472
0
  return PR_HANDLED(cmd);
473
0
}
474
475
0
MODRET site_help(cmd_rec *cmd) {
476
0
  register unsigned int i = 0;
477
478
  /* Have to support 'HELP SITE' as well as 'SITE HELP'.  Most clients expect
479
   * the former (it's mentioned in RFC959), whereas the latter is more
480
   * syntactically correct.
481
   */
482
483
0
  if (!dir_check_limits(cmd, session.dir_config, "SITE_HELP", FALSE)) {
484
0
    int xerrno = EACCES;
485
486
0
    pr_log_debug(DEBUG8, "SITE HELP denied by <Limit> configuration");
487
488
    /* Returning 501 is the best we can do.  It would be nicer if RFC959 allowed
489
     * 550 as a possible response.
490
     */
491
0
    pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[0],
492
0
      strerror(xerrno));
493
494
0
    pr_cmd_set_errno(cmd, xerrno);
495
0
    errno = xerrno;
496
0
    return PR_ERROR(cmd);
497
0
  }
498
499
0
  if (cmd->argc == 1 || (cmd->argc == 2 &&
500
0
      ((strcasecmp(cmd->argv[0], "SITE") == 0 &&
501
0
        strcasecmp(cmd->argv[1], "HELP") == 0) ||
502
0
       (strcasecmp(cmd->argv[0], "HELP") == 0 &&
503
0
        strcasecmp(cmd->argv[1], "SITE") == 0)))) {
504
505
0
    for (i = 0; _help[i].cmd; i++) {
506
0
      if (_help[i].implemented) {
507
0
        pr_response_add(i != 0 ? R_DUP : R_214, "%s", _help[i].cmd);
508
509
0
      } else {
510
0
        pr_response_add(i != 0 ? R_DUP : R_214, "%s",
511
0
          pstrcat(cmd->pool, _help[i].cmd, "*", NULL));
512
0
      }
513
0
    }
514
515
0
  } else {
516
0
    char *arg, *cp = NULL;
517
518
0
    arg = cmd->argv[1];
519
0
    for (cp = arg; *cp; cp++) {
520
0
      *cp = toupper((int) *cp);
521
0
    }
522
523
0
    for (i = 0; _help[i].cmd; i++) {
524
0
      if (strcasecmp(arg, _help[i].cmd) == 0) {
525
0
        pr_response_add(R_214, _("Syntax: SITE %s %s"),
526
0
          (char *) cmd->argv[1], _help[i].syntax);
527
0
        return PR_HANDLED(cmd);
528
0
      }
529
0
    }
530
531
0
    pr_response_add_err(R_502, _("Unknown command 'SITE %s'"), cmd->arg);
532
0
    pr_cmd_set_errno(cmd, ENOSYS);
533
0
    errno = ENOSYS;
534
0
    return PR_ERROR(cmd);
535
0
  }
536
537
0
  return PR_HANDLED(cmd);
538
0
}
539
540
/* The site_commands table is local only, and not registered with our
541
 * module.
542
 */
543
544
static cmdtable site_commands[] = {
545
  { CMD, "HELP",  G_NONE,   site_help,  FALSE,  FALSE },
546
  { CMD, "CHGRP", G_NONE,   site_chgrp, TRUE, FALSE },
547
  { CMD, "CHMOD", G_NONE,   site_chmod, TRUE, FALSE },
548
  { 0, NULL }
549
};
550
551
0
modret_t *site_dispatch(cmd_rec *cmd) {
552
0
  register unsigned int i = 0;
553
554
0
  if (!cmd->argc) {
555
0
    pr_response_add_err(R_500, _("'SITE' requires parameters"));
556
557
0
    pr_cmd_set_errno(cmd, EINVAL);
558
0
    errno = EINVAL;
559
0
    return PR_ERROR(cmd);
560
0
  }
561
562
0
  for (i = 0; site_commands[i].command; i++) {
563
0
    if (strcmp(cmd->argv[0], site_commands[i].command) == 0) {
564
0
      if (site_commands[i].requires_auth && cmd_auth_chk &&
565
0
          !cmd_auth_chk(cmd)) {
566
0
        pr_response_send(R_530, _("Please login with USER and PASS"));
567
568
0
        pr_cmd_set_errno(cmd, EPERM);
569
0
        errno = EPERM;
570
0
        return PR_ERROR(cmd);
571
0
      }
572
573
0
      return site_commands[i].handler(cmd);
574
0
    }
575
0
  }
576
577
0
  pr_response_add_err(R_500, _("'SITE %s' not understood"),
578
0
    (char *) cmd->argv[0]);
579
580
0
  pr_cmd_set_errno(cmd, EINVAL);
581
0
  errno = EINVAL;
582
0
  return PR_ERROR(cmd);
583
0
}
584
585
/* Command handlers
586
 */
587
588
0
MODRET site_pre_cmd(cmd_rec *cmd) {
589
0
  if (cmd->argc > 1 &&
590
0
      strcasecmp(cmd->argv[1], "help") == 0) {
591
0
    pr_response_add(R_214,
592
0
      _("The following SITE commands are recognized (* =>'s unimplemented)"));
593
0
  }
594
595
0
  return PR_DECLINED(cmd);
596
0
}
597
598
0
MODRET site_cmd(cmd_rec *cmd) {
599
0
  char *cp = NULL;
600
0
  cmd_rec *tmpcmd = NULL;
601
602
  /* Make a copy of the cmd structure for passing to pr_module_call(). */
603
0
  tmpcmd = pcalloc(cmd->pool, sizeof(cmd_rec));
604
0
  memcpy(tmpcmd, cmd, sizeof(cmd_rec));
605
606
0
  tmpcmd->argc--;
607
0
  tmpcmd->argv++;
608
609
0
  if (tmpcmd->argc) {
610
0
    for (cp = tmpcmd->argv[0]; *cp; cp++) {
611
0
      *cp = toupper((int) *cp);
612
0
    }
613
0
  }
614
615
0
  tmpcmd->notes = cmd->notes;
616
617
0
  return site_dispatch(tmpcmd);
618
0
}
619
620
0
MODRET site_post_cmd(cmd_rec *cmd) {
621
0
  if (cmd->argc > 1 &&
622
0
      strcasecmp(cmd->argv[1], "help") == 0) {
623
0
    pr_response_add(R_214, _("Direct comments to %s"),
624
0
      (cmd->server->ServerAdmin ? cmd->server->ServerAdmin : "ftp-admin"));
625
0
  }
626
627
0
  return PR_DECLINED(cmd);
628
0
}
629
630
/* Initialization routines
631
 */
632
633
0
static int site_init(void) {
634
  /* Add the commands handled by this module to the HELP list. */
635
0
  pr_help_add(C_SITE, _("<sp> string"), TRUE);
636
637
0
  return 0;
638
0
}
639
640
/* Module API tables
641
 */
642
643
static cmdtable site_cmdtab[] = {
644
  { PRE_CMD,  C_SITE, G_NONE, site_pre_cmd,   FALSE,  FALSE },
645
  { CMD,      C_SITE, G_NONE, site_cmd,       FALSE,  FALSE,  CL_MISC },
646
  { POST_CMD, C_SITE, G_NONE, site_post_cmd,  FALSE,  FALSE },
647
  { 0, NULL }
648
};
649
650
module site_module = {
651
  NULL, NULL,
652
653
  /* Module API version */
654
  0x20,
655
656
  /* Module name */
657
  "site",
658
659
  /* Module configuration table */
660
  NULL,
661
662
  /* Module command handler table */
663
  site_cmdtab,
664
665
  /* Module auth handler table */
666
  NULL,
667
668
  /* Module initialization function */
669
  site_init,
670
671
  /* Session initialization function */
672
  NULL
673
};