Mercurial > dropbear
comparison cli-runopts.c @ 579:8c737cd7c1af
merge of '48fdaa8706d1acda35e9d564adc9a1fbc96c18c8'
and '658fd03abd21e0da7c4c89b9fff9dc693c72daae'
author | Matt Johnston <matt@ucc.asn.au> |
---|---|
date | Sat, 27 Feb 2010 11:53:18 +0000 |
parents | 69e98c45db7c 44f486b72427 |
children | dd9947170fc8 |
comparison
equal
deleted
inserted
replaced
577:69e98c45db7c | 579:8c737cd7c1af |
---|---|
27 #include "signkey.h" | 27 #include "signkey.h" |
28 #include "buffer.h" | 28 #include "buffer.h" |
29 #include "dbutil.h" | 29 #include "dbutil.h" |
30 #include "algo.h" | 30 #include "algo.h" |
31 #include "tcpfwd.h" | 31 #include "tcpfwd.h" |
32 #include "list.h" | |
32 | 33 |
33 cli_runopts cli_opts; /* GLOBAL */ | 34 cli_runopts cli_opts; /* GLOBAL */ |
34 | 35 |
35 static void printhelp(); | 36 static void printhelp(); |
36 static void parse_hostname(const char* orighostarg); | 37 static void parse_hostname(const char* orighostarg); |
38 static void fill_own_user(); | 39 static void fill_own_user(); |
39 #ifdef ENABLE_CLI_PUBKEY_AUTH | 40 #ifdef ENABLE_CLI_PUBKEY_AUTH |
40 static void loadidentityfile(const char* filename); | 41 static void loadidentityfile(const char* filename); |
41 #endif | 42 #endif |
42 #ifdef ENABLE_CLI_ANYTCPFWD | 43 #ifdef ENABLE_CLI_ANYTCPFWD |
43 static void addforward(const char* str, struct TCPFwdList** fwdlist); | 44 static void addforward(const char* str, m_list *fwdlist); |
44 #endif | 45 #endif |
45 #ifdef ENABLE_CLI_NETCAT | 46 #ifdef ENABLE_CLI_NETCAT |
46 static void add_netcat(const char *str); | 47 static void add_netcat(const char *str); |
47 #endif | 48 #endif |
48 | 49 |
64 "-y Always accept remote host key if unknown\n" | 65 "-y Always accept remote host key if unknown\n" |
65 "-s Request a subsystem (use for sftp)\n" | 66 "-s Request a subsystem (use for sftp)\n" |
66 #ifdef ENABLE_CLI_PUBKEY_AUTH | 67 #ifdef ENABLE_CLI_PUBKEY_AUTH |
67 "-i <identityfile> (multiple allowed)\n" | 68 "-i <identityfile> (multiple allowed)\n" |
68 #endif | 69 #endif |
70 #ifdef ENABLE_CLI_AGENTFWD | |
71 "-A Enable agent auth forwarding\n" | |
72 #endif | |
69 #ifdef ENABLE_CLI_LOCALTCPFWD | 73 #ifdef ENABLE_CLI_LOCALTCPFWD |
70 "-L <listenport:remotehost:remoteport> Local port forwarding\n" | 74 "-L <listenport:remotehost:remoteport> Local port forwarding\n" |
71 "-g Allow remote hosts to connect to forwarded ports\n" | 75 "-g Allow remote hosts to connect to forwarded ports\n" |
72 #endif | 76 #endif |
73 #ifdef ENABLE_CLI_REMOTETCPFWD | 77 #ifdef ENABLE_CLI_REMOTETCPFWD |
89 DEFAULT_RECV_WINDOW, DEFAULT_KEEPALIVE, DEFAULT_IDLE_TIMEOUT); | 93 DEFAULT_RECV_WINDOW, DEFAULT_KEEPALIVE, DEFAULT_IDLE_TIMEOUT); |
90 | 94 |
91 } | 95 } |
92 | 96 |
93 void cli_getopts(int argc, char ** argv) { | 97 void cli_getopts(int argc, char ** argv) { |
94 | |
95 unsigned int i, j; | 98 unsigned int i, j; |
96 char ** next = 0; | 99 char ** next = 0; |
97 unsigned int cmdlen; | 100 unsigned int cmdlen; |
98 #ifdef ENABLE_CLI_PUBKEY_AUTH | 101 #ifdef ENABLE_CLI_PUBKEY_AUTH |
99 int nextiskey = 0; /* A flag if the next argument is a keyfile */ | 102 int nextiskey = 0; /* A flag if the next argument is a keyfile */ |
110 char* dummy = NULL; /* Not used for anything real */ | 113 char* dummy = NULL; /* Not used for anything real */ |
111 | 114 |
112 char* recv_window_arg = NULL; | 115 char* recv_window_arg = NULL; |
113 char* keepalive_arg = NULL; | 116 char* keepalive_arg = NULL; |
114 char* idle_timeout_arg = NULL; | 117 char* idle_timeout_arg = NULL; |
118 char *host_arg = NULL; | |
115 | 119 |
116 /* see printhelp() for options */ | 120 /* see printhelp() for options */ |
117 cli_opts.progname = argv[0]; | 121 cli_opts.progname = argv[0]; |
118 cli_opts.remotehost = NULL; | 122 cli_opts.remotehost = NULL; |
119 cli_opts.remoteport = NULL; | 123 cli_opts.remoteport = NULL; |
123 cli_opts.backgrounded = 0; | 127 cli_opts.backgrounded = 0; |
124 cli_opts.wantpty = 9; /* 9 means "it hasn't been touched", gets set later */ | 128 cli_opts.wantpty = 9; /* 9 means "it hasn't been touched", gets set later */ |
125 cli_opts.always_accept_key = 0; | 129 cli_opts.always_accept_key = 0; |
126 cli_opts.is_subsystem = 0; | 130 cli_opts.is_subsystem = 0; |
127 #ifdef ENABLE_CLI_PUBKEY_AUTH | 131 #ifdef ENABLE_CLI_PUBKEY_AUTH |
128 cli_opts.privkeys = NULL; | 132 cli_opts.privkeys = list_new(); |
129 #endif | 133 #endif |
130 #ifdef ENABLE_CLI_LOCALTCPFWD | 134 #ifdef ENABLE_CLI_LOCALTCPFWD |
131 cli_opts.localfwds = NULL; | 135 cli_opts.localfwds = list_new(); |
132 opts.listen_fwd_all = 0; | 136 opts.listen_fwd_all = 0; |
133 #endif | 137 #endif |
134 #ifdef ENABLE_CLI_REMOTETCPFWD | 138 #ifdef ENABLE_CLI_REMOTETCPFWD |
135 cli_opts.remotefwds = NULL; | 139 cli_opts.remotefwds = list_new(); |
140 #endif | |
141 #ifdef ENABLE_CLI_AGENTFWD | |
142 cli_opts.agent_fwd = 0; | |
143 cli_opts.agent_keys_loaded = 0; | |
136 #endif | 144 #endif |
137 #ifdef ENABLE_CLI_PROXYCMD | 145 #ifdef ENABLE_CLI_PROXYCMD |
138 cli_opts.proxycmd = NULL; | 146 cli_opts.proxycmd = NULL; |
147 #endif | |
148 #ifndef DISABLE_ZLIB | |
149 opts.enable_compress = 1; | |
139 #endif | 150 #endif |
140 /* not yet | 151 /* not yet |
141 opts.ipv4 = 1; | 152 opts.ipv4 = 1; |
142 opts.ipv6 = 1; | 153 opts.ipv6 = 1; |
143 */ | 154 */ |
156 } | 167 } |
157 #endif | 168 #endif |
158 #ifdef ENABLE_CLI_REMOTETCPFWD | 169 #ifdef ENABLE_CLI_REMOTETCPFWD |
159 if (nextisremote) { | 170 if (nextisremote) { |
160 TRACE(("nextisremote true")) | 171 TRACE(("nextisremote true")) |
161 addforward(argv[i], &cli_opts.remotefwds); | 172 addforward(argv[i], cli_opts.remotefwds); |
162 nextisremote = 0; | 173 nextisremote = 0; |
163 continue; | 174 continue; |
164 } | 175 } |
165 #endif | 176 #endif |
166 #ifdef ENABLE_CLI_LOCALTCPFWD | 177 #ifdef ENABLE_CLI_LOCALTCPFWD |
167 if (nextislocal) { | 178 if (nextislocal) { |
168 TRACE(("nextislocal true")) | 179 TRACE(("nextislocal true")) |
169 addforward(argv[i], &cli_opts.localfwds); | 180 addforward(argv[i], cli_opts.localfwds); |
170 nextislocal = 0; | 181 nextislocal = 0; |
171 continue; | 182 continue; |
172 } | 183 } |
173 #endif | 184 #endif |
174 #ifdef ENABLE_CLI_NETCAT | 185 #ifdef ENABLE_CLI_NETCAT |
264 next = &keepalive_arg; | 275 next = &keepalive_arg; |
265 break; | 276 break; |
266 case 'I': | 277 case 'I': |
267 next = &idle_timeout_arg; | 278 next = &idle_timeout_arg; |
268 break; | 279 break; |
280 #ifdef ENABLE_CLI_AGENTFWD | |
281 case 'A': | |
282 cli_opts.agent_fwd = 1; | |
283 break; | |
284 #endif | |
269 #ifdef DEBUG_TRACE | 285 #ifdef DEBUG_TRACE |
270 case 'v': | 286 case 'v': |
271 debug_trace = 1; | 287 debug_trace = 1; |
272 break; | 288 break; |
273 #endif | 289 #endif |
302 } else { | 318 } else { |
303 TRACE(("non-flag arg: '%s'", argv[i])) | 319 TRACE(("non-flag arg: '%s'", argv[i])) |
304 | 320 |
305 /* Either the hostname or commands */ | 321 /* Either the hostname or commands */ |
306 | 322 |
307 if (cli_opts.remotehost == NULL) { | 323 if (host_arg == NULL) { |
308 #ifdef ENABLE_CLI_MULTIHOP | 324 host_arg = argv[i]; |
309 parse_multihop_hostname(argv[i], argv[0]); | |
310 #else | |
311 parse_hostname(argv[i]); | |
312 #endif | |
313 } else { | 325 } else { |
314 | 326 |
315 /* this is part of the commands to send - after this we | 327 /* this is part of the commands to send - after this we |
316 * don't parse any more options, and flags are sent as the | 328 * don't parse any more options, and flags are sent as the |
317 * command */ | 329 * command */ |
336 } | 348 } |
337 } | 349 } |
338 | 350 |
339 /* And now a few sanity checks and setup */ | 351 /* And now a few sanity checks and setup */ |
340 | 352 |
341 if (cli_opts.remotehost == NULL) { | 353 if (host_arg == NULL) { |
342 printhelp(); | 354 printhelp(); |
343 exit(EXIT_FAILURE); | 355 exit(EXIT_FAILURE); |
344 } | 356 } |
345 | 357 |
346 if (cli_opts.remoteport == NULL) { | 358 if (cli_opts.remoteport == NULL) { |
367 if (opts.recv_window == 0 || opts.recv_window > MAX_RECV_WINDOW) { | 379 if (opts.recv_window == 0 || opts.recv_window > MAX_RECV_WINDOW) { |
368 dropbear_exit("Bad recv window '%s'", recv_window_arg); | 380 dropbear_exit("Bad recv window '%s'", recv_window_arg); |
369 } | 381 } |
370 } | 382 } |
371 if (keepalive_arg) { | 383 if (keepalive_arg) { |
372 if (m_str_to_uint(keepalive_arg, &opts.keepalive_secs) == DROPBEAR_FAILURE) { | 384 unsigned int val; |
385 if (m_str_to_uint(keepalive_arg, &val) == DROPBEAR_FAILURE) { | |
373 dropbear_exit("Bad keepalive '%s'", keepalive_arg); | 386 dropbear_exit("Bad keepalive '%s'", keepalive_arg); |
374 } | 387 } |
388 opts.keepalive_secs = val; | |
375 } | 389 } |
376 | 390 |
377 if (idle_timeout_arg) { | 391 if (idle_timeout_arg) { |
378 if (m_str_to_uint(idle_timeout_arg, &opts.idle_timeout_secs) == DROPBEAR_FAILURE) { | 392 unsigned int val; |
393 if (m_str_to_uint(idle_timeout_arg, &val) == DROPBEAR_FAILURE) { | |
379 dropbear_exit("Bad idle_timeout '%s'", idle_timeout_arg); | 394 dropbear_exit("Bad idle_timeout '%s'", idle_timeout_arg); |
380 } | 395 } |
396 opts.idle_timeout_secs = val; | |
381 } | 397 } |
382 | 398 |
383 #ifdef ENABLE_CLI_NETCAT | 399 #ifdef ENABLE_CLI_NETCAT |
384 if (cli_opts.cmd && cli_opts.netcat_host) { | 400 if (cli_opts.cmd && cli_opts.netcat_host) { |
385 dropbear_log(LOG_INFO, "Ignoring command '%s' in netcat mode", cli_opts.cmd); | 401 dropbear_log(LOG_INFO, "Ignoring command '%s' in netcat mode", cli_opts.cmd); |
386 } | 402 } |
387 #endif | 403 #endif |
388 | 404 |
405 /* The hostname gets set up last, since | |
406 * in multi-hop mode it will require knowledge | |
407 * of other flags such as -i */ | |
408 #ifdef ENABLE_CLI_MULTIHOP | |
409 parse_multihop_hostname(host_arg, argv[0]); | |
410 #else | |
411 parse_hostname(host_arg); | |
412 #endif | |
389 } | 413 } |
390 | 414 |
391 #ifdef ENABLE_CLI_PUBKEY_AUTH | 415 #ifdef ENABLE_CLI_PUBKEY_AUTH |
392 static void loadidentityfile(const char* filename) { | 416 static void loadidentityfile(const char* filename) { |
393 | |
394 struct SignKeyList * nextkey; | |
395 sign_key *key; | 417 sign_key *key; |
396 int keytype; | 418 int keytype; |
397 | 419 |
398 key = new_sign_key(); | 420 key = new_sign_key(); |
399 keytype = DROPBEAR_SIGNKEY_ANY; | 421 keytype = DROPBEAR_SIGNKEY_ANY; |
400 if ( readhostkey(filename, key, &keytype) != DROPBEAR_SUCCESS ) { | 422 if ( readhostkey(filename, key, &keytype) != DROPBEAR_SUCCESS ) { |
401 | |
402 fprintf(stderr, "Failed loading keyfile '%s'\n", filename); | 423 fprintf(stderr, "Failed loading keyfile '%s'\n", filename); |
403 sign_key_free(key); | 424 sign_key_free(key); |
404 | |
405 } else { | 425 } else { |
406 | 426 key->type = keytype; |
407 nextkey = (struct SignKeyList*)m_malloc(sizeof(struct SignKeyList)); | 427 key->source = SIGNKEY_SOURCE_RAW_FILE; |
408 nextkey->key = key; | 428 key->filename = m_strdup(filename); |
409 nextkey->next = cli_opts.privkeys; | 429 list_append(cli_opts.privkeys, key); |
410 nextkey->type = keytype; | |
411 cli_opts.privkeys = nextkey; | |
412 } | 430 } |
413 } | 431 } |
414 #endif | 432 #endif |
415 | 433 |
416 #ifdef ENABLE_CLI_MULTIHOP | 434 #ifdef ENABLE_CLI_MULTIHOP |
435 | |
436 static char* | |
437 multihop_passthrough_args() { | |
438 char *ret; | |
439 int total; | |
440 unsigned int len = 0; | |
441 m_list_elem *iter; | |
442 /* Fill out -i and -W options that make sense for all | |
443 * the intermediate processes */ | |
444 for (iter = cli_opts.privkeys->first; iter; iter = iter->next) | |
445 { | |
446 sign_key * key = (sign_key*)iter->item; | |
447 len += 3 + strlen(key->filename); | |
448 } | |
449 len += 20; // space for -W <size>, terminator. | |
450 ret = m_malloc(len); | |
451 total = 0; | |
452 | |
453 if (opts.recv_window != DEFAULT_RECV_WINDOW) | |
454 { | |
455 int written = snprintf(ret+total, len-total, "-W %d", opts.recv_window); | |
456 total += written; | |
457 } | |
458 | |
459 for (iter = cli_opts.privkeys->first; iter; iter = iter->next) | |
460 { | |
461 sign_key * key = (sign_key*)iter->item; | |
462 const size_t size = len - total; | |
463 int written = snprintf(ret+total, size, "-i %s", key->filename); | |
464 dropbear_assert((unsigned int)written < size); | |
465 total += written; | |
466 } | |
467 | |
468 return ret; | |
469 } | |
417 | 470 |
418 /* Sets up 'onion-forwarding' connections. This will spawn | 471 /* Sets up 'onion-forwarding' connections. This will spawn |
419 * a separate dbclient process for each hop. | 472 * a separate dbclient process for each hop. |
420 * As an example, if the cmdline is | 473 * As an example, if the cmdline is |
421 * dbclient wrt,madako,canyons | 474 * dbclient wrt,madako,canyons |
427 * | 480 * |
428 * Ports for hosts can be specified as host/port. | 481 * Ports for hosts can be specified as host/port. |
429 */ | 482 */ |
430 static void parse_multihop_hostname(const char* orighostarg, const char* argv0) { | 483 static void parse_multihop_hostname(const char* orighostarg, const char* argv0) { |
431 char *userhostarg = NULL; | 484 char *userhostarg = NULL; |
432 char *last_hop = NULL;; | 485 char *hostbuf = NULL; |
486 char *last_hop = NULL; | |
433 char *remainder = NULL; | 487 char *remainder = NULL; |
434 | 488 |
435 /* both scp and rsync parse a user@host argument | 489 /* both scp and rsync parse a user@host argument |
436 * and turn it into "-l user host". This breaks | 490 * and turn it into "-l user host". This breaks |
437 * for our multihop syntax, so we suture it back together. | 491 * for our multihop syntax, so we suture it back together. |
439 * though that should be fairly uncommon. */ | 493 * though that should be fairly uncommon. */ |
440 if (cli_opts.username | 494 if (cli_opts.username |
441 && strchr(cli_opts.username, ',') | 495 && strchr(cli_opts.username, ',') |
442 && strchr(cli_opts.username, '@')) { | 496 && strchr(cli_opts.username, '@')) { |
443 unsigned int len = strlen(orighostarg) + strlen(cli_opts.username) + 2; | 497 unsigned int len = strlen(orighostarg) + strlen(cli_opts.username) + 2; |
444 userhostarg = m_malloc(len); | 498 hostbuf = m_malloc(len); |
445 snprintf(userhostarg, len, "%s@%s", cli_opts.username, orighostarg); | 499 snprintf(hostbuf, len, "%s@%s", cli_opts.username, orighostarg); |
446 } else { | 500 } else { |
447 userhostarg = m_strdup(orighostarg); | 501 hostbuf = m_strdup(orighostarg); |
448 } | 502 } |
503 userhostarg = hostbuf; | |
449 | 504 |
450 last_hop = strrchr(userhostarg, ','); | 505 last_hop = strrchr(userhostarg, ','); |
451 if (last_hop) { | 506 if (last_hop) { |
452 if (last_hop == userhostarg) { | 507 if (last_hop == userhostarg) { |
453 dropbear_exit("Bad multi-hop hostnames"); | 508 dropbear_exit("Bad multi-hop hostnames"); |
461 parse_hostname(userhostarg); | 516 parse_hostname(userhostarg); |
462 | 517 |
463 if (last_hop) { | 518 if (last_hop) { |
464 /* Set up the proxycmd */ | 519 /* Set up the proxycmd */ |
465 unsigned int cmd_len = 0; | 520 unsigned int cmd_len = 0; |
521 char *passthrough_args = multihop_passthrough_args(); | |
466 if (cli_opts.proxycmd) { | 522 if (cli_opts.proxycmd) { |
467 dropbear_exit("-J can't be used with multihop mode"); | 523 dropbear_exit("-J can't be used with multihop mode"); |
468 } | 524 } |
469 if (cli_opts.remoteport == NULL) { | 525 if (cli_opts.remoteport == NULL) { |
470 cli_opts.remoteport = "22"; | 526 cli_opts.remoteport = "22"; |
471 } | 527 } |
472 cmd_len = strlen(remainder) | 528 cmd_len = strlen(argv0) + strlen(remainder) |
473 + strlen(cli_opts.remotehost) + strlen(cli_opts.remoteport) | 529 + strlen(cli_opts.remotehost) + strlen(cli_opts.remoteport) |
474 + strlen(argv0) + 30; | 530 + strlen(passthrough_args) |
531 + 30; | |
475 cli_opts.proxycmd = m_malloc(cmd_len); | 532 cli_opts.proxycmd = m_malloc(cmd_len); |
476 snprintf(cli_opts.proxycmd, cmd_len, "%s -B %s:%s %s", | 533 snprintf(cli_opts.proxycmd, cmd_len, "%s -B %s:%s %s %s", |
477 argv0, cli_opts.remotehost, cli_opts.remoteport, remainder); | 534 argv0, cli_opts.remotehost, cli_opts.remoteport, |
478 } | 535 passthrough_args, remainder); |
536 #ifndef DISABLE_ZLIB | |
537 /* The stream will be incompressible since it's encrypted. */ | |
538 opts.enable_compress = 0; | |
539 #endif | |
540 m_free(passthrough_args); | |
541 } | |
542 m_free(hostbuf); | |
479 } | 543 } |
480 #endif /* !ENABLE_CLI_MULTIHOP */ | 544 #endif /* !ENABLE_CLI_MULTIHOP */ |
481 | 545 |
482 /* Parses a [user@]hostname[/port] argument. */ | 546 /* Parses a [user@]hostname[/port] argument. */ |
483 static void parse_hostname(const char* orighostarg) { | 547 static void parse_hostname(const char* orighostarg) { |
564 } | 628 } |
565 | 629 |
566 #ifdef ENABLE_CLI_ANYTCPFWD | 630 #ifdef ENABLE_CLI_ANYTCPFWD |
567 /* Turn a "[listenaddr:]listenport:remoteaddr:remoteport" string into into a forwarding | 631 /* Turn a "[listenaddr:]listenport:remoteaddr:remoteport" string into into a forwarding |
568 * set, and add it to the forwarding list */ | 632 * set, and add it to the forwarding list */ |
569 static void addforward(const char* origstr, struct TCPFwdList** fwdlist) { | 633 static void addforward(const char* origstr, m_list *fwdlist) { |
570 | 634 |
571 char *part1 = NULL, *part2 = NULL, *part3 = NULL, *part4 = NULL; | 635 char *part1 = NULL, *part2 = NULL, *part3 = NULL, *part4 = NULL; |
572 char * listenaddr = NULL; | 636 char * listenaddr = NULL; |
573 char * listenport = NULL; | 637 char * listenport = NULL; |
574 char * connectaddr = NULL; | 638 char * connectaddr = NULL; |
575 char * connectport = NULL; | 639 char * connectport = NULL; |
576 struct TCPFwdList* newfwd = NULL; | 640 struct TCPFwdEntry* newfwd = NULL; |
577 char * str = NULL; | 641 char * str = NULL; |
578 | 642 |
579 TRACE(("enter addforward")) | 643 TRACE(("enter addforward")) |
580 | 644 |
581 /* We need to split the original argument up. This var | 645 /* We need to split the original argument up. This var |
616 listenport = part1; | 680 listenport = part1; |
617 connectaddr = part2; | 681 connectaddr = part2; |
618 connectport = part3; | 682 connectport = part3; |
619 } | 683 } |
620 | 684 |
621 newfwd = (struct TCPFwdList*)m_malloc(sizeof(struct TCPFwdList)); | 685 } |
686 | |
687 newfwd = m_malloc(sizeof(struct TCPFwdEntry)); | |
622 | 688 |
623 /* Now we check the ports - note that the port ints are unsigned, | 689 /* Now we check the ports - note that the port ints are unsigned, |
624 * the check later only checks for >= MAX_PORT */ | 690 * the check later only checks for >= MAX_PORT */ |
625 if (m_str_to_uint(listenport, &newfwd->listenport) == DROPBEAR_FAILURE) { | 691 if (m_str_to_uint(listenport, &newfwd->listenport) == DROPBEAR_FAILURE) { |
626 TRACE(("bad listenport strtoul")) | 692 TRACE(("bad listenport strtoul")) |
644 TRACE(("connectport > 65535")) | 710 TRACE(("connectport > 65535")) |
645 goto badport; | 711 goto badport; |
646 } | 712 } |
647 | 713 |
648 newfwd->have_reply = 0; | 714 newfwd->have_reply = 0; |
649 newfwd->next = *fwdlist; | 715 list_append(fwdlist, newfwd); |
650 *fwdlist = newfwd; | |
651 | 716 |
652 TRACE(("leave addforward: done")) | 717 TRACE(("leave addforward: done")) |
653 return; | 718 return; |
654 | 719 |
655 fail: | 720 fail: |