Support listening on port 0.
[openocd.git] / src / server / server.c
1 /***************************************************************************
2 * Copyright (C) 2005 by Dominic Rath *
3 * Dominic.Rath@gmx.de *
4 * *
5 * Copyright (C) 2007-2010 Øyvind Harboe *
6 * oyvind.harboe@zylin.com *
7 * *
8 * Copyright (C) 2008 by Spencer Oliver *
9 * spen@spen-soft.co.uk *
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 * This program is distributed in the hope that it will be useful, *
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
19 * GNU General Public License for more details. *
20 * *
21 * You should have received a copy of the GNU General Public License *
22 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
23 ***************************************************************************/
24
25 #ifdef HAVE_CONFIG_H
26 #include "config.h"
27 #endif
28
29 #include "server.h"
30 #include <target/target.h>
31 #include <target/target_request.h>
32 #include <target/openrisc/jsp_server.h>
33 #include "openocd.h"
34 #include "tcl_server.h"
35 #include "telnet_server.h"
36
37 #include <signal.h>
38
39 #ifdef HAVE_NETDB_H
40 #include <netdb.h>
41 #endif
42
43 #ifndef _WIN32
44 #include <netinet/tcp.h>
45 #endif
46
47 static struct service *services;
48
49 /* shutdown_openocd == 1: exit the main event loop, and quit the
50 * debugger; 2: quit with non-zero return code */
51 static int shutdown_openocd;
52
53 /* store received signal to exit application by killing ourselves */
54 static int last_signal;
55
56 /* set the polling period to 100ms */
57 static int polling_period = 100;
58
59 /* address by name on which to listen for incoming TCP/IP connections */
60 static char *bindto_name;
61
62 static int add_connection(struct service *service, struct command_context *cmd_ctx)
63 {
64 socklen_t address_size;
65 struct connection *c, **p;
66 int retval;
67 int flag = 1;
68
69 c = malloc(sizeof(struct connection));
70 c->fd = -1;
71 c->fd_out = -1;
72 memset(&c->sin, 0, sizeof(c->sin));
73 c->cmd_ctx = copy_command_context(cmd_ctx);
74 c->service = service;
75 c->input_pending = 0;
76 c->priv = NULL;
77 c->next = NULL;
78
79 if (service->type == CONNECTION_TCP) {
80 address_size = sizeof(c->sin);
81
82 c->fd = accept(service->fd, (struct sockaddr *)&service->sin, &address_size);
83 c->fd_out = c->fd;
84
85 /* This increases performance dramatically for e.g. GDB load which
86 * does not have a sliding window protocol.
87 *
88 * Ignore errors from this fn as it probably just means less performance
89 */
90 setsockopt(c->fd, /* socket affected */
91 IPPROTO_TCP, /* set option at TCP level */
92 TCP_NODELAY, /* name of option */
93 (char *)&flag, /* the cast is historical cruft */
94 sizeof(int)); /* length of option value */
95
96 LOG_INFO("accepting '%s' connection on tcp/%s", service->name, service->port);
97 retval = service->new_connection(c);
98 if (retval != ERROR_OK) {
99 close_socket(c->fd);
100 LOG_ERROR("attempted '%s' connection rejected", service->name);
101 command_done(c->cmd_ctx);
102 free(c);
103 return retval;
104 }
105 } else if (service->type == CONNECTION_STDINOUT) {
106 c->fd = service->fd;
107 c->fd_out = fileno(stdout);
108
109 #ifdef _WIN32
110 /* we are using stdin/out so ignore ctrl-c under windoze */
111 SetConsoleCtrlHandler(NULL, TRUE);
112 #endif
113
114 /* do not check for new connections again on stdin */
115 service->fd = -1;
116
117 LOG_INFO("accepting '%s' connection from pipe", service->name);
118 retval = service->new_connection(c);
119 if (retval != ERROR_OK) {
120 LOG_ERROR("attempted '%s' connection rejected", service->name);
121 command_done(c->cmd_ctx);
122 free(c);
123 return retval;
124 }
125 } else if (service->type == CONNECTION_PIPE) {
126 c->fd = service->fd;
127 /* do not check for new connections again on stdin */
128 service->fd = -1;
129
130 char *out_file = alloc_printf("%so", service->port);
131 c->fd_out = open(out_file, O_WRONLY);
132 free(out_file);
133 if (c->fd_out == -1) {
134 LOG_ERROR("could not open %s", service->port);
135 command_done(c->cmd_ctx);
136 free(c);
137 return ERROR_FAIL;
138 }
139
140 LOG_INFO("accepting '%s' connection from pipe %s", service->name, service->port);
141 retval = service->new_connection(c);
142 if (retval != ERROR_OK) {
143 LOG_ERROR("attempted '%s' connection rejected", service->name);
144 command_done(c->cmd_ctx);
145 free(c);
146 return retval;
147 }
148 }
149
150 /* add to the end of linked list */
151 for (p = &service->connections; *p; p = &(*p)->next)
152 ;
153 *p = c;
154
155 if (service->max_connections != CONNECTION_LIMIT_UNLIMITED)
156 service->max_connections--;
157
158 return ERROR_OK;
159 }
160
161 static int remove_connection(struct service *service, struct connection *connection)
162 {
163 struct connection **p = &service->connections;
164 struct connection *c;
165
166 /* find connection */
167 while ((c = *p)) {
168 if (c->fd == connection->fd) {
169 service->connection_closed(c);
170 if (service->type == CONNECTION_TCP)
171 close_socket(c->fd);
172 else if (service->type == CONNECTION_PIPE) {
173 /* The service will listen to the pipe again */
174 c->service->fd = c->fd;
175 }
176
177 command_done(c->cmd_ctx);
178
179 /* delete connection */
180 *p = c->next;
181 free(c);
182
183 if (service->max_connections != CONNECTION_LIMIT_UNLIMITED)
184 service->max_connections++;
185
186 break;
187 }
188
189 /* redirect p to next list pointer */
190 p = &(*p)->next;
191 }
192
193 return ERROR_OK;
194 }
195
196 static void free_service(struct service *c)
197 {
198 free(c->name);
199 free(c->port);
200 free(c);
201 }
202
203 int add_service(char *name,
204 const char *port,
205 int max_connections,
206 new_connection_handler_t new_connection_handler,
207 input_handler_t input_handler,
208 connection_closed_handler_t connection_closed_handler,
209 void *priv)
210 {
211 struct service *c, **p;
212 struct hostent *hp;
213 int so_reuseaddr_option = 1;
214
215 c = malloc(sizeof(struct service));
216
217 c->name = strdup(name);
218 c->port = strdup(port);
219 c->max_connections = 1; /* Only TCP/IP ports can support more than one connection */
220 c->fd = -1;
221 c->connections = NULL;
222 c->new_connection = new_connection_handler;
223 c->input = input_handler;
224 c->connection_closed = connection_closed_handler;
225 c->priv = priv;
226 c->next = NULL;
227 long portnumber;
228 if (strcmp(c->port, "pipe") == 0)
229 c->type = CONNECTION_STDINOUT;
230 else {
231 char *end;
232 portnumber = strtol(c->port, &end, 0);
233 if (!*end && (parse_long(c->port, &portnumber) == ERROR_OK)) {
234 c->portnumber = portnumber;
235 c->type = CONNECTION_TCP;
236 } else
237 c->type = CONNECTION_PIPE;
238 }
239
240 if (c->type == CONNECTION_TCP) {
241 c->max_connections = max_connections;
242
243 c->fd = socket(AF_INET, SOCK_STREAM, 0);
244 if (c->fd == -1) {
245 LOG_ERROR("error creating socket: %s", strerror(errno));
246 free_service(c);
247 return ERROR_FAIL;
248 }
249
250 setsockopt(c->fd,
251 SOL_SOCKET,
252 SO_REUSEADDR,
253 (void *)&so_reuseaddr_option,
254 sizeof(int));
255
256 socket_nonblock(c->fd);
257
258 memset(&c->sin, 0, sizeof(c->sin));
259 c->sin.sin_family = AF_INET;
260
261 if (bindto_name == NULL)
262 c->sin.sin_addr.s_addr = INADDR_ANY;
263 else {
264 hp = gethostbyname(bindto_name);
265 if (hp == NULL) {
266 LOG_ERROR("couldn't resolve bindto address: %s", bindto_name);
267 close_socket(c->fd);
268 free_service(c);
269 return ERROR_FAIL;
270 }
271 memcpy(&c->sin.sin_addr, hp->h_addr_list[0], hp->h_length);
272 }
273 c->sin.sin_port = htons(c->portnumber);
274
275 if (bind(c->fd, (struct sockaddr *)&c->sin, sizeof(c->sin)) == -1) {
276 LOG_ERROR("couldn't bind %s to socket on port %d: %s", name, c->portnumber, strerror(errno));
277 close_socket(c->fd);
278 free_service(c);
279 return ERROR_FAIL;
280 }
281
282 #ifndef _WIN32
283 int segsize = 65536;
284 setsockopt(c->fd, IPPROTO_TCP, TCP_MAXSEG, &segsize, sizeof(int));
285 #endif
286 int window_size = 128 * 1024;
287
288 /* These setsockopt()s must happen before the listen() */
289
290 setsockopt(c->fd, SOL_SOCKET, SO_SNDBUF,
291 (char *)&window_size, sizeof(window_size));
292 setsockopt(c->fd, SOL_SOCKET, SO_RCVBUF,
293 (char *)&window_size, sizeof(window_size));
294
295 if (listen(c->fd, 1) == -1) {
296 LOG_ERROR("couldn't listen on socket: %s", strerror(errno));
297 close_socket(c->fd);
298 free_service(c);
299 return ERROR_FAIL;
300 }
301
302 struct sockaddr_in addr_in;
303 socklen_t addr_in_size = sizeof(addr_in);
304 getsockname(c->fd, (struct sockaddr *)&addr_in, &addr_in_size);
305 LOG_INFO("Listening on port %hu for %s connections",
306 ntohs(addr_in.sin_port), name);
307 } else if (c->type == CONNECTION_STDINOUT) {
308 c->fd = fileno(stdin);
309
310 #ifdef _WIN32
311 /* for win32 set stdin/stdout to binary mode */
312 if (_setmode(_fileno(stdout), _O_BINARY) < 0)
313 LOG_WARNING("cannot change stdout mode to binary");
314 if (_setmode(_fileno(stdin), _O_BINARY) < 0)
315 LOG_WARNING("cannot change stdin mode to binary");
316 if (_setmode(_fileno(stderr), _O_BINARY) < 0)
317 LOG_WARNING("cannot change stderr mode to binary");
318 #else
319 socket_nonblock(c->fd);
320 #endif
321 } else if (c->type == CONNECTION_PIPE) {
322 #ifdef _WIN32
323 /* we currenty do not support named pipes under win32
324 * so exit openocd for now */
325 LOG_ERROR("Named pipes currently not supported under this os");
326 free_service(c);
327 return ERROR_FAIL;
328 #else
329 /* Pipe we're reading from */
330 c->fd = open(c->port, O_RDONLY | O_NONBLOCK);
331 if (c->fd == -1) {
332 LOG_ERROR("could not open %s", c->port);
333 free_service(c);
334 return ERROR_FAIL;
335 }
336 #endif
337 }
338
339 /* add to the end of linked list */
340 for (p = &services; *p; p = &(*p)->next)
341 ;
342 *p = c;
343
344 return ERROR_OK;
345 }
346
347 static int remove_services(void)
348 {
349 struct service *c = services;
350
351 /* loop service */
352 while (c) {
353 struct service *next = c->next;
354
355 if (c->name)
356 free(c->name);
357
358 if (c->type == CONNECTION_PIPE) {
359 if (c->fd != -1)
360 close(c->fd);
361 }
362 if (c->port)
363 free(c->port);
364
365 if (c->priv)
366 free(c->priv);
367
368 /* delete service */
369 free(c);
370
371 /* remember the last service for unlinking */
372 c = next;
373 }
374
375 services = NULL;
376
377 return ERROR_OK;
378 }
379
380 int server_loop(struct command_context *command_context)
381 {
382 struct service *service;
383
384 bool poll_ok = true;
385
386 /* used in select() */
387 fd_set read_fds;
388 int fd_max;
389
390 /* used in accept() */
391 int retval;
392
393 #ifndef _WIN32
394 if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
395 LOG_ERROR("couldn't set SIGPIPE to SIG_IGN");
396 #endif
397
398 while (!shutdown_openocd) {
399 /* monitor sockets for activity */
400 fd_max = 0;
401 FD_ZERO(&read_fds);
402
403 /* add service and connection fds to read_fds */
404 for (service = services; service; service = service->next) {
405 if (service->fd != -1) {
406 /* listen for new connections */
407 FD_SET(service->fd, &read_fds);
408
409 if (service->fd > fd_max)
410 fd_max = service->fd;
411 }
412
413 if (service->connections) {
414 struct connection *c;
415
416 for (c = service->connections; c; c = c->next) {
417 /* check for activity on the connection */
418 FD_SET(c->fd, &read_fds);
419 if (c->fd > fd_max)
420 fd_max = c->fd;
421 }
422 }
423 }
424
425 struct timeval tv;
426 tv.tv_sec = 0;
427 if (poll_ok) {
428 /* we're just polling this iteration, this is faster on embedded
429 * hosts */
430 tv.tv_usec = 0;
431 retval = socket_select(fd_max + 1, &read_fds, NULL, NULL, &tv);
432 } else {
433 /* Every 100ms, can be changed with "poll_period" command */
434 tv.tv_usec = polling_period * 1000;
435 /* Only while we're sleeping we'll let others run */
436 openocd_sleep_prelude();
437 kept_alive();
438 retval = socket_select(fd_max + 1, &read_fds, NULL, NULL, &tv);
439 openocd_sleep_postlude();
440 }
441
442 if (retval == -1) {
443 #ifdef _WIN32
444
445 errno = WSAGetLastError();
446
447 if (errno == WSAEINTR)
448 FD_ZERO(&read_fds);
449 else {
450 LOG_ERROR("error during select: %s", strerror(errno));
451 return ERROR_FAIL;
452 }
453 #else
454
455 if (errno == EINTR)
456 FD_ZERO(&read_fds);
457 else {
458 LOG_ERROR("error during select: %s", strerror(errno));
459 return ERROR_FAIL;
460 }
461 #endif
462 }
463
464 if (retval == 0) {
465 /* We only execute these callbacks when there was nothing to do or we timed
466 *out */
467 target_call_timer_callbacks();
468 process_jim_events(command_context);
469
470 FD_ZERO(&read_fds); /* eCos leaves read_fds unchanged in this case! */
471
472 /* We timed out/there was nothing to do, timeout rather than poll next time
473 **/
474 poll_ok = false;
475 } else {
476 /* There was something to do, next time we'll just poll */
477 poll_ok = true;
478 }
479
480 /* This is a simple back-off algorithm where we immediately
481 * re-poll if we did something this time around.
482 *
483 * This greatly improves performance of DCC.
484 */
485 poll_ok = poll_ok || target_got_message();
486
487 for (service = services; service; service = service->next) {
488 /* handle new connections on listeners */
489 if ((service->fd != -1)
490 && (FD_ISSET(service->fd, &read_fds))) {
491 if (service->max_connections != 0)
492 add_connection(service, command_context);
493 else {
494 if (service->type == CONNECTION_TCP) {
495 struct sockaddr_in sin;
496 socklen_t address_size = sizeof(sin);
497 int tmp_fd;
498 tmp_fd = accept(service->fd,
499 (struct sockaddr *)&service->sin,
500 &address_size);
501 close_socket(tmp_fd);
502 }
503 LOG_INFO(
504 "rejected '%s' connection, no more connections allowed",
505 service->name);
506 }
507 }
508
509 /* handle activity on connections */
510 if (service->connections) {
511 struct connection *c;
512
513 for (c = service->connections; c; ) {
514 if ((FD_ISSET(c->fd, &read_fds)) || c->input_pending) {
515 retval = service->input(c);
516 if (retval != ERROR_OK) {
517 struct connection *next = c->next;
518 if (service->type == CONNECTION_PIPE ||
519 service->type == CONNECTION_STDINOUT) {
520 /* if connection uses a pipe then
521 * shutdown openocd on error */
522 shutdown_openocd = 1;
523 }
524 remove_connection(service, c);
525 LOG_INFO("dropped '%s' connection",
526 service->name);
527 c = next;
528 continue;
529 }
530 }
531 c = c->next;
532 }
533 }
534 }
535
536 #ifdef _WIN32
537 MSG msg;
538 while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
539 if (msg.message == WM_QUIT)
540 shutdown_openocd = 1;
541 }
542 #endif
543 }
544
545 return shutdown_openocd != 2 ? ERROR_OK : ERROR_FAIL;
546 }
547
548 #ifdef _WIN32
549 BOOL WINAPI ControlHandler(DWORD dwCtrlType)
550 {
551 shutdown_openocd = 1;
552 return TRUE;
553 }
554 #endif
555
556 void sig_handler(int sig)
557 {
558 /* store only first signal that hits us */
559 if (!last_signal)
560 last_signal = sig;
561 shutdown_openocd = 1;
562 }
563
564 int server_preinit(void)
565 {
566 /* this currently only calls WSAStartup on native win32 systems
567 * before any socket operations are performed.
568 * This is an issue if you call init in your config script */
569
570 #ifdef _WIN32
571 WORD wVersionRequested;
572 WSADATA wsaData;
573
574 wVersionRequested = MAKEWORD(2, 2);
575
576 if (WSAStartup(wVersionRequested, &wsaData) != 0) {
577 LOG_ERROR("Failed to Open Winsock");
578 return ERROR_FAIL;
579 }
580
581 /* register ctrl-c handler */
582 SetConsoleCtrlHandler(ControlHandler, TRUE);
583
584 signal(SIGBREAK, sig_handler);
585 #endif
586 signal(SIGINT, sig_handler);
587 signal(SIGTERM, sig_handler);
588 signal(SIGABRT, sig_handler);
589
590 return ERROR_OK;
591 }
592
593 int server_init(struct command_context *cmd_ctx)
594 {
595 int ret = tcl_init();
596
597 if (ret != ERROR_OK)
598 return ret;
599
600 ret = telnet_init("Open On-Chip Debugger");
601
602 if (ret != ERROR_OK) {
603 remove_services();
604 return ret;
605 }
606
607 return ERROR_OK;
608 }
609
610 int server_quit(void)
611 {
612 remove_services();
613 target_quit();
614
615 #ifdef _WIN32
616 WSACleanup();
617 SetConsoleCtrlHandler(ControlHandler, FALSE);
618
619 return ERROR_OK;
620 #endif
621
622 /* return signal number so we can kill ourselves */
623 return last_signal;
624 }
625
626 void exit_on_signal(int sig)
627 {
628 #ifndef _WIN32
629 /* bring back default system handler and kill yourself */
630 signal(sig, SIG_DFL);
631 kill(getpid(), sig);
632 #endif
633 }
634
635 int connection_write(struct connection *connection, const void *data, int len)
636 {
637 if (len == 0) {
638 /* successful no-op. Sockets and pipes behave differently here... */
639 return 0;
640 }
641 if (connection->service->type == CONNECTION_TCP)
642 return write_socket(connection->fd_out, data, len);
643 else
644 return write(connection->fd_out, data, len);
645 }
646
647 int connection_read(struct connection *connection, void *data, int len)
648 {
649 if (connection->service->type == CONNECTION_TCP)
650 return read_socket(connection->fd, data, len);
651 else
652 return read(connection->fd, data, len);
653 }
654
655 /* tell the server we want to shut down */
656 COMMAND_HANDLER(handle_shutdown_command)
657 {
658 LOG_USER("shutdown command invoked");
659
660 shutdown_openocd = 1;
661
662 if (CMD_ARGC == 1) {
663 if (!strcmp(CMD_ARGV[0], "error")) {
664 shutdown_openocd = 2;
665 return ERROR_FAIL;
666 }
667 }
668
669 return ERROR_COMMAND_CLOSE_CONNECTION;
670 }
671
672 COMMAND_HANDLER(handle_poll_period_command)
673 {
674 if (CMD_ARGC == 0)
675 LOG_WARNING("You need to set a period value");
676 else
677 COMMAND_PARSE_NUMBER(int, CMD_ARGV[0], polling_period);
678
679 LOG_INFO("set servers polling period to %ums", polling_period);
680
681 return ERROR_OK;
682 }
683
684 COMMAND_HANDLER(handle_bindto_command)
685 {
686 switch (CMD_ARGC) {
687 case 0:
688 command_print(CMD_CTX, "bindto name: %s", bindto_name);
689 break;
690 case 1:
691 free(bindto_name);
692 bindto_name = strdup(CMD_ARGV[0]);
693 break;
694 default:
695 return ERROR_COMMAND_SYNTAX_ERROR;
696 }
697 return ERROR_OK;
698 }
699
700 static const struct command_registration server_command_handlers[] = {
701 {
702 .name = "shutdown",
703 .handler = &handle_shutdown_command,
704 .mode = COMMAND_ANY,
705 .usage = "",
706 .help = "shut the server down",
707 },
708 {
709 .name = "poll_period",
710 .handler = &handle_poll_period_command,
711 .mode = COMMAND_ANY,
712 .usage = "",
713 .help = "set the servers polling period",
714 },
715 {
716 .name = "bindto",
717 .handler = &handle_bindto_command,
718 .mode = COMMAND_ANY,
719 .usage = "[name]",
720 .help = "Specify address by name on which to listen for "
721 "incoming TCP/IP connections",
722 },
723 COMMAND_REGISTRATION_DONE
724 };
725
726 int server_register_commands(struct command_context *cmd_ctx)
727 {
728 int retval = telnet_register_commands(cmd_ctx);
729 if (ERROR_OK != retval)
730 return retval;
731
732 retval = tcl_register_commands(cmd_ctx);
733 if (ERROR_OK != retval)
734 return retval;
735
736 retval = jsp_register_commands(cmd_ctx);
737 if (ERROR_OK != retval)
738 return retval;
739
740 return register_commands(cmd_ctx, NULL, server_command_handlers);
741 }
742
743 COMMAND_HELPER(server_port_command, unsigned short *out)
744 {
745 switch (CMD_ARGC) {
746 case 0:
747 command_print(CMD_CTX, "%d", *out);
748 break;
749 case 1:
750 {
751 uint16_t port;
752 COMMAND_PARSE_NUMBER(u16, CMD_ARGV[0], port);
753 *out = port;
754 break;
755 }
756 default:
757 return ERROR_COMMAND_SYNTAX_ERROR;
758 }
759 return ERROR_OK;
760 }
761
762 COMMAND_HELPER(server_pipe_command, char **out)
763 {
764 switch (CMD_ARGC) {
765 case 0:
766 command_print(CMD_CTX, "%s", *out);
767 break;
768 case 1:
769 {
770 if (CMD_CTX->mode == COMMAND_EXEC) {
771 LOG_WARNING("unable to change server port after init");
772 return ERROR_COMMAND_ARGUMENT_INVALID;
773 }
774 free(*out);
775 *out = strdup(CMD_ARGV[0]);
776 break;
777 }
778 default:
779 return ERROR_COMMAND_SYNTAX_ERROR;
780 }
781 return ERROR_OK;
782 }

Linking to existing account procedure

If you already have an account and want to add another login method you MUST first sign in with your existing account and then change URL to read https://review.openocd.org/login/?link to get to this page again but this time it'll work for linking. Thank you.

SSH host keys fingerprints

1024 SHA256:YKx8b7u5ZWdcbp7/4AeXNaqElP49m6QrwfXaqQGJAOk gerrit-code-review@openocd.zylin.com (DSA)
384 SHA256:jHIbSQa4REvwCFG4cq5LBlBLxmxSqelQPem/EXIrxjk gerrit-code-review@openocd.org (ECDSA)
521 SHA256:UAOPYkU9Fjtcao0Ul/Rrlnj/OsQvt+pgdYSZ4jOYdgs gerrit-code-review@openocd.org (ECDSA)
256 SHA256:A13M5QlnozFOvTllybRZH6vm7iSt0XLxbA48yfc2yfY gerrit-code-review@openocd.org (ECDSA)
256 SHA256:spYMBqEYoAOtK7yZBrcwE8ZpYt6b68Cfh9yEVetvbXg gerrit-code-review@openocd.org (ED25519)
+--[ED25519 256]--+
|=..              |
|+o..   .         |
|*.o   . .        |
|+B . . .         |
|Bo. = o S        |
|Oo.+ + =         |
|oB=.* = . o      |
| =+=.+   + E     |
|. .=o   . o      |
+----[SHA256]-----+
2048 SHA256:0Onrb7/PHjpo6iVZ7xQX2riKN83FJ3KGU0TvI0TaFG4 gerrit-code-review@openocd.zylin.com (RSA)