[BACK]Return to ssh-keyscan.c CVS log [TXT][DIR] Up to [local] / src / usr.bin / ssh

Diff for /src/usr.bin/ssh/ssh-keyscan.c between version 1.22.4.3 and 1.23

version 1.22.4.3, 2002/03/09 00:20:45 version 1.23, 2001/06/05 05:05:39
Line 14 
Line 14 
   
 #include <openssl/bn.h>  #include <openssl/bn.h>
   
 #include <setjmp.h>  
 #include "xmalloc.h"  #include "xmalloc.h"
 #include "ssh.h"  #include "ssh.h"
 #include "ssh1.h"  #include "ssh1.h"
 #include "key.h"  #include "key.h"
 #include "kex.h"  
 #include "compat.h"  
 #include "myproposal.h"  
 #include "packet.h"  
 #include "dispatch.h"  
 #include "buffer.h"  #include "buffer.h"
 #include "bufaux.h"  #include "bufaux.h"
 #include "log.h"  #include "log.h"
 #include "atomicio.h"  #include "atomicio.h"
 #include "misc.h"  
   
 /* Flag indicating whether IPv4 or IPv6.  This can be set on the command line.  static int argno = 1;           /* Number of argument currently being parsed */
    Default value is AF_UNSPEC means both IPv4 and IPv6. */  
 int IPv4or6 = AF_UNSPEC;  
   
 int ssh_port = SSH_DEFAULT_PORT;  int family = AF_UNSPEC;         /* IPv4, IPv6 or both */
   
 #define KT_RSA1 1  
 #define KT_DSA  2  
 #define KT_RSA  4  
   
 int get_keytypes = KT_RSA1;     /* Get only RSA1 keys by default */  
   
 #define MAXMAXFD 256  #define MAXMAXFD 256
   
 /* The number of seconds after which to give up on a TCP connection */  /* The number of seconds after which to give up on a TCP connection */
Line 54 
Line 39 
 fd_set *read_wait;  fd_set *read_wait;
 size_t read_wait_size;  size_t read_wait_size;
 int ncon;  int ncon;
 int nonfatal_fatal = 0;  
 jmp_buf kexjmp;  
 Key *kexjmp_key;  
   
 /*  /*
  * Keep a connection structure for each file descriptor.  The state   * Keep a connection structure for each file descriptor.  The state
Line 72 
Line 54 
         int c_plen;             /* Packet length field for ssh packet */          int c_plen;             /* Packet length field for ssh packet */
         int c_len;              /* Total bytes which must be read. */          int c_len;              /* Total bytes which must be read. */
         int c_off;              /* Length of data read so far. */          int c_off;              /* Length of data read so far. */
         int c_keytype;          /* Only one of KT_RSA1, KT_DSA, or KT_RSA */  
         char *c_namebase;       /* Address to free for c_name and c_namelist */          char *c_namebase;       /* Address to free for c_name and c_namelist */
         char *c_name;           /* Hostname of connection for errors */          char *c_name;           /* Hostname of connection for errors */
         char *c_namelist;       /* Pointer to other possible addresses */          char *c_namelist;       /* Pointer to other possible addresses */
         char *c_output_name;    /* Hostname of connection for output */          char *c_output_name;    /* Hostname of connection for output */
         char *c_data;           /* Data read from this fd */          char *c_data;           /* Data read from this fd */
         Kex *c_kex;             /* The key-exchange struct for ssh2 */  
         struct timeval c_tv;    /* Time at which connection gets aborted */          struct timeval c_tv;    /* Time at which connection gets aborted */
         TAILQ_ENTRY(Connection) c_link; /* List of connections in timeout order. */          TAILQ_ENTRY(Connection) c_link; /* List of connections in timeout order. */
 } con;  } con;
Line 102 
Line 82 
         void (*errfun) (const char *,...);          void (*errfun) (const char *,...);
 } Linebuf;  } Linebuf;
   
 static Linebuf *  Linebuf *
 Linebuf_alloc(const char *filename, void (*errfun) (const char *,...))  Linebuf_alloc(const char *filename, void (*errfun) (const char *,...))
 {  {
         Linebuf *lb;          Linebuf *lb;
Line 136 
Line 116 
         return (lb);          return (lb);
 }  }
   
 static void  void
 Linebuf_free(Linebuf * lb)  Linebuf_free(Linebuf * lb)
 {  {
         fclose(lb->stream);          fclose(lb->stream);
Line 144 
Line 124 
         xfree(lb);          xfree(lb);
 }  }
   
 #if 0  void
 static void  
 Linebuf_restart(Linebuf * lb)  Linebuf_restart(Linebuf * lb)
 {  {
         clearerr(lb->stream);          clearerr(lb->stream);
Line 153 
Line 132 
         lb->lineno = 0;          lb->lineno = 0;
 }  }
   
 static int  int
 Linebuf_lineno(Linebuf * lb)  Linebuf_lineno(Linebuf * lb)
 {  {
         return (lb->lineno);          return (lb->lineno);
 }  }
 #endif  
   
 static char *  char *
 Linebuf_getline(Linebuf * lb)  Linebuf_getline(Linebuf * lb)
 {  {
         int n = 0;          int n = 0;
Line 197 
Line 175 
         }          }
 }  }
   
 static int  int
 fdlim_get(int hard)  fdlim_get(int hard)
 {  {
         struct rlimit rlfd;          struct rlimit rlfd;
Line 210 
Line 188 
                 return hard ? rlfd.rlim_max : rlfd.rlim_cur;                  return hard ? rlfd.rlim_max : rlfd.rlim_cur;
 }  }
   
 static int  int
 fdlim_set(int lim)  fdlim_set(int lim)
 {  {
         struct rlimit rlfd;          struct rlimit rlfd;
Line 229 
Line 207 
  * separators.  This is the same as the 4.4BSD strsep, but different from the   * separators.  This is the same as the 4.4BSD strsep, but different from the
  * one in the GNU libc.   * one in the GNU libc.
  */   */
 static char *  char *
 xstrsep(char **str, const char *delim)  xstrsep(char **str, const char *delim)
 {  {
         char *s, *e;          char *s, *e;
Line 251 
Line 229 
  * Get the next non-null token (like GNU strsep).  Strsep() will return a   * Get the next non-null token (like GNU strsep).  Strsep() will return a
  * null token for two adjacent separators, so we may have to loop.   * null token for two adjacent separators, so we may have to loop.
  */   */
 static char *  char *
 strnnsep(char **stringp, char *delim)  strnnsep(char **stringp, char *delim)
 {  {
         char *tok;          char *tok;
Line 262 
Line 240 
         return (tok);          return (tok);
 }  }
   
 static Key *  void
 keygrab_ssh1(con *c)  keyprint(char *host, char *output_name, char *kd, int len)
 {  {
         static Key *rsa;          static Key *rsa;
         static Buffer msg;          static Buffer msg;
Line 272 
Line 250 
                 buffer_init(&msg);                  buffer_init(&msg);
                 rsa = key_new(KEY_RSA1);                  rsa = key_new(KEY_RSA1);
         }          }
         buffer_append(&msg, c->c_data, c->c_plen);          buffer_append(&msg, kd, len);
         buffer_consume(&msg, 8 - (c->c_plen & 7));      /* padding */          buffer_consume(&msg, 8 - (len & 7));    /* padding */
         if (buffer_get_char(&msg) != (int) SSH_SMSG_PUBLIC_KEY) {          if (buffer_get_char(&msg) != (int) SSH_SMSG_PUBLIC_KEY) {
                 error("%s: invalid packet type", c->c_name);                  error("%s: invalid packet type", host);
                 buffer_clear(&msg);                  buffer_clear(&msg);
                 return NULL;                  return;
         }          }
         buffer_consume(&msg, 8);                /* cookie */          buffer_consume(&msg, 8);                /* cookie */
   
Line 290 
Line 268 
         (void) buffer_get_int(&msg);          (void) buffer_get_int(&msg);
         buffer_get_bignum(&msg, rsa->rsa->e);          buffer_get_bignum(&msg, rsa->rsa->e);
         buffer_get_bignum(&msg, rsa->rsa->n);          buffer_get_bignum(&msg, rsa->rsa->n);
   
         buffer_clear(&msg);          buffer_clear(&msg);
   
         return (rsa);          fprintf(stdout, "%s ", output_name ? output_name : host);
 }          key_write(rsa, stdout);
   
 static int  
 hostjump(Key *hostkey)  
 {  
         kexjmp_key = hostkey;  
         longjmp(kexjmp, 1);  
 }  
   
 static int  
 ssh2_capable(int remote_major, int remote_minor)  
 {  
         switch (remote_major) {  
         case 1:  
                 if (remote_minor == 99)  
                         return 1;  
                 break;  
         case 2:  
                 return 1;  
         default:  
                 break;  
         }  
         return 0;  
 }  
   
 static Key *  
 keygrab_ssh2(con *c)  
 {  
         int j;  
   
         packet_set_connection(c->c_fd, c->c_fd);  
         enable_compat20();  
         myproposal[PROPOSAL_SERVER_HOST_KEY_ALGS] = c->c_keytype == KT_DSA?  
             "ssh-dss": "ssh-rsa";  
         c->c_kex = kex_setup(myproposal);  
         c->c_kex->verify_host_key = hostjump;  
   
         if (!(j = setjmp(kexjmp))) {  
                 nonfatal_fatal = 1;  
                 dispatch_run(DISPATCH_BLOCK, &c->c_kex->done, c->c_kex);  
                 fprintf(stderr, "Impossible! dispatch_run() returned!\n");  
                 exit(1);  
         }  
         nonfatal_fatal = 0;  
         xfree(c->c_kex);  
         c->c_kex = NULL;  
         packet_close();  
   
         return j < 0? NULL : kexjmp_key;  
 }  
   
 static void  
 keyprint(con *c, Key *key)  
 {  
         if (!key)  
                 return;  
   
         fprintf(stdout, "%s ", c->c_output_name ? c->c_output_name : c->c_name);  
         key_write(key, stdout);  
         fputs("\n", stdout);          fputs("\n", stdout);
 }  }
   
 static int  int
 tcpconnect(char *host)  tcpconnect(char *host)
 {  {
         struct addrinfo hints, *ai, *aitop;          struct addrinfo hints, *ai, *aitop;
         char strport[NI_MAXSERV];          char strport[NI_MAXSERV];
         int gaierr, s = -1;          int gaierr, s = -1;
   
         snprintf(strport, sizeof strport, "%d", ssh_port);          snprintf(strport, sizeof strport, "%d", SSH_DEFAULT_PORT);
         memset(&hints, 0, sizeof(hints));          memset(&hints, 0, sizeof(hints));
         hints.ai_family = IPv4or6;          hints.ai_family = family;
         hints.ai_socktype = SOCK_STREAM;          hints.ai_socktype = SOCK_STREAM;
         if ((gaierr = getaddrinfo(host, strport, &hints, &aitop)) != 0)          if ((gaierr = getaddrinfo(host, strport, &hints, &aitop)) != 0)
                 fatal("getaddrinfo %s: %s", host, gai_strerror(gaierr));                  fatal("getaddrinfo %s: %s", host, gai_strerror(gaierr));
Line 389 
Line 308 
         return s;          return s;
 }  }
   
 static int  int
 conalloc(char *iname, char *oname, int keytype)  conalloc(char *iname, char *oname)
 {  {
         int s;          int s;
         char *namebase, *name, *namelist;          char *namebase, *name, *namelist;
Line 419 
Line 338 
         fdcon[s].c_data = (char *) &fdcon[s].c_plen;          fdcon[s].c_data = (char *) &fdcon[s].c_plen;
         fdcon[s].c_len = 4;          fdcon[s].c_len = 4;
         fdcon[s].c_off = 0;          fdcon[s].c_off = 0;
         fdcon[s].c_keytype = keytype;  
         gettimeofday(&fdcon[s].c_tv, NULL);          gettimeofday(&fdcon[s].c_tv, NULL);
         fdcon[s].c_tv.tv_sec += timeout;          fdcon[s].c_tv.tv_sec += timeout;
         TAILQ_INSERT_TAIL(&tq, &fdcon[s], c_link);          TAILQ_INSERT_TAIL(&tq, &fdcon[s], c_link);
Line 428 
Line 346 
         return (s);          return (s);
 }  }
   
 static void  void
 confree(int s)  confree(int s)
 {  {
         if (s >= maxfd || fdcon[s].c_status == CS_UNUSED)          if (s >= maxfd || fdcon[s].c_status == CS_UNUSED)
Line 439 
Line 357 
         if (fdcon[s].c_status == CS_KEYS)          if (fdcon[s].c_status == CS_KEYS)
                 xfree(fdcon[s].c_data);                  xfree(fdcon[s].c_data);
         fdcon[s].c_status = CS_UNUSED;          fdcon[s].c_status = CS_UNUSED;
         fdcon[s].c_keytype = 0;  
         TAILQ_REMOVE(&tq, &fdcon[s], c_link);          TAILQ_REMOVE(&tq, &fdcon[s], c_link);
         FD_CLR(s, read_wait);          FD_CLR(s, read_wait);
         ncon--;          ncon--;
 }  }
   
 static void  void
 contouch(int s)  contouch(int s)
 {  {
         TAILQ_REMOVE(&tq, &fdcon[s], c_link);          TAILQ_REMOVE(&tq, &fdcon[s], c_link);
Line 454 
Line 371 
         TAILQ_INSERT_TAIL(&tq, &fdcon[s], c_link);          TAILQ_INSERT_TAIL(&tq, &fdcon[s], c_link);
 }  }
   
 static int  int
 conrecycle(int s)  conrecycle(int s)
 {  {
         int ret;          int ret;
         con *c = &fdcon[s];          con *c = &fdcon[s];
           char *iname, *oname;
   
         ret = conalloc(c->c_namelist, c->c_output_name, c->c_keytype);          iname = xstrdup(c->c_namelist);
           oname = xstrdup(c->c_output_name);
         confree(s);          confree(s);
           ret = conalloc(iname, oname);
           xfree(iname);
           xfree(oname);
         return (ret);          return (ret);
 }  }
   
 static void  void
 congreet(int s)  congreet(int s)
 {  {
         char buf[256], *cp;          char buf[80], *cp;
         char remote_version[sizeof buf];  
         size_t bufsiz;          size_t bufsiz;
         int remote_major, remote_minor, n = 0;          int n = 0;
         con *c = &fdcon[s];          con *c = &fdcon[s];
   
         bufsiz = sizeof(buf);          bufsiz = sizeof(buf);
         cp = buf;          cp = buf;
         while (bufsiz-- && (n = read(s, cp, 1)) == 1 && *cp != '\n') {          while (bufsiz-- && (n = read(s, cp, 1)) == 1 && *cp != '\n' && *cp != '\r')
                 if (*cp == '\r')  
                         *cp = '\n';  
                 cp++;                  cp++;
         }  
         if (n < 0) {          if (n < 0) {
                 if (errno != ECONNREFUSED)                  if (errno != ECONNREFUSED)
                         error("read (%s): %s", c->c_name, strerror(errno));                          error("read (%s): %s", c->c_name, strerror(errno));
                 conrecycle(s);                  conrecycle(s);
                 return;                  return;
         }          }
         if (n == 0) {  
                 error("%s: Connection closed by remote host", c->c_name);  
                 conrecycle(s);  
                 return;  
         }  
         if (*cp != '\n' && *cp != '\r') {          if (*cp != '\n' && *cp != '\r') {
                 error("%s: bad greeting", c->c_name);                  error("%s: bad greeting", c->c_name);
                 confree(s);                  confree(s);
                 return;                  return;
         }          }
         *cp = '\0';          *cp = '\0';
         if (sscanf(buf, "SSH-%d.%d-%[^\n]\n",          fprintf(stderr, "# %s %s\n", c->c_name, buf);
             &remote_major, &remote_minor, remote_version) == 3)          n = snprintf(buf, sizeof buf, "SSH-1.5-OpenSSH-keyscan\r\n");
                 compat_datafellows(remote_version);  
         else  
                 datafellows = 0;  
         if (c->c_keytype != KT_RSA1) {  
                 if (!ssh2_capable(remote_major, remote_minor)) {  
                         debug("%s doesn't support ssh2", c->c_name);  
                         confree(s);  
                         return;  
                 }  
         } else if (remote_major != 1) {  
                 debug("%s doesn't support ssh1", c->c_name);  
                 confree(s);  
                 return;  
         }  
         fprintf(stderr, "# %s %s\n", c->c_name, chop(buf));  
         n = snprintf(buf, sizeof buf, "SSH-%d.%d-OpenSSH-keyscan\r\n",  
             c->c_keytype == KT_RSA1? PROTOCOL_MAJOR_1 : PROTOCOL_MAJOR_2,  
             c->c_keytype == KT_RSA1? PROTOCOL_MINOR_1 : PROTOCOL_MINOR_2);  
         if (atomicio(write, s, buf, n) != n) {          if (atomicio(write, s, buf, n) != n) {
                 error("write (%s): %s", c->c_name, strerror(errno));                  error("write (%s): %s", c->c_name, strerror(errno));
                 confree(s);                  confree(s);
                 return;                  return;
         }          }
         if (c->c_keytype != KT_RSA1) {  
                 keyprint(c, keygrab_ssh2(c));  
                 confree(s);  
                 return;  
         }  
         c->c_status = CS_SIZE;          c->c_status = CS_SIZE;
         contouch(s);          contouch(s);
 }  }
   
 static void  void
 conread(int s)  conread(int s)
 {  {
         int n;          int n;
Line 560 
Line 450 
                         c->c_status = CS_KEYS;                          c->c_status = CS_KEYS;
                         break;                          break;
                 case CS_KEYS:                  case CS_KEYS:
                         keyprint(c, keygrab_ssh1(c));                          keyprint(c->c_name, c->c_output_name, c->c_data, c->c_plen);
                         confree(s);                          confree(s);
                         return;                          return;
                         break;                          break;
Line 572 
Line 462 
         contouch(s);          contouch(s);
 }  }
   
 static void  void
 conloop(void)  conloop(void)
 {  {
         fd_set *r, *e;          fd_set *r, *e;
Line 624 
Line 514 
         }          }
 }  }
   
 static void  char *
 do_host(char *host)  nexthost(int argc, char **argv)
 {  {
         char *name = strnnsep(&host, " \t\n");          static Linebuf *lb;
         int j;  
   
         if (name == NULL)          for (;;) {
                 return;                  if (!lb) {
         for (j = KT_RSA1; j <= KT_RSA; j *= 2) {                          if (argno >= argc)
                 if (get_keytypes & j) {                                  return (NULL);
                         while (ncon >= MAXCON)                          if (argv[argno][0] != '-')
                                 conloop();                                  return (argv[argno++]);
                         conalloc(name, *host ? host : name, j);                          if (!strcmp(argv[argno], "--")) {
                                   if (++argno >= argc)
                                           return (NULL);
                                   return (argv[argno++]);
                           } else if (!strncmp(argv[argno], "-f", 2)) {
                                   char *fname;
   
                                   if (argv[argno][2])
                                           fname = &argv[argno++][2];
                                   else if (++argno >= argc) {
                                           error("missing filename for `-f'");
                                           return (NULL);
                                   } else
                                           fname = argv[argno++];
                                   if (!strcmp(fname, "-"))
                                           fname = NULL;
                                   lb = Linebuf_alloc(fname, error);
                           } else
                                   error("ignoring invalid/misplaced option `%s'",
                                       argv[argno++]);
                   } else {
                           char *line;
   
                           line = Linebuf_getline(lb);
                           if (line)
                                   return (line);
                           Linebuf_free(lb);
                           lb = NULL;
                 }                  }
         }          }
 }  }
   
 void  void
 fatal(const char *fmt,...)  
 {  
         va_list args;  
         va_start(args, fmt);  
         do_log(SYSLOG_LEVEL_FATAL, fmt, args);  
         va_end(args);  
         if (nonfatal_fatal)  
                 longjmp(kexjmp, -1);  
         else  
                 fatal_cleanup();  
 }  
   
 static void  
 usage(void)  usage(void)
 {  {
         fprintf(stderr, "Usage: %s [options] host ...\n",          fatal("usage: %s [-t timeout] { [--] host | -f file } ...", __progname);
             __progname);          return;
         fprintf(stderr, "Options:\n");  
         fprintf(stderr, "  -f file     Read hosts or addresses from file.\n");  
         fprintf(stderr, "  -p port     Connect to the specified port.\n");  
         fprintf(stderr, "  -t keytype  Specify the host key type.\n");  
         fprintf(stderr, "  -T timeout  Set connection timeout.\n");  
         fprintf(stderr, "  -v          Verbose; display verbose debugging messages.\n");  
         fprintf(stderr, "  -4          Use IPv4 only.\n");  
         fprintf(stderr, "  -6          Use IPv6 only.\n");  
         exit(1);  
 }  }
   
 int  int
 main(int argc, char **argv)  main(int argc, char **argv)
 {  {
         int debug_flag = 0, log_level = SYSLOG_LEVEL_INFO;          char *host = NULL;
         int opt, fopt_count = 0;  
         char *tname;  
   
         extern int optind;  
         extern char *optarg;  
   
         TAILQ_INIT(&tq);          TAILQ_INIT(&tq);
   
         if (argc <= 1)          if (argc <= argno)
                 usage();                  usage();
   
         while ((opt = getopt(argc, argv, "v46p:T:t:f:")) != -1) {          if (argv[1][0] == '-' && argv[1][1] == 't') {
                 switch (opt) {                  argno++;
                 case 'p':                  if (argv[1][2])
                         ssh_port = a2port(optarg);                          timeout = atoi(&argv[1][2]);
                         if (ssh_port == 0) {                  else {
                                 fprintf(stderr, "Bad port '%s'\n", optarg);                          if (argno >= argc)
                                 exit(1);  
                         }  
                         break;  
                 case 'T':  
                         timeout = atoi(optarg);  
                         if (timeout <= 0)  
                                 usage();                                  usage();
                         break;                          timeout = atoi(argv[argno++]);
                 case 'v':  
                         if (!debug_flag) {  
                                 debug_flag = 1;  
                                 log_level = SYSLOG_LEVEL_DEBUG1;  
                         }  
                         else if (log_level < SYSLOG_LEVEL_DEBUG3)  
                                 log_level++;  
                         else  
                                 fatal("Too high debugging level.");  
                         break;  
                 case 'f':  
                         if (strcmp(optarg, "-") == 0)  
                                 optarg = NULL;  
                         argv[fopt_count++] = optarg;  
                         break;  
                 case 't':  
                         get_keytypes = 0;  
                         tname = strtok(optarg, ",");  
                         while (tname) {  
                                 int type = key_type_from_name(tname);  
                                 switch (type) {  
                                 case KEY_RSA1:  
                                         get_keytypes |= KT_RSA1;  
                                         break;  
                                 case KEY_DSA:  
                                         get_keytypes |= KT_DSA;  
                                         break;  
                                 case KEY_RSA:  
                                         get_keytypes |= KT_RSA;  
                                         break;  
                                 case KEY_UNSPEC:  
                                         fatal("unknown key type %s", tname);  
                                 }  
                                 tname = strtok(NULL, ",");  
                         }  
                         break;  
                 case '4':  
                         IPv4or6 = AF_INET;  
                         break;  
                 case '6':  
                         IPv4or6 = AF_INET6;  
                         break;  
                 case '?':  
                 default:  
                         usage();  
                 }                  }
                   if (timeout <= 0)
                           usage();
         }          }
         if (optind == argc && !fopt_count)          if (argc <= argno)
                 usage();                  usage();
   
         log_init("ssh-keyscan", log_level, SYSLOG_FACILITY_USER, 1);  
   
         maxfd = fdlim_get(1);          maxfd = fdlim_get(1);
         if (maxfd < 0)          if (maxfd < 0)
                 fatal("%s: fdlim_get: bad value", __progname);                  fatal("%s: fdlim_get: bad value", __progname);
Line 767 
Line 605 
         read_wait = xmalloc(read_wait_size);          read_wait = xmalloc(read_wait_size);
         memset(read_wait, 0, read_wait_size);          memset(read_wait, 0, read_wait_size);
   
         if (fopt_count) {          do {
                 Linebuf *lb;                  while (ncon < MAXCON) {
                 char *line;                          char *name;
                 int j;  
   
                 for (j = 0; j < fopt_count; j++) {                          host = nexthost(argc, argv);
                         lb = Linebuf_alloc(argv[j], error);                          if (host == NULL)
                         if (!lb)                                  break;
                                 continue;                          name = strnnsep(&host, " \t\n");
                         while ((line = Linebuf_getline(lb)) != NULL)                          conalloc(name, *host ? host : name);
                                 do_host(line);  
                         Linebuf_free(lb);  
                 }                  }
         }                  conloop();
           } while (host);
         while (optind < argc)  
                 do_host(argv[optind++]);  
   
         while (ncon > 0)          while (ncon > 0)
                 conloop();                  conloop();
   

Legend:
Removed from v.1.22.4.3  
changed lines
  Added in v.1.23