X-Git-Url: http://andersk.mit.edu/gitweb/openssh.git/blobdiff_plain/42f11eb24fa39e437b4f1e6beb5cc83901aa5bdd..4278ff63eceba288225b85bf82506c680d427821:/serverloop.c diff --git a/serverloop.c b/serverloop.c index a7f8e72b..0b44182c 100644 --- a/serverloop.c +++ b/serverloop.c @@ -11,7 +11,7 @@ * called by a name other than "ssh" or "Secure Shell". * * SSH2 support by Markus Friedl. - * Copyright (c) 2000 Markus Friedl. All rights reserved. + * Copyright (c) 2000, 2001 Markus Friedl. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -35,14 +35,14 @@ */ #include "includes.h" -RCSID("$OpenBSD: serverloop.c,v 1.42 2001/01/21 19:05:55 markus Exp $"); +RCSID("$OpenBSD: serverloop.c,v 1.76 2001/07/17 21:04:58 markus Exp $"); #include "xmalloc.h" #include "packet.h" #include "buffer.h" #include "log.h" #include "servconf.h" -#include "pty.h" +#include "sshpty.h" #include "channels.h" #include "compat.h" #include "ssh1.h" @@ -53,9 +53,14 @@ RCSID("$OpenBSD: serverloop.c,v 1.42 2001/01/21 19:05:55 markus Exp $"); #include "auth-options.h" #include "serverloop.h" #include "misc.h" +#include "kex.h" extern ServerOptions options; +/* XXX */ +extern Kex *xxx_kex; +static Authctxt *xxx_authctxt; + static Buffer stdin_buffer; /* Buffer for stdin data. */ static Buffer stdout_buffer; /* Buffer for stdout data. */ static Buffer stderr_buffer; /* Buffer for stderr data. */ @@ -70,48 +75,31 @@ static long fdout_bytes = 0; /* Number of stdout bytes read from program. */ static int stdin_eof = 0; /* EOF message received from client. */ static int fdout_eof = 0; /* EOF encountered reading from fdout. */ static int fderr_eof = 0; /* EOF encountered readung from fderr. */ +static int fdin_is_tty = 0; /* fdin points to a tty. */ static int connection_in; /* Connection to client (input). */ static int connection_out; /* Connection to client (output). */ -static u_int buffer_high;/* "Soft" max buffer size. */ -static int max_fd; /* Max file descriptor number for select(). */ +static int connection_closed = 0; /* Connection to client closed. */ +static u_int buffer_high; /* "Soft" max buffer size. */ /* * This SIGCHLD kludge is used to detect when the child exits. The server * will exit after that, as soon as forwarded connections have terminated. */ -static pid_t child_pid; /* Pid of the child. */ static volatile int child_terminated; /* The child has terminated. */ -static volatile int child_wait_status; /* Status from wait(). */ -void server_init_dispatch(void); +/* prototypes */ +static void server_init_dispatch(void); -void -sigchld_handler(int sig) -{ - int save_errno = errno; - pid_t wait_pid; +int client_alive_timeouts = 0; - debug("Received SIGCHLD."); - wait_pid = wait((int *) &child_wait_status); - if (wait_pid != -1) { - if (wait_pid != child_pid) - error("Strange, got SIGCHLD and wait returned pid %d but child is %d", - wait_pid, child_pid); - if (WIFEXITED(child_wait_status) || - WIFSIGNALED(child_wait_status)) - child_terminated = 1; - } - signal(SIGCHLD, sigchld_handler); - errno = save_errno; -} -void -sigchld_handler2(int sig) +static void +sigchld_handler(int sig) { int save_errno = errno; debug("Received SIGCHLD."); child_terminated = 1; - signal(SIGCHLD, sigchld_handler2); + mysignal(SIGCHLD, sigchld_handler); errno = save_errno; } @@ -119,8 +107,8 @@ sigchld_handler2(int sig) * Make packets from buffered stderr data, and buffer it for sending * to the client. */ -void -make_packets_from_stderr_data() +static void +make_packets_from_stderr_data(void) { int len; @@ -148,8 +136,8 @@ make_packets_from_stderr_data() * Make packets from buffered stdout data, and buffer it for sending to the * client. */ -void -make_packets_from_stdout_data() +static void +make_packets_from_stdout_data(void) { int len; @@ -163,7 +151,7 @@ make_packets_from_stdout_data() } else { /* Keep the packets at reasonable size. */ if (len > packet_get_maxsize()) - len = packet_get_maxsize(); + len = packet_get_maxsize(); } packet_start(SSH_SMSG_STDOUT_DATA); packet_put_string(buffer_ptr(&stdout_buffer), len); @@ -179,24 +167,38 @@ make_packets_from_stdout_data() * have data or can accept data. Optionally, a maximum time can be specified * for the duration of the wait (0 = infinite). */ -void -wait_until_can_do_something(fd_set * readset, fd_set * writeset, - u_int max_time_milliseconds) +static void +wait_until_can_do_something(fd_set **readsetp, fd_set **writesetp, int *maxfdp, + int *nallocp, u_int max_time_milliseconds) { struct timeval tv, *tvp; int ret; + int client_alive_scheduled = 0; + + /* + * if using client_alive, set the max timeout accordingly, + * and indicate that this particular timeout was for client + * alive by setting the client_alive_scheduled flag. + * + * this could be randomized somewhat to make traffic + * analysis more difficult, but we're not doing it yet. + */ + if (compat20 && + max_time_milliseconds == 0 && options.client_alive_interval) { + client_alive_scheduled = 1; + max_time_milliseconds = options.client_alive_interval * 1000; + } /* When select fails we restart from here. */ retry_select: - /* Initialize select() masks. */ - FD_ZERO(readset); - FD_ZERO(writeset); + /* Allocate and update select() masks for channel descriptors. */ + channel_prepare_select(readsetp, writesetp, maxfdp, nallocp, 0); if (compat20) { /* wrong: bad condition XXX */ if (channel_not_very_much_buffered_data()) - FD_SET(connection_in, readset); + FD_SET(connection_in, *readsetp); } else { /* * Read packets from the client unless we have too much @@ -204,44 +206,38 @@ retry_select: */ if (buffer_len(&stdin_buffer) < buffer_high && channel_not_very_much_buffered_data()) - FD_SET(connection_in, readset); + FD_SET(connection_in, *readsetp); /* * If there is not too much data already buffered going to * the client, try to get some more data from the program. */ if (packet_not_very_much_data_to_write()) { if (!fdout_eof) - FD_SET(fdout, readset); + FD_SET(fdout, *readsetp); if (!fderr_eof) - FD_SET(fderr, readset); + FD_SET(fderr, *readsetp); } /* * If we have buffered data, try to write some of that data * to the program. */ if (fdin != -1 && buffer_len(&stdin_buffer) > 0) - FD_SET(fdin, writeset); + FD_SET(fdin, *writesetp); } - /* Set masks for channel descriptors. */ - channel_prepare_select(readset, writeset); /* * If we have buffered packet data going to the client, mark that * descriptor. */ if (packet_have_data_to_write()) - FD_SET(connection_out, writeset); - - /* Update the maximum descriptor number if appropriate. */ - if (channel_max_fd() > max_fd) - max_fd = channel_max_fd(); + FD_SET(connection_out, *writesetp); /* * If child has terminated and there is enough buffer space to read * from it, then read as much as is available and exit. */ if (child_terminated && packet_not_very_much_data_to_write()) - if (max_time_milliseconds == 0) + if (max_time_milliseconds == 0 || client_alive_scheduled) max_time_milliseconds = 100; if (max_time_milliseconds == 0) @@ -252,24 +248,48 @@ retry_select: tvp = &tv; } if (tvp!=NULL) - debug2("tvp!=NULL kid %d mili %d", child_terminated, max_time_milliseconds); + debug3("tvp!=NULL kid %d mili %d", child_terminated, max_time_milliseconds); /* Wait for something to happen, or the timeout to expire. */ - ret = select(max_fd + 1, readset, writeset, NULL, tvp); + ret = select((*maxfdp)+1, *readsetp, *writesetp, NULL, tvp); - if (ret < 0) { + if (ret == -1) { if (errno != EINTR) error("select: %.100s", strerror(errno)); else goto retry_select; } + if (ret == 0 && client_alive_scheduled) { + /* timeout, check to see how many we have had */ + client_alive_timeouts++; + + if (client_alive_timeouts > options.client_alive_count_max ) { + packet_disconnect( + "Timeout, your session not responding."); + } else { + /* + * send a bogus channel request with "wantreply" + * we should get back a failure + */ + int id; + + id = channel_find_open(); + if (id != -1) { + channel_request_start(id, + "keepalive@openssh.com", 1); + packet_send(); + } else + packet_disconnect( + "No open channels after timeout!"); + } + } } /* * Processes input from the client and the program. Input data is stored * in buffers and processed later. */ -void +static void process_input(fd_set * readset) { int len; @@ -280,6 +300,9 @@ process_input(fd_set * readset) len = read(connection_in, buf, sizeof(buf)); if (len == 0) { verbose("Connection closed by remote host."); + connection_closed = 1; + if (compat20) + return; fatal_cleanup(); } else if (len < 0) { if (errno != EINTR && errno != EAGAIN) { @@ -322,9 +345,10 @@ process_input(fd_set * readset) /* * Sends data from internal buffers to client program stdin. */ -void +static void process_output(fd_set * writeset) { + struct termios tio; int len; /* Write buffered data to program stdin. */ @@ -344,7 +368,17 @@ process_output(fd_set * writeset) #endif fdin = -1; } else { - /* Successful write. Consume the data from the buffer. */ + /* Successful write. */ + if (fdin_is_tty && tcgetattr(fdin, &tio) == 0 && + !(tio.c_lflag & ECHO) && (tio.c_lflag & ICANON)) { + /* + * Simulate echo to reduce the impact of + * traffic analysis + */ + packet_send_ignore(len); + packet_send(); + } + /* Consume the data from the buffer. */ buffer_consume(&stdin_buffer, len); /* Update the count of bytes written to the program. */ stdin_bytes += len; @@ -359,8 +393,8 @@ process_output(fd_set * writeset) * Wait until all buffered output has been sent to the client. * This is used when the program terminates. */ -void -drain_output() +static void +drain_output(void) { /* Send any buffered stdout data to the client. */ if (buffer_len(&stdout_buffer) > 0) { @@ -384,10 +418,10 @@ drain_output() packet_write_wait(); } -void -process_buffered_input_packets() +static void +process_buffered_input_packets(void) { - dispatch_run(DISPATCH_NONBLOCK, NULL, NULL); + dispatch_run(DISPATCH_NONBLOCK, NULL, compat20 ? xxx_kex : NULL); } /* @@ -400,7 +434,8 @@ process_buffered_input_packets() void server_loop(pid_t pid, int fdin_arg, int fdout_arg, int fderr_arg) { - fd_set readset, writeset; + fd_set *readset = NULL, *writeset = NULL; + int max_fd = 0, nalloc = 0; int wait_status; /* Status returned by wait(). */ pid_t wait_pid; /* pid returned by wait(). */ int waiting_termination = 0; /* Have displayed waiting close message. */ @@ -412,10 +447,8 @@ server_loop(pid_t pid, int fdin_arg, int fdout_arg, int fderr_arg) debug("Entering interactive session."); /* Initialize the SIGCHLD kludge. */ - child_pid = pid; child_terminated = 0; - signal(SIGCHLD, sigchld_handler); - signal(SIGPIPE, SIG_IGN); + mysignal(SIGCHLD, sigchld_handler); /* Initialize our global variables. */ fdin = fdin_arg; @@ -429,6 +462,9 @@ server_loop(pid_t pid, int fdin_arg, int fdout_arg, int fderr_arg) if (fderr != -1) set_nonblock(fderr); + if (!(datafellows & SSH_BUG_IGNOREMSG) && isatty(fdin)) + fdin_is_tty = 1; + connection_in = packet_get_connection_in(); connection_out = packet_get_connection_out(); @@ -440,16 +476,14 @@ server_loop(pid_t pid, int fdin_arg, int fdout_arg, int fderr_arg) else buffer_high = 64 * 1024; +#if 0 /* Initialize max_fd to the maximum of the known file descriptors. */ - max_fd = fdin; - if (fdout > max_fd) - max_fd = fdout; - if (fderr != -1 && fderr > max_fd) - max_fd = fderr; - if (connection_in > max_fd) - max_fd = connection_in; - if (connection_out > max_fd) - max_fd = connection_out; + max_fd = MAX(connection_in, connection_out); + max_fd = MAX(max_fd, fdin); + max_fd = MAX(max_fd, fdout); + if (fderr != -1) + max_fd = MAX(max_fd, fderr); +#endif /* Initialize Initialize buffers. */ buffer_init(&stdin_buffer); @@ -535,19 +569,28 @@ server_loop(pid_t pid, int fdin_arg, int fdout_arg, int fderr_arg) xfree(cp); } } + max_fd = MAX(connection_in, connection_out); + max_fd = MAX(max_fd, fdin); + max_fd = MAX(max_fd, fdout); + max_fd = MAX(max_fd, fderr); + /* Sleep in select() until we can do something. */ - wait_until_can_do_something(&readset, &writeset, - max_time_milliseconds); + wait_until_can_do_something(&readset, &writeset, &max_fd, + &nalloc, max_time_milliseconds); /* Process any channel events. */ - channel_after_select(&readset, &writeset); + channel_after_select(readset, writeset); /* Process input from the client and from program stdout/stderr. */ - process_input(&readset); + process_input(readset); /* Process output to the client and to program stdin. */ - process_output(&writeset); + process_output(writeset); } + if (readset) + xfree(readset); + if (writeset) + xfree(writeset); /* Cleanup and termination code. */ @@ -575,30 +618,17 @@ server_loop(pid_t pid, int fdin_arg, int fdout_arg, int fderr_arg) close(fdin); fdin = -1; - /* Stop listening for channels; this removes unix domain sockets. */ - channel_stop_listening(); - - /* Wait for the child to exit. Get its exit status. */ - wait_pid = wait(&wait_status); - if (wait_pid == -1) { - /* - * It is possible that the wait was handled by SIGCHLD - * handler. This may result in either: this call - * returning with EINTR, or: this call returning ECHILD. - */ - if (child_terminated) - wait_status = child_wait_status; - else - packet_disconnect("wait: %.100s", strerror(errno)); - } else { - /* Check if it matches the process we forked. */ - if (wait_pid != pid) - error("Strange, wait returned pid %d, expected %d", - wait_pid, pid); - } + channel_free_all(); /* We no longer want our SIGCHLD handler to be called. */ - signal(SIGCHLD, SIG_DFL); + mysignal(SIGCHLD, SIG_DFL); + + wait_pid = waitpid(-1, &wait_status, child_terminated ? WNOHANG : 0); + if (wait_pid == -1) + packet_disconnect("wait: %.100s", strerror(errno)); + else if (wait_pid != pid) + error("Strange, wait returned pid %d, expected %d", + wait_pid, pid); /* Check if it exited normally. */ if (WIFEXITED(wait_status)) { @@ -636,52 +666,87 @@ server_loop(pid_t pid, int fdin_arg, int fdout_arg, int fderr_arg) } void -server_loop2(void) +server_loop2(Authctxt *authctxt) { - fd_set readset, writeset; - int had_channel = 0; - int status; + fd_set *readset = NULL, *writeset = NULL; + int rekeying = 0, max_fd, status, nalloc = 0; pid_t pid; debug("Entering interactive session for SSH2."); - signal(SIGCHLD, sigchld_handler2); - signal(SIGPIPE, SIG_IGN); + mysignal(SIGCHLD, sigchld_handler); child_terminated = 0; connection_in = packet_get_connection_in(); connection_out = packet_get_connection_out(); - max_fd = connection_in; - if (connection_out > max_fd) - max_fd = connection_out; + + max_fd = MAX(connection_in, connection_out); + xxx_authctxt = authctxt; + server_init_dispatch(); for (;;) { process_buffered_input_packets(); - if (!had_channel && channel_still_open()) - had_channel = 1; - if (had_channel && !channel_still_open()) { - debug("!channel_still_open."); - break; - } - if (packet_not_very_much_data_to_write()) + + rekeying = (xxx_kex != NULL && !xxx_kex->done); + + if (!rekeying && packet_not_very_much_data_to_write()) channel_output_poll(); - wait_until_can_do_something(&readset, &writeset, 0); + wait_until_can_do_something(&readset, &writeset, &max_fd, + &nalloc, 0); if (child_terminated) { while ((pid = waitpid(-1, &status, WNOHANG)) > 0) session_close_by_pid(pid, status); child_terminated = 0; } - channel_after_select(&readset, &writeset); - process_input(&readset); - process_output(&writeset); + if (!rekeying) + channel_after_select(readset, writeset); + process_input(readset); + if (connection_closed) + break; + process_output(writeset); } - signal(SIGCHLD, SIG_DFL); + if (readset) + xfree(readset); + if (writeset) + xfree(writeset); + + mysignal(SIGCHLD, SIG_DFL); + while ((pid = waitpid(-1, &status, WNOHANG)) > 0) session_close_by_pid(pid, status); + /* + * there is a race between channel_free_all() killing children and + * children dying before kill() + */ + channel_detach_all(); channel_stop_listening(); + + while (session_have_children()) { + pid = waitpid(-1, &status, 0); + if (pid > 0) + session_close_by_pid(pid, status); + else { + error("waitpid returned %d: %s", pid, strerror(errno)); + break; + } + } + channel_free_all(); } -void +static void +server_input_channel_failure(int type, int plen, void *ctxt) +{ + debug("Got CHANNEL_FAILURE for keepalive"); + /* + * reset timeout, since we got a sane answer from the client. + * even if this was generated by something other than + * the bogus CHANNEL_REQUEST we send for keepalives. + */ + client_alive_timeouts = 0; +} + + +static void server_input_stdin_data(int type, int plen, void *ctxt) { char *data; @@ -698,7 +763,7 @@ server_input_stdin_data(int type, int plen, void *ctxt) xfree(data); } -void +static void server_input_eof(int type, int plen, void *ctxt) { /* @@ -711,7 +776,7 @@ server_input_eof(int type, int plen, void *ctxt) stdin_eof = 1; } -void +static void server_input_window_size(int type, int plen, void *ctxt) { int row = packet_get_int(); @@ -725,10 +790,11 @@ server_input_window_size(int type, int plen, void *ctxt) pty_change_window_size(fdin, row, col, xpixel, ypixel); } -Channel * +static Channel * server_request_direct_tcpip(char *ctype) { - int sock, newch; + Channel *c; + int sock; char *target, *originator; int target_port, originator_port; @@ -742,26 +808,25 @@ server_request_direct_tcpip(char *ctype) originator, originator_port, target, target_port); /* XXX check permission */ - if (no_port_forwarding_flag || !options.allow_tcp_forwarding) { - xfree(target); - xfree(originator); - return NULL; - } sock = channel_connect_to(target, target_port); xfree(target); xfree(originator); if (sock < 0) return NULL; - newch = channel_new(ctype, SSH_CHANNEL_CONNECTING, + c = channel_new(ctype, SSH_CHANNEL_CONNECTING, sock, sock, -1, CHAN_TCP_WINDOW_DEFAULT, CHAN_TCP_PACKET_DEFAULT, 0, xstrdup("direct-tcpip"), 1); - return (newch >= 0) ? channel_lookup(newch) : NULL; + if (c == NULL) { + error("server_request_direct_tcpip: channel_new failed"); + close(sock); + } + return c; } -Channel * +static Channel * server_request_session(char *ctype) { - int newch; + Channel *c; debug("input_session_request"); packet_done(); @@ -771,22 +836,25 @@ server_request_session(char *ctype) * SSH_CHANNEL_LARVAL. Additionally, a callback for handling all * CHANNEL_REQUEST messages is registered. */ - newch = channel_new(ctype, SSH_CHANNEL_LARVAL, - -1, -1, -1, 0, CHAN_SES_PACKET_DEFAULT, + c = channel_new(ctype, SSH_CHANNEL_LARVAL, + -1, -1, -1, /*window size*/0, CHAN_SES_PACKET_DEFAULT, 0, xstrdup("server-session"), 1); - if (session_open(newch) == 1) { - channel_register_callback(newch, SSH2_MSG_CHANNEL_REQUEST, - session_input_channel_req, (void *)0); - channel_register_cleanup(newch, session_close_by_channel); - return channel_lookup(newch); - } else { - debug("session open failed, free channel %d", newch); - channel_free(newch); + if (c == NULL) { + error("server_request_session: channel_new failed"); + return NULL; + } + if (session_open(xxx_authctxt, c->self) != 1) { + debug("session open failed, free channel %d", c->self); + channel_free(c); + return NULL; } - return NULL; + channel_register_callback(c->self, SSH2_MSG_CHANNEL_REQUEST, + session_input_channel_req, (void *)0); + channel_register_cleanup(c->self, session_close_by_channel); + return c; } -void +static void server_input_channel_open(int type, int plen, void *ctxt) { Channel *c = NULL; @@ -814,26 +882,29 @@ server_input_channel_open(int type, int plen, void *ctxt) c->remote_id = rchan; c->remote_window = rwindow; c->remote_maxpacket = rmaxpack; - - packet_start(SSH2_MSG_CHANNEL_OPEN_CONFIRMATION); - packet_put_int(c->remote_id); - packet_put_int(c->self); - packet_put_int(c->local_window); - packet_put_int(c->local_maxpacket); - packet_send(); + if (c->type != SSH_CHANNEL_CONNECTING) { + packet_start(SSH2_MSG_CHANNEL_OPEN_CONFIRMATION); + packet_put_int(c->remote_id); + packet_put_int(c->self); + packet_put_int(c->local_window); + packet_put_int(c->local_maxpacket); + packet_send(); + } } else { debug("server_input_channel_open: failure %s", ctype); packet_start(SSH2_MSG_CHANNEL_OPEN_FAILURE); packet_put_int(rchan); packet_put_int(SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED); - packet_put_cstring("bla bla"); - packet_put_cstring(""); + if (!(datafellows & SSH_BUG_OPENFAILURE)) { + packet_put_cstring("open failed"); + packet_put_cstring(""); + } packet_send(); } xfree(ctype); } -void +static void server_input_global_request(int type, int plen, void *ctxt) { char *rtype; @@ -843,7 +914,8 @@ server_input_global_request(int type, int plen, void *ctxt) rtype = packet_get_string(NULL); want_reply = packet_get_char(); debug("server_input_global_request: rtype %s want_reply %d", rtype, want_reply); - + + /* -R style forwarding */ if (strcmp(rtype, "tcpip-forward") == 0) { struct passwd *pw; char *listen_address; @@ -865,12 +937,11 @@ server_input_global_request(int type, int plen, void *ctxt) packet_send_debug("Server has disabled port forwarding."); } else { /* Start listening on the port */ - channel_request_forwarding( + success = channel_request_forwarding( listen_address, listen_port, /*unspec host_to_connect*/ "", /*unspec port_to_connect*/ 0, options.gateway_ports, /*remote*/ 1); - success = 1; } xfree(listen_address); } @@ -883,8 +954,8 @@ server_input_global_request(int type, int plen, void *ctxt) xfree(rtype); } -void -server_init_dispatch_20() +static void +server_init_dispatch_20(void) { debug("server_init_dispatch_20"); dispatch_init(&dispatch_protocol_error); @@ -898,9 +969,13 @@ server_init_dispatch_20() dispatch_set(SSH2_MSG_CHANNEL_REQUEST, &channel_input_channel_request); dispatch_set(SSH2_MSG_CHANNEL_WINDOW_ADJUST, &channel_input_window_adjust); dispatch_set(SSH2_MSG_GLOBAL_REQUEST, &server_input_global_request); + /* client_alive */ + dispatch_set(SSH2_MSG_CHANNEL_FAILURE, &server_input_channel_failure); + /* rekeying */ + dispatch_set(SSH2_MSG_KEXINIT, &kex_input_kexinit); } -void -server_init_dispatch_13() +static void +server_init_dispatch_13(void) { debug("server_init_dispatch_13"); dispatch_init(NULL); @@ -914,16 +989,16 @@ server_init_dispatch_13() dispatch_set(SSH_MSG_CHANNEL_OPEN_FAILURE, &channel_input_open_failure); dispatch_set(SSH_MSG_PORT_OPEN, &channel_input_port_open); } -void -server_init_dispatch_15() +static void +server_init_dispatch_15(void) { server_init_dispatch_13(); debug("server_init_dispatch_15"); dispatch_set(SSH_MSG_CHANNEL_CLOSE, &channel_input_ieof); dispatch_set(SSH_MSG_CHANNEL_CLOSE_CONFIRMATION, &channel_input_oclose); } -void -server_init_dispatch() +static void +server_init_dispatch(void) { if (compat20) server_init_dispatch_20();