16d5a238ae2e9d7bbe490bb39a512ed597cece81
[oftpd.git] / src / oftpd.c
1 /*
2  * $Id$
3  */
4
5 #include <config.h>
6 #include <stdio.h>
7 #include <unistd.h>
8 #include <sys/types.h>
9 #include <sys/stat.h>
10 #include <fcntl.h>
11 #include <string.h>
12 #include <errno.h>
13 #include <signal.h>
14 #include <pwd.h>
15 #include <syslog.h>
16 #include <pthread.h>
17 #include <stdlib.h>
18 #include <sys/socket.h>
19 #include <sys/un.h>
20
21 #include "oftpd.h"
22 #include "ftp_listener.h"
23 #include "error.h"
24
25 /* put our executable name here where everybody can see it */
26 static const char *exe_name = "oftpd";
27 static int log_facility;
28
29 int pasv_port_low = 1024;
30 int pasv_port_high = MAX_PORT;
31
32 /* This is used by the reopen_syslog_hack. */
33 static int my_syslog_fd = -1;
34
35 static void daemonize();
36 static void print_usage(const char *error);
37 static void init_syslog_hack (void);
38
39 int main(int argc, char *argv[])
40 {
41     int i;
42     
43     long num;
44     char *endptr;
45     int port;
46     int max_clients;
47
48     char *user_ptr;
49     char *dir_ptr;
50     char *address;
51     
52     char temp_buf[256];
53
54     struct passwd *user_info;
55     error_t err;
56
57     ftp_listener_t ftp_listener;
58
59     int detach;
60
61     sigset_t term_signal;
62     int sig;
63
64     /* grab our executable name */
65     if (argc > 0) {
66         exe_name = argv[0];
67     }
68
69     /* verify we're running as root */
70     if (geteuid() != 0) {
71         fprintf(stderr, "%s: program needs root permission to run\n", exe_name);
72         exit(1);
73     }
74
75     /* default command-line arguments */
76     port = FTP_PORT;
77     user_ptr = NULL;
78     dir_ptr = NULL;
79     address = FTP_ADDRESS;
80     max_clients = MAX_CLIENTS;
81     detach = 1;
82     log_facility = LOG_FTP;
83
84     /* check our command-line arguments */
85     /* we're stubbornly refusing to use getopt(), because we can */
86     /* :) */ 
87     for (i=1; i<argc; i++) {
88         
89         /* flags/optional arguments */
90         if (argv[i][0] == '-') {
91             if (strcmp(argv[i], "-p") == 0 
92                 || strcmp(argv[i], "--port") == 0) {
93                 if (++i >= argc) {
94                     print_usage("missing port number");
95                     exit(1);
96                 }
97                 num = strtol(argv[i], &endptr, 0);
98                 if ((num < MIN_PORT) || (num > MAX_PORT) || (*endptr != '\0')) {
99
100                     snprintf(temp_buf, sizeof(temp_buf), 
101                              "port must be a number between %d and %d",
102                              MIN_PORT, MAX_PORT);
103                     print_usage(temp_buf);
104
105                     exit(1);
106                 }
107                 port = num;
108             }
109             else if (strcmp(argv[i], "-r") == 0 
110                      || strcmp(argv[i], "--pasv-range") == 0) {
111                 i += 2;  
112                 if (i >= argc) {
113                     print_usage("invalid passive port range");
114                     exit(1);
115                 }
116                 num = strtol(argv[i-1], &endptr, 0);
117                 if ((num < 1024) || (num > MAX_PORT) || (*endptr != '\0')) {
118
119                     snprintf(temp_buf, sizeof(temp_buf), 
120                              "low passive port must be a number between 1024 and %d",
121                              MAX_PORT);
122                     print_usage(temp_buf);
123
124                     exit(1);
125                 }
126                 pasv_port_low = num;
127                 num = strtol(argv[i], &endptr, 0);
128                 if ((num < pasv_port_low) || (num > MAX_PORT) || (*endptr != '\0')) {
129
130                     snprintf(temp_buf, sizeof(temp_buf), 
131                              "high passive port must be a number between %d and %d",
132                              pasv_port_low, MAX_PORT);
133                     print_usage(temp_buf);
134
135                     exit(1);
136                 }
137                 pasv_port_high = num;
138             } else if (strcmp(argv[i], "-h") == 0
139                        || strcmp(argv[i], "--help") == 0) {
140                 print_usage(NULL);
141                 exit(0);
142             } else if (strcmp(argv[i], "-i") == 0
143                        || strcmp(argv[i], "--interface") == 0) {
144                 if (++i >= argc) {
145                     print_usage("missing interface");
146                     exit(1);
147                 }
148                 address = argv[i];
149             } else if (strcmp(argv[i], "-m") == 0
150                        || strcmp(argv[i], "--max-clients") == 0) {
151                 if (++i >= argc) {
152                     print_usage("missing number of max clients");
153                     exit(1);
154                 }
155                 num = strtol(argv[i], &endptr, 0);
156                 if ((num < MIN_NUM_CLIENTS) || (num > MAX_NUM_CLIENTS) 
157                     || (*endptr != '\0')) {
158
159                     snprintf(temp_buf, sizeof(temp_buf),
160                         "max clients must be a number between %d and %d",
161                         MIN_NUM_CLIENTS, MAX_NUM_CLIENTS);
162                     print_usage(temp_buf);
163
164                     exit(1);
165                 }
166                 max_clients = num;
167             } else if (strcmp(argv[i], "-N") == 0 
168                        || strcmp(argv[i], "--nodetach") == 0) {
169                 detach = 0;
170             } else if (strcmp(argv[i], "-l") == 0
171                        || strcmp(argv[i], "--local") == 0) {
172                 if (++i >= argc) {
173                     print_usage("missing number for local facility logging");
174                     exit(1);
175                 }
176                 switch (argv[i][0]) {
177                     case '0': 
178                         log_facility = LOG_LOCAL0;
179                         break;
180                     case '1': 
181                         log_facility = LOG_LOCAL1;
182                         break;
183                     case '2': 
184                         log_facility = LOG_LOCAL2;
185                         break;
186                     case '3': 
187                         log_facility = LOG_LOCAL3;
188                         break;
189                     case '4': 
190                         log_facility = LOG_LOCAL4;
191                         break;
192                     case '5': 
193                         log_facility = LOG_LOCAL5;
194                         break;
195                     case '6': 
196                         log_facility = LOG_LOCAL6;
197                         break;
198                     case '7': 
199                         log_facility = LOG_LOCAL7;
200                         break;
201                 }
202             } else {
203                 print_usage("unknown option");
204                 exit(1);
205             }
206
207         /* required parameters */
208         } else {
209             if (user_ptr == NULL) {
210                 user_ptr = argv[i];
211             } else if (dir_ptr == NULL) {
212                 dir_ptr = argv[i];
213             } else {
214                 print_usage("too many arguments on the command line");
215                 exit(1);
216             }
217         }
218     }
219     if ((user_ptr == NULL) || (dir_ptr == NULL)) {
220         print_usage("missing user and/or directory name");
221         exit(1);
222     }
223
224     user_info = getpwnam(user_ptr);
225     if (user_info == NULL) {
226         fprintf(stderr, "%s: invalid user name\n", exe_name);
227         exit(1);
228     }
229
230     /* become a daemon */
231     if (detach) {
232         daemonize();
233     }
234
235     /* avoid SIGPIPE on socket activity */
236     signal(SIGPIPE, SIG_IGN);         
237
238     /* log the start time */
239     openlog(NULL, LOG_NDELAY, log_facility);
240     syslog(LOG_INFO,"Starting, version %s, as PID %d", VERSION, getpid());
241     init_syslog_hack ();
242
243     /* change to root directory */
244     if (chroot(dir_ptr) != 0) {
245         syslog(LOG_ERR, "error with root directory; %s\n", exe_name, 
246           strerror(errno));
247         exit(1);
248     }
249     if (chdir("/") != 0) {
250         syslog(LOG_ERR, "error changing directory; %s\n", strerror(errno));
251         exit(1);
252     }
253
254     /* create our main listener */
255     if (!ftp_listener_init(&ftp_listener, 
256                            address,
257                            port,
258                            max_clients,
259                            INACTIVITY_TIMEOUT, 
260                            &err)) 
261     {
262         syslog(LOG_ERR, "error initializing FTP listener; %s",
263           error_get_desc(&err));
264         exit(1);
265     }
266
267     /* set user to be as inoffensive as possible */
268     if (setgid(user_info->pw_gid) != 0) {
269         syslog(LOG_ERR, "error changing group; %s", strerror(errno));
270         exit(1);
271     }
272     if (setuid(user_info->pw_uid) != 0) {
273         syslog(LOG_ERR, "error changing group; %s", strerror(errno));
274         exit(1);
275     }
276
277     /* start our listener */
278     if (ftp_listener_start(&ftp_listener, &err) == 0) {
279         syslog(LOG_ERR, "error starting FTP service; %s", error_get_desc(&err));
280         exit(1);
281     }
282
283     /* wait for a SIGTERM and exit gracefully */
284     sigemptyset(&term_signal);
285     sigaddset(&term_signal, SIGTERM);
286     sigaddset(&term_signal, SIGINT);
287     pthread_sigmask(SIG_BLOCK, &term_signal, NULL);
288     sigwait(&term_signal, &sig);
289     if (sig == SIGTERM) {
290         syslog(LOG_INFO, "SIGTERM received, shutting down");
291     } else { 
292         syslog(LOG_INFO, "SIGINT received, shutting down");
293     }
294     ftp_listener_stop(&ftp_listener);
295     syslog(LOG_INFO, "all connections finished, FTP server exiting");
296     exit(0);
297 }
298
299 static void print_usage(const char *error)
300 {
301     if (error != NULL) {
302         fprintf(stderr, "%s: %s\n", exe_name, error);
303     }
304     fprintf(stderr, 
305            " Syntax: %s [ options... ] user_name root_directory\n", exe_name);
306     fprintf(stderr, 
307            " Options:\n"
308            " -p, --port <num>\n"
309            "     Set the port to listen on (Default: %d)\n"
310            " -i, --interface <IP Address>\n"
311            "     Set the interface to listen on (Default: all)\n"
312            " -m, --max-clients <num>\n"
313            "     Set the number of clients allowed at one time (Default: %d)\n"
314            "-l, --local <local-logging>\n"
315            "     Use LOCAL facility for syslog, local-logging is 0 to 7\n"
316            " -N, --nodetach\n"
317            "     Do not detach from TTY and become a daemon\n",
318            DEFAULT_FTP_PORT, MAX_CLIENTS);
319 }
320
321 static void daemonize()
322 {
323     int fork_ret;
324     int max_fd;
325     int null_fd;
326     int fd;
327
328     null_fd = open("/dev/null", O_RDWR);
329     if (null_fd == -1) {
330         fprintf(stderr, "%s: error opening null output device; %s\n", exe_name, 
331           strerror(errno));
332         exit(1);
333     }
334
335     max_fd = sysconf(_SC_OPEN_MAX);
336     if (max_fd == -1) {
337         fprintf(stderr, "%s: error getting maximum open file; %s\n", exe_name, 
338           strerror(errno));
339         exit(1);
340     }
341
342
343     fork_ret = fork();
344     if (fork_ret == -1) {
345         fprintf(stderr, "%s: error forking; %s\n", exe_name, strerror(errno));
346         exit(1);
347     }
348     if (fork_ret != 0) {
349         exit(0);
350     }
351     if (setsid() == -1) {
352         fprintf(stderr, "%s: error creating process group; %s\n", exe_name, 
353           strerror(errno));
354         exit(1);
355     }
356     fork_ret = fork();
357     if (fork_ret == -1) {
358         fprintf(stderr, "%s: error forking; %s\n", exe_name, strerror(errno));
359         exit(1);
360     }
361     if (fork_ret != 0) {
362         exit(0);
363     }
364     if (dup2(null_fd, 0) == -1) {
365         syslog(LOG_ERR, "error setting input to null; %s", 
366           strerror(errno));
367         exit(1);
368     }
369     if (dup2(null_fd, 1) == -1) {
370         syslog(LOG_ERR, "error setting output to null; %s", 
371           strerror(errno));
372         exit(1);
373     }
374     if (dup2(null_fd, 2) == -1) {
375         syslog(LOG_ERR, "error setting error output to null; %s", 
376           strerror(errno));
377         exit(1);
378     }
379     for (fd=3; fd<max_fd; fd++) {
380         close(fd);
381     }
382 }
383
384
385 /* Figure out the syslog fd and store it away. */
386 static void init_syslog_hack ()
387 {
388     struct sockaddr_un addr;
389     socklen_t len;
390     int fd;
391
392     my_syslog_fd = -1;
393     /* We know that stdin, stdout and stderr are connected to
394      * /dev/null, so we can start with 3.  FIXME: We should use a
395      * POSIX thing to figure out the highest possible fd. */
396     for (fd=3; fd < 1024; fd++) {
397         len = sizeof addr;
398         if (!getsockname (fd, (struct sockaddr*)&addr, &len)) {
399             if (addr.sun_family == PF_LOCAL) {
400                 /* Found a unix domain socket.  This is what we were
401                  * looking for. */
402                 my_syslog_fd = fd;
403                 syslog(LOG_INFO,"Found my syslog file descriptor (%d).", fd);
404                 return;
405             }
406         }
407     }
408 }
409
410
411 void reopen_syslog_hack (int fd)
412 {
413     if (fd < 0)
414         return;
415     if (my_syslog_fd == -1)
416         return; /* not initialized, so we can't use the hack. */
417     if (fd == my_syslog_fd) {
418         ; /* We lost the fd in the meantime, otherwise the next
419            * file descriptor should not get this fd. */
420     }
421     else {
422       struct sockaddr_un addr;
423       socklen_t len = sizeof addr;
424
425       if (!getsockname (my_syslog_fd, (struct sockaddr*)&addr, &len)) 
426         if (addr.sun_family == PF_LOCAL) 
427           return;   /* Okay, this is still a unix domain socket, so
428                        everything seems to be fine. */
429     }
430
431     my_syslog_fd = -1;
432     /* fixme: If we would employ a syslog() wrapper and use a lock
433      * there we could easily avoid to lose messages.  But OTOH, this
434      * is just a glibc fix, so we will live with that.  Anyway, we can
435      * detect a lost syslog file descriptor only after an open or
436      * socket call - so it does not matter at all. */
437     openlog(NULL, LOG_NDELAY, log_facility);
438     syslog(LOG_INFO,"Redone the openlog call to fix a glibc bug."
439                     "Some messages might have been lost.");
440     init_syslog_hack ();
441 }