gdb_server, target: Add target_address_bits()
[openocd.git] / src / server / telnet_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 "telnet_server.h"
30 #include <target/target_request.h>
31 #include <helper/configuration.h>
32
33 static char *telnet_port;
34
35 static char *negotiate =
36 "\xFF\xFB\x03" /* IAC WILL Suppress Go Ahead */
37 "\xFF\xFB\x01" /* IAC WILL Echo */
38 "\xFF\xFD\x03" /* IAC DO Suppress Go Ahead */
39 "\xFF\xFE\x01"; /* IAC DON'T Echo */
40
41 #define CTRL(c) (c - '@')
42 #define TELNET_HISTORY ".openocd_history"
43
44 /* The only way we can detect that the socket is closed is the first time
45 * we write to it, we will fail. Subsequent write operations will
46 * succeed. Shudder!
47 */
48 static int telnet_write(struct connection *connection, const void *data,
49 int len)
50 {
51 struct telnet_connection *t_con = connection->priv;
52 if (t_con->closed)
53 return ERROR_SERVER_REMOTE_CLOSED;
54
55 if (connection_write(connection, data, len) == len)
56 return ERROR_OK;
57 t_con->closed = true;
58 return ERROR_SERVER_REMOTE_CLOSED;
59 }
60
61 static int telnet_prompt(struct connection *connection)
62 {
63 struct telnet_connection *t_con = connection->priv;
64
65 return telnet_write(connection, t_con->prompt, strlen(t_con->prompt));
66 }
67
68 static int telnet_outputline(struct connection *connection, const char *line)
69 {
70 int len;
71
72 /* process lines in buffer */
73 while (*line) {
74 char *line_end = strchr(line, '\n');
75
76 if (line_end)
77 len = line_end-line;
78 else
79 len = strlen(line);
80
81 telnet_write(connection, line, len);
82 if (line_end) {
83 telnet_write(connection, "\r\n", 2);
84 line += len + 1;
85 } else
86 line += len;
87 }
88
89 return ERROR_OK;
90 }
91
92 static int telnet_output(struct command_context *cmd_ctx, const char *line)
93 {
94 struct connection *connection = cmd_ctx->output_handler_priv;
95
96 return telnet_outputline(connection, line);
97 }
98
99 static void telnet_log_callback(void *priv, const char *file, unsigned line,
100 const char *function, const char *string)
101 {
102 struct connection *connection = priv;
103 struct telnet_connection *t_con = connection->priv;
104 size_t i;
105 size_t tmp;
106
107 /* If the prompt is not visible, simply output the message. */
108 if (!t_con->prompt_visible) {
109 telnet_outputline(connection, string);
110 return;
111 }
112
113 /* Clear the command line. */
114 tmp = strlen(t_con->prompt) + t_con->line_size;
115
116 for (i = 0; i < tmp; i += 16)
117 telnet_write(connection, "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b",
118 MIN(tmp - i, 16));
119
120 for (i = 0; i < tmp; i += 16)
121 telnet_write(connection, " ", MIN(tmp - i, 16));
122
123 for (i = 0; i < tmp; i += 16)
124 telnet_write(connection, "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b",
125 MIN(tmp - i, 16));
126
127 telnet_outputline(connection, string);
128
129 /* Put the command line to its previous state. */
130 telnet_prompt(connection);
131 telnet_write(connection, t_con->line, t_con->line_size);
132
133 for (i = t_con->line_cursor; i < t_con->line_size; i++)
134 telnet_write(connection, "\b", 1);
135 }
136
137 static void telnet_load_history(struct telnet_connection *t_con)
138 {
139 FILE *histfp;
140 char buffer[TELNET_BUFFER_SIZE];
141 int i = 0;
142
143 char *history = get_home_dir(TELNET_HISTORY);
144
145 if (history == NULL) {
146 LOG_INFO("unable to get user home directory, telnet history will be disabled");
147 return;
148 }
149
150 histfp = fopen(history, "rb");
151
152 if (histfp) {
153
154 while (fgets(buffer, sizeof(buffer), histfp) != NULL) {
155
156 char *p = strchr(buffer, '\n');
157 if (p)
158 *p = '\0';
159 if (buffer[0] && i < TELNET_LINE_HISTORY_SIZE)
160 t_con->history[i++] = strdup(buffer);
161 }
162
163 t_con->next_history = i;
164 t_con->next_history %= TELNET_LINE_HISTORY_SIZE;
165 /* try to set to last entry - 1, that way we skip over any exit/shutdown cmds */
166 t_con->current_history = t_con->next_history > 0 ? i - 1 : 0;
167 fclose(histfp);
168 }
169
170 free(history);
171 }
172
173 static void telnet_save_history(struct telnet_connection *t_con)
174 {
175 FILE *histfp;
176 int i;
177 int num;
178
179 char *history = get_home_dir(TELNET_HISTORY);
180
181 if (history == NULL) {
182 LOG_INFO("unable to get user home directory, telnet history will be disabled");
183 return;
184 }
185
186 histfp = fopen(history, "wb");
187
188 if (histfp) {
189
190 num = TELNET_LINE_HISTORY_SIZE;
191 i = t_con->current_history + 1;
192 i %= TELNET_LINE_HISTORY_SIZE;
193
194 while (t_con->history[i] == NULL && num > 0) {
195 i++;
196 i %= TELNET_LINE_HISTORY_SIZE;
197 num--;
198 }
199
200 if (num > 0) {
201 for (; num > 0; num--) {
202 fprintf(histfp, "%s\n", t_con->history[i]);
203 i++;
204 i %= TELNET_LINE_HISTORY_SIZE;
205 }
206 }
207 fclose(histfp);
208 }
209
210 free(history);
211 }
212
213 static int telnet_new_connection(struct connection *connection)
214 {
215 struct telnet_connection *telnet_connection;
216 struct telnet_service *telnet_service = connection->service->priv;
217 int i;
218
219 telnet_connection = malloc(sizeof(struct telnet_connection));
220
221 if (!telnet_connection) {
222 LOG_ERROR("Failed to allocate telnet connection.");
223 return ERROR_FAIL;
224 }
225
226 connection->priv = telnet_connection;
227
228 /* initialize telnet connection information */
229 telnet_connection->closed = false;
230 telnet_connection->line_size = 0;
231 telnet_connection->line_cursor = 0;
232 telnet_connection->prompt = strdup("> ");
233 telnet_connection->prompt_visible = true;
234 telnet_connection->state = TELNET_STATE_DATA;
235
236 /* output goes through telnet connection */
237 command_set_output_handler(connection->cmd_ctx, telnet_output, connection);
238
239 /* negotiate telnet options */
240 telnet_write(connection, negotiate, strlen(negotiate));
241
242 /* print connection banner */
243 if (telnet_service->banner) {
244 telnet_write(connection, telnet_service->banner, strlen(telnet_service->banner));
245 telnet_write(connection, "\r\n", 2);
246 }
247
248 /* the prompt is always placed at the line beginning */
249 telnet_write(connection, "\r", 1);
250 telnet_prompt(connection);
251
252 /* initialize history */
253 for (i = 0; i < TELNET_LINE_HISTORY_SIZE; i++)
254 telnet_connection->history[i] = NULL;
255 telnet_connection->next_history = 0;
256 telnet_connection->current_history = 0;
257 telnet_load_history(telnet_connection);
258
259 log_add_callback(telnet_log_callback, connection);
260
261 return ERROR_OK;
262 }
263
264 static void telnet_clear_line(struct connection *connection,
265 struct telnet_connection *t_con)
266 {
267 /* move to end of line */
268 if (t_con->line_cursor < t_con->line_size)
269 telnet_write(connection,
270 t_con->line + t_con->line_cursor,
271 t_con->line_size - t_con->line_cursor);
272
273 /* backspace, overwrite with space, backspace */
274 while (t_con->line_size > 0) {
275 telnet_write(connection, "\b \b", 3);
276 t_con->line_size--;
277 }
278 t_con->line_cursor = 0;
279 }
280
281 static void telnet_history_go(struct connection *connection, int idx)
282 {
283 struct telnet_connection *t_con = connection->priv;
284
285 if (t_con->history[idx]) {
286 telnet_clear_line(connection, t_con);
287 t_con->line_size = strlen(t_con->history[idx]);
288 t_con->line_cursor = t_con->line_size;
289 memcpy(t_con->line, t_con->history[idx], t_con->line_size);
290 telnet_write(connection, t_con->line, t_con->line_size);
291 t_con->current_history = idx;
292 }
293 t_con->state = TELNET_STATE_DATA;
294 }
295
296 static void telnet_history_up(struct connection *connection)
297 {
298 struct telnet_connection *t_con = connection->priv;
299
300 size_t last_history = (t_con->current_history > 0) ?
301 t_con->current_history - 1 :
302 TELNET_LINE_HISTORY_SIZE-1;
303 telnet_history_go(connection, last_history);
304 }
305
306 static void telnet_history_down(struct connection *connection)
307 {
308 struct telnet_connection *t_con = connection->priv;
309 size_t next_history;
310
311 next_history = (t_con->current_history + 1) % TELNET_LINE_HISTORY_SIZE;
312 telnet_history_go(connection, next_history);
313 }
314
315 static void telnet_move_cursor(struct connection *connection, size_t pos)
316 {
317 struct telnet_connection *tc;
318 size_t tmp;
319
320 tc = connection->priv;
321
322 if (pos < tc->line_cursor) {
323 tmp = tc->line_cursor - pos;
324
325 for (size_t i = 0; i < tmp; i += 16)
326 telnet_write(connection, "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b",
327 MIN(tmp - i, 16));
328 } else {
329 tmp = pos - tc->line_cursor;
330
331 for (size_t i = 0; i < tmp; i += 16)
332 telnet_write(connection, tc->line + tc->line_cursor + i,
333 MIN(tmp - i, 16));
334 }
335
336 tc->line_cursor = pos;
337 }
338
339 static int telnet_input(struct connection *connection)
340 {
341 int bytes_read;
342 unsigned char buffer[TELNET_BUFFER_SIZE];
343 unsigned char *buf_p;
344 struct telnet_connection *t_con = connection->priv;
345 struct command_context *command_context = connection->cmd_ctx;
346
347 bytes_read = connection_read(connection, buffer, TELNET_BUFFER_SIZE);
348
349 if (bytes_read == 0)
350 return ERROR_SERVER_REMOTE_CLOSED;
351 else if (bytes_read == -1) {
352 LOG_ERROR("error during read: %s", strerror(errno));
353 return ERROR_SERVER_REMOTE_CLOSED;
354 }
355
356 buf_p = buffer;
357 while (bytes_read) {
358 switch (t_con->state) {
359 case TELNET_STATE_DATA:
360 if (*buf_p == 0xff)
361 t_con->state = TELNET_STATE_IAC;
362 else {
363 if (isprint(*buf_p)) { /* printable character */
364 /* watch buffer size leaving one spare character for
365 * string null termination */
366 if (t_con->line_size == TELNET_LINE_MAX_SIZE-1) {
367 /* output audible bell if buffer is full
368 * "\a" does not work, at least on windows */
369 telnet_write(connection, "\x07", 1);
370 } else if (t_con->line_cursor == t_con->line_size) {
371 telnet_write(connection, buf_p, 1);
372 t_con->line[t_con->line_size++] = *buf_p;
373 t_con->line_cursor++;
374 } else {
375 size_t i;
376 memmove(t_con->line + t_con->line_cursor + 1,
377 t_con->line + t_con->line_cursor,
378 t_con->line_size - t_con->line_cursor);
379 t_con->line[t_con->line_cursor] = *buf_p;
380 t_con->line_size++;
381 telnet_write(connection,
382 t_con->line + t_con->line_cursor,
383 t_con->line_size - t_con->line_cursor);
384 t_con->line_cursor++;
385 for (i = t_con->line_cursor; i < t_con->line_size; i++)
386 telnet_write(connection, "\b", 1);
387 }
388 } else { /* non-printable */
389 if (*buf_p == 0x1b) { /* escape */
390 t_con->state = TELNET_STATE_ESCAPE;
391 t_con->last_escape = '\x00';
392 } else if ((*buf_p == 0xd) || (*buf_p == 0xa)) { /* CR/LF */
393 int retval;
394
395 /* skip over combinations with CR/LF and NUL characters */
396 if ((bytes_read > 1) && ((*(buf_p + 1) == 0xa) ||
397 (*(buf_p + 1) == 0xd))) {
398 buf_p++;
399 bytes_read--;
400 }
401 if ((bytes_read > 1) && (*(buf_p + 1) == 0)) {
402 buf_p++;
403 bytes_read--;
404 }
405 t_con->line[t_con->line_size] = 0;
406
407 telnet_write(connection, "\r\n\x00", 3);
408
409 if (strcmp(t_con->line, "history") == 0) {
410 size_t i;
411 for (i = 1; i < TELNET_LINE_HISTORY_SIZE; i++) {
412 /* the t_con->next_history line contains empty string
413 * (unless NULL), thus it is not printed */
414 char *history_line = t_con->history[(t_con->
415 next_history + i) %
416 TELNET_LINE_HISTORY_SIZE];
417 if (history_line) {
418 telnet_write(connection, history_line,
419 strlen(history_line));
420 telnet_write(connection, "\r\n\x00", 3);
421 }
422 }
423 t_con->line_size = 0;
424 t_con->line_cursor = 0;
425 continue;
426 }
427
428 /* save only non-blank not repeating lines in the history */
429 char *prev_line = t_con->history[(t_con->current_history > 0) ?
430 t_con->current_history - 1 : TELNET_LINE_HISTORY_SIZE-1];
431 if (*t_con->line && (prev_line == NULL ||
432 strcmp(t_con->line, prev_line))) {
433 /* if the history slot is already taken, free it */
434 if (t_con->history[t_con->next_history])
435 free(t_con->history[t_con->next_history]);
436
437 /* add line to history */
438 t_con->history[t_con->next_history] = strdup(t_con->line);
439
440 /* wrap history at TELNET_LINE_HISTORY_SIZE */
441 t_con->next_history = (t_con->next_history + 1) %
442 TELNET_LINE_HISTORY_SIZE;
443
444 /* current history line starts at the new entry */
445 t_con->current_history =
446 t_con->next_history;
447
448 if (t_con->history[t_con->current_history])
449 free(t_con->history[t_con->current_history]);
450 t_con->history[t_con->current_history] = strdup("");
451 }
452
453 t_con->line_size = 0;
454
455 /* to suppress prompt in log callback during command execution */
456 t_con->prompt_visible = false;
457
458 if (strcmp(t_con->line, "shutdown") == 0)
459 telnet_save_history(t_con);
460
461 retval = command_run_line(command_context, t_con->line);
462
463 t_con->line_cursor = 0;
464 t_con->prompt_visible = true;
465
466 if (retval == ERROR_COMMAND_CLOSE_CONNECTION)
467 return ERROR_SERVER_REMOTE_CLOSED;
468
469 /* the prompt is always * placed at the line beginning */
470 telnet_write(connection, "\r", 1);
471
472 retval = telnet_prompt(connection);
473 if (retval == ERROR_SERVER_REMOTE_CLOSED)
474 return ERROR_SERVER_REMOTE_CLOSED;
475
476 } else if ((*buf_p == 0x7f) || (*buf_p == 0x8)) { /* delete character */
477 if (t_con->line_cursor > 0) {
478 if (t_con->line_cursor != t_con->line_size) {
479 size_t i;
480 telnet_write(connection, "\b", 1);
481 t_con->line_cursor--;
482 t_con->line_size--;
483 memmove(t_con->line + t_con->line_cursor,
484 t_con->line + t_con->line_cursor + 1,
485 t_con->line_size -
486 t_con->line_cursor);
487
488 telnet_write(connection,
489 t_con->line + t_con->line_cursor,
490 t_con->line_size -
491 t_con->line_cursor);
492 telnet_write(connection, " \b", 2);
493 for (i = t_con->line_cursor; i < t_con->line_size; i++)
494 telnet_write(connection, "\b", 1);
495 } else {
496 t_con->line_size--;
497 t_con->line_cursor--;
498 /* back space: move the 'printer' head one char
499 * back, overwrite with space, move back again */
500 telnet_write(connection, "\b \b", 3);
501 }
502 }
503 } else if (*buf_p == 0x15) /* clear line */
504 telnet_clear_line(connection, t_con);
505 else if (*buf_p == CTRL('B')) { /* cursor left */
506 if (t_con->line_cursor > 0) {
507 telnet_write(connection, "\b", 1);
508 t_con->line_cursor--;
509 }
510 t_con->state = TELNET_STATE_DATA;
511 } else if (*buf_p == CTRL('F')) { /* cursor right */
512 if (t_con->line_cursor < t_con->line_size)
513 telnet_write(connection, t_con->line + t_con->line_cursor++, 1);
514 t_con->state = TELNET_STATE_DATA;
515 } else if (*buf_p == CTRL('P')) /* cursor up */
516 telnet_history_up(connection);
517 else if (*buf_p == CTRL('N')) /* cursor down */
518 telnet_history_down(connection);
519 else if (*buf_p == CTRL('A'))
520 telnet_move_cursor(connection, 0);
521 else if (*buf_p == CTRL('E'))
522 telnet_move_cursor(connection, t_con->line_size);
523 else
524 LOG_DEBUG("unhandled nonprintable: %2.2x", *buf_p);
525 }
526 }
527 break;
528 case TELNET_STATE_IAC:
529 switch (*buf_p) {
530 case 0xfe:
531 t_con->state = TELNET_STATE_DONT;
532 break;
533 case 0xfd:
534 t_con->state = TELNET_STATE_DO;
535 break;
536 case 0xfc:
537 t_con->state = TELNET_STATE_WONT;
538 break;
539 case 0xfb:
540 t_con->state = TELNET_STATE_WILL;
541 break;
542 }
543 break;
544 case TELNET_STATE_SB:
545 break;
546 case TELNET_STATE_SE:
547 break;
548 case TELNET_STATE_WILL:
549 case TELNET_STATE_WONT:
550 case TELNET_STATE_DO:
551 case TELNET_STATE_DONT:
552 t_con->state = TELNET_STATE_DATA;
553 break;
554 case TELNET_STATE_ESCAPE:
555 if (t_con->last_escape == '[') {
556 if (*buf_p == 'D') { /* cursor left */
557 if (t_con->line_cursor > 0) {
558 telnet_write(connection, "\b", 1);
559 t_con->line_cursor--;
560 }
561 t_con->state = TELNET_STATE_DATA;
562 } else if (*buf_p == 'C') { /* cursor right */
563 if (t_con->line_cursor < t_con->line_size)
564 telnet_write(connection,
565 t_con->line + t_con->line_cursor++, 1);
566 t_con->state = TELNET_STATE_DATA;
567 } else if (*buf_p == 'A') { /* cursor up */
568 telnet_history_up(connection);
569 } else if (*buf_p == 'B') { /* cursor down */
570 telnet_history_down(connection);
571 } else if (*buf_p == '3')
572 t_con->last_escape = *buf_p;
573 else
574 t_con->state = TELNET_STATE_DATA;
575 } else if (t_con->last_escape == '3') {
576 /* Remove character */
577 if (*buf_p == '~') {
578 if (t_con->line_cursor < t_con->line_size) {
579 size_t i;
580 t_con->line_size--;
581 /* remove char from line buffer */
582 memmove(t_con->line + t_con->line_cursor,
583 t_con->line + t_con->line_cursor + 1,
584 t_con->line_size - t_con->line_cursor);
585
586 /* print remainder of buffer */
587 telnet_write(connection, t_con->line + t_con->line_cursor,
588 t_con->line_size - t_con->line_cursor);
589 /* overwrite last char with whitespace */
590 telnet_write(connection, " \b", 2);
591
592 /* move back to cursor position*/
593 for (i = t_con->line_cursor; i < t_con->line_size; i++)
594 telnet_write(connection, "\b", 1);
595 }
596
597 t_con->state = TELNET_STATE_DATA;
598 } else
599 t_con->state = TELNET_STATE_DATA;
600 } else if (t_con->last_escape == '\x00') {
601 if (*buf_p == '[')
602 t_con->last_escape = *buf_p;
603 else
604 t_con->state = TELNET_STATE_DATA;
605 } else {
606 LOG_ERROR("BUG: unexpected value in t_con->last_escape");
607 t_con->state = TELNET_STATE_DATA;
608 }
609
610 break;
611 default:
612 LOG_ERROR("unknown telnet state");
613 return ERROR_FAIL;
614 }
615
616 bytes_read--;
617 buf_p++;
618 }
619
620 return ERROR_OK;
621 }
622
623 static int telnet_connection_closed(struct connection *connection)
624 {
625 struct telnet_connection *t_con = connection->priv;
626 int i;
627
628 log_remove_callback(telnet_log_callback, connection);
629
630 if (t_con->prompt) {
631 free(t_con->prompt);
632 t_con->prompt = NULL;
633 }
634
635 /* save telnet history */
636 telnet_save_history(t_con);
637
638 for (i = 0; i < TELNET_LINE_HISTORY_SIZE; i++) {
639 if (t_con->history[i]) {
640 free(t_con->history[i]);
641 t_con->history[i] = NULL;
642 }
643 }
644
645 /* if this connection registered a debug-message receiver delete it */
646 delete_debug_msg_receiver(connection->cmd_ctx, NULL);
647
648 if (connection->priv) {
649 free(connection->priv);
650 connection->priv = NULL;
651 } else
652 LOG_ERROR("BUG: connection->priv == NULL");
653
654 return ERROR_OK;
655 }
656
657 int telnet_init(char *banner)
658 {
659 if (strcmp(telnet_port, "disabled") == 0) {
660 LOG_INFO("telnet server disabled");
661 return ERROR_OK;
662 }
663
664 struct telnet_service *telnet_service =
665 malloc(sizeof(struct telnet_service));
666
667 if (!telnet_service) {
668 LOG_ERROR("Failed to allocate telnet service.");
669 return ERROR_FAIL;
670 }
671
672 telnet_service->banner = banner;
673
674 int ret = add_service("telnet", telnet_port, CONNECTION_LIMIT_UNLIMITED,
675 telnet_new_connection, telnet_input, telnet_connection_closed,
676 telnet_service);
677
678 if (ret != ERROR_OK) {
679 free(telnet_service);
680 return ret;
681 }
682
683 return ERROR_OK;
684 }
685
686 /* daemon configuration command telnet_port */
687 COMMAND_HANDLER(handle_telnet_port_command)
688 {
689 return CALL_COMMAND_HANDLER(server_pipe_command, &telnet_port);
690 }
691
692 COMMAND_HANDLER(handle_exit_command)
693 {
694 return ERROR_COMMAND_CLOSE_CONNECTION;
695 }
696
697 static const struct command_registration telnet_command_handlers[] = {
698 {
699 .name = "exit",
700 .handler = handle_exit_command,
701 .mode = COMMAND_EXEC,
702 .usage = "",
703 .help = "exit telnet session",
704 },
705 {
706 .name = "telnet_port",
707 .handler = handle_telnet_port_command,
708 .mode = COMMAND_ANY,
709 .help = "Specify port on which to listen "
710 "for incoming telnet connections. "
711 "Read help on 'gdb_port'.",
712 .usage = "[port_num]",
713 },
714 COMMAND_REGISTRATION_DONE
715 };
716
717 int telnet_register_commands(struct command_context *cmd_ctx)
718 {
719 telnet_port = strdup("4444");
720 return register_commands(cmd_ctx, NULL, telnet_command_handlers);
721 }
722
723 void telnet_service_free(void)
724 {
725 free(telnet_port);
726 }

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)