Created a shiny new abstraction for the socket handling. Has many
[u/mdw/putty] / plink.c
CommitLineData
12dc4ec0 1/*
2 * PLink - a command-line (stdin/stdout) variant of PuTTY.
3 */
4
4d331a77 5#ifndef AUTO_WINSOCK
12dc4ec0 6#include <winsock2.h>
4d331a77 7#endif
12dc4ec0 8#include <windows.h>
9#include <stdio.h>
10#include <stdarg.h>
11
12#define PUTTY_DO_GLOBALS /* actually _define_ globals */
13#include "putty.h"
a9422f39 14#include "storage.h"
8df7a775 15#include "tree234.h"
12dc4ec0 16
17void fatalbox (char *p, ...) {
18 va_list ap;
19 fprintf(stderr, "FATAL ERROR: ", p);
20 va_start(ap, p);
21 vfprintf(stderr, p, ap);
22 va_end(ap);
23 fputc('\n', stderr);
24 WSACleanup();
25 exit(1);
26}
8d5de777 27void connection_fatal (char *p, ...) {
28 va_list ap;
29 fprintf(stderr, "FATAL ERROR: ", p);
30 va_start(ap, p);
31 vfprintf(stderr, p, ap);
32 va_end(ap);
33 fputc('\n', stderr);
34 WSACleanup();
35 exit(1);
36}
12dc4ec0 37
d8426c54 38static char *password = NULL;
39
a9422f39 40void logevent(char *string) { }
41
42void verify_ssh_host_key(char *host, int port, char *keytype,
43 char *keystr, char *fingerprint) {
44 int ret;
fcbb94d3 45 HANDLE hin;
46 DWORD savemode, i;
a9422f39 47
48 static const char absentmsg[] =
49 "The server's host key is not cached in the registry. You\n"
50 "have no guarantee that the server is the computer you\n"
51 "think it is.\n"
52 "The server's key fingerprint is:\n"
53 "%s\n"
54 "If you trust this host, enter \"y\" to add the key to\n"
55 "PuTTY's cache and carry on connecting.\n"
56 "If you do not trust this host, enter \"n\" to abandon the\n"
57 "connection.\n"
58 "Continue connecting? (y/n) ";
59
60 static const char wrongmsg[] =
61 "WARNING - POTENTIAL SECURITY BREACH!\n"
62 "The server's host key does not match the one PuTTY has\n"
63 "cached in the registry. This means that either the\n"
64 "server administrator has changed the host key, or you\n"
65 "have actually connected to another computer pretending\n"
66 "to be the server.\n"
67 "The new key fingerprint is:\n"
68 "%s\n"
69 "If you were expecting this change and trust the new key,\n"
70 "enter \"y\" to update PuTTY's cache and continue connecting.\n"
71 "If you want to carry on connecting but without updating\n"
72 "the cache, enter \"n\".\n"
73 "If you want to abandon the connection completely, press\n"
74 "Return to cancel. Pressing Return is the ONLY guaranteed\n"
75 "safe choice.\n"
76 "Update cached key? (y/n, Return cancels connection) ";
77
78 static const char abandoned[] = "Connection abandoned.\n";
79
80 char line[32];
81
82 /*
83 * Verify the key against the registry.
84 */
85 ret = verify_host_key(host, port, keytype, keystr);
86
87 if (ret == 0) /* success - key matched OK */
88 return;
fcbb94d3 89
90 if (ret == 2) /* key was different */
a9422f39 91 fprintf(stderr, wrongmsg, fingerprint);
fcbb94d3 92 if (ret == 1) /* key was absent */
93 fprintf(stderr, absentmsg, fingerprint);
94
95 hin = GetStdHandle(STD_INPUT_HANDLE);
96 GetConsoleMode(hin, &savemode);
97 SetConsoleMode(hin, (savemode | ENABLE_ECHO_INPUT |
98 ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT));
99 ReadFile(hin, line, sizeof(line)-1, &i, NULL);
100 SetConsoleMode(hin, savemode);
101
102 if (ret == 2) { /* key was different */
103 if (line[0] != '\0' && line[0] != '\r' && line[0] != '\n') {
a9422f39 104 if (line[0] == 'y' || line[0] == 'Y')
105 store_host_key(host, port, keytype, keystr);
106 } else {
107 fprintf(stderr, abandoned);
108 exit(0);
109 }
110 }
111 if (ret == 1) { /* key was absent */
fcbb94d3 112 if (line[0] == 'y' || line[0] == 'Y')
a9422f39 113 store_host_key(host, port, keytype, keystr);
114 else {
115 fprintf(stderr, abandoned);
116 exit(0);
117 }
118 }
119}
989b10e9 120
fe50e814 121HANDLE outhandle, errhandle;
6f34e365 122DWORD orig_console_mode;
123
8df7a775 124WSAEVENT netevent;
125
6f34e365 126void begin_session(void) {
127 if (!cfg.ldisc_term)
128 SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_PROCESSED_INPUT);
129 else
130 SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), orig_console_mode);
131}
12dc4ec0 132
fe50e814 133void from_backend(int is_stderr, char *data, int len) {
134 int pos;
12dc4ec0 135 DWORD ret;
fe50e814 136 HANDLE h = (is_stderr ? errhandle : outhandle);
137
138 pos = 0;
139 while (pos < len) {
140 if (!WriteFile(h, data+pos, len-pos, &ret, NULL))
12dc4ec0 141 return; /* give up in panic */
fe50e814 142 pos += ret;
12dc4ec0 143 }
12dc4ec0 144}
145
146struct input_data {
147 DWORD len;
148 char buffer[4096];
8df7a775 149 HANDLE event, eventback;
12dc4ec0 150};
151
67779be7 152static int get_password(const char *prompt, char *str, int maxlen)
153{
154 HANDLE hin, hout;
155 DWORD savemode, i;
156
67779be7 157 if (password) {
158 static int tried_once = 0;
159
160 if (tried_once) {
161 return 0;
162 } else {
163 strncpy(str, password, maxlen);
164 str[maxlen-1] = '\0';
165 tried_once = 1;
166 return 1;
167 }
168 }
67779be7 169
170 hin = GetStdHandle(STD_INPUT_HANDLE);
171 hout = GetStdHandle(STD_OUTPUT_HANDLE);
172 if (hin == INVALID_HANDLE_VALUE || hout == INVALID_HANDLE_VALUE) {
173 fprintf(stderr, "Cannot get standard input/output handles");
174 return 0;
175 }
176
177 GetConsoleMode(hin, &savemode);
178 SetConsoleMode(hin, (savemode & (~ENABLE_ECHO_INPUT)) |
179 ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT);
180
181 WriteFile(hout, prompt, strlen(prompt), &i, NULL);
182 ReadFile(hin, str, maxlen-1, &i, NULL);
183
184 SetConsoleMode(hin, savemode);
185
186 if ((int)i > maxlen) i = maxlen-1; else i = i - 2;
187 str[i] = '\0';
188
189 WriteFile(hout, "\r\n", 2, &i, NULL);
190
191 return 1;
192}
193
6d2d5e8d 194static DWORD WINAPI stdin_read_thread(void *param) {
12dc4ec0 195 struct input_data *idata = (struct input_data *)param;
196 HANDLE inhandle;
197
198 inhandle = GetStdHandle(STD_INPUT_HANDLE);
199
200 while (ReadFile(inhandle, idata->buffer, sizeof(idata->buffer),
8df7a775 201 &idata->len, NULL) && idata->len > 0) {
12dc4ec0 202 SetEvent(idata->event);
8df7a775 203 WaitForSingleObject(idata->eventback, INFINITE);
12dc4ec0 204 }
205
206 idata->len = 0;
207 SetEvent(idata->event);
208
209 return 0;
210}
211
d8426c54 212/*
213 * Short description of parameters.
214 */
215static void usage(void)
216{
217 printf("PuTTY Link: command-line connection utility\n");
218 printf("%s\n", ver);
219 printf("Usage: plink [options] [user@]host [command]\n");
220 printf("Options:\n");
221 printf(" -v show verbose messages\n");
222 printf(" -ssh force use of ssh protocol\n");
223 printf(" -P port connect to specified port\n");
224 printf(" -pw passw login with specified password\n");
225 exit(1);
226}
227
8df7a775 228char *do_select(SOCKET skt, int startup) {
229 int events;
230 if (startup) {
231 events = FD_READ | FD_WRITE | FD_OOB | FD_CLOSE;
232 } else {
233 events = 0;
234 }
235 if (WSAEventSelect (skt, netevent, events) == SOCKET_ERROR) {
236 switch (WSAGetLastError()) {
237 case WSAENETDOWN: return "Network is down";
238 default: return "WSAAsyncSelect(): unknown error";
239 }
240 }
241 return NULL;
242}
243
12dc4ec0 244int main(int argc, char **argv) {
245 WSADATA wsadata;
246 WORD winsock_ver;
8df7a775 247 WSAEVENT stdinevent;
12dc4ec0 248 HANDLE handles[2];
12dc4ec0 249 DWORD threadid;
250 struct input_data idata;
251 int sending;
d8426c54 252 int portnumber = -1;
8df7a775 253 SOCKET *sklist;
254 int skcount, sksize;
255 int connopen;
12dc4ec0 256
67779be7 257 ssh_get_password = get_password;
258
8df7a775 259 sklist = NULL; skcount = sksize = 0;
260
67779be7 261 flags = FLAG_STDERR;
12dc4ec0 262 /*
263 * Process the command line.
264 */
a9422f39 265 do_defaults(NULL, &cfg);
e7a7383f 266 default_protocol = cfg.protocol;
267 default_port = cfg.port;
8cb9c947 268 {
269 /*
270 * Override the default protocol if PLINK_PROTOCOL is set.
271 */
272 char *p = getenv("PLINK_PROTOCOL");
273 int i;
274 if (p) {
275 for (i = 0; backends[i].backend != NULL; i++) {
276 if (!strcmp(backends[i].name, p)) {
277 default_protocol = cfg.protocol = backends[i].protocol;
278 default_port = cfg.port = backends[i].backend->default_port;
279 break;
280 }
281 }
282 }
283 }
12dc4ec0 284 while (--argc) {
285 char *p = *++argv;
286 if (*p == '-') {
287 if (!strcmp(p, "-ssh")) {
288 default_protocol = cfg.protocol = PROT_SSH;
289 default_port = cfg.port = 22;
9d33ebdd 290 } else if (!strcmp(p, "-telnet")) {
291 default_protocol = cfg.protocol = PROT_TELNET;
292 default_port = cfg.port = 23;
293 } else if (!strcmp(p, "-raw")) {
294 default_protocol = cfg.protocol = PROT_RAW;
67779be7 295 } else if (!strcmp(p, "-v")) {
296 flags |= FLAG_VERBOSE;
12dc4ec0 297 } else if (!strcmp(p, "-log")) {
298 logfile = "putty.log";
d8426c54 299 } else if (!strcmp(p, "-pw") && argc > 1) {
300 --argc, password = *++argv;
e7a7383f 301 } else if (!strcmp(p, "-l") && argc > 1) {
302 char *username;
303 --argc, username = *++argv;
304 strncpy(cfg.username, username, sizeof(cfg.username));
305 cfg.username[sizeof(cfg.username)-1] = '\0';
d8426c54 306 } else if (!strcmp(p, "-P") && argc > 1) {
307 --argc, portnumber = atoi(*++argv);
308 }
12dc4ec0 309 } else if (*p) {
310 if (!*cfg.host) {
311 char *q = p;
312 /*
313 * If the hostname starts with "telnet:", set the
314 * protocol to Telnet and process the string as a
315 * Telnet URL.
316 */
317 if (!strncmp(q, "telnet:", 7)) {
318 char c;
319
320 q += 7;
321 if (q[0] == '/' && q[1] == '/')
322 q += 2;
323 cfg.protocol = PROT_TELNET;
324 p = q;
325 while (*p && *p != ':' && *p != '/') p++;
326 c = *p;
327 if (*p)
328 *p++ = '\0';
329 if (c == ':')
330 cfg.port = atoi(p);
331 else
332 cfg.port = -1;
333 strncpy (cfg.host, q, sizeof(cfg.host)-1);
334 cfg.host[sizeof(cfg.host)-1] = '\0';
335 } else {
9d33ebdd 336 char *r;
337 /*
338 * Before we process the [user@]host string, we
339 * first check for the presence of a protocol
340 * prefix (a protocol name followed by ",").
341 */
342 r = strchr(p, ',');
343 if (r) {
344 int i, j;
345 for (i = 0; backends[i].backend != NULL; i++) {
346 j = strlen(backends[i].name);
347 if (j == r-p &&
348 !memcmp(backends[i].name, p, j)) {
349 default_protocol = cfg.protocol = backends[i].protocol;
350 portnumber = backends[i].backend->default_port;
351 p = r+1;
352 break;
353 }
354 }
355 }
356
12dc4ec0 357 /*
358 * Three cases. Either (a) there's a nonzero
359 * length string followed by an @, in which
360 * case that's user and the remainder is host.
361 * Or (b) there's only one string, not counting
362 * a potential initial @, and it exists in the
363 * saved-sessions database. Or (c) only one
364 * string and it _doesn't_ exist in the
365 * database.
366 */
9d33ebdd 367 r = strrchr(p, '@');
12dc4ec0 368 if (r == p) p++, r = NULL; /* discount initial @ */
369 if (r == NULL) {
370 /*
371 * One string.
372 */
a9422f39 373 do_defaults (p, &cfg);
12dc4ec0 374 if (cfg.host[0] == '\0') {
375 /* No settings for this host; use defaults */
376 strncpy(cfg.host, p, sizeof(cfg.host)-1);
377 cfg.host[sizeof(cfg.host)-1] = '\0';
378 cfg.port = 22;
379 }
380 } else {
381 *r++ = '\0';
382 strncpy(cfg.username, p, sizeof(cfg.username)-1);
383 cfg.username[sizeof(cfg.username)-1] = '\0';
384 strncpy(cfg.host, r, sizeof(cfg.host)-1);
385 cfg.host[sizeof(cfg.host)-1] = '\0';
386 cfg.port = 22;
387 }
388 }
389 } else {
390 int len = sizeof(cfg.remote_cmd) - 1;
391 char *cp = cfg.remote_cmd;
392 int len2;
393
394 strncpy(cp, p, len); cp[len] = '\0';
395 len2 = strlen(cp); len -= len2; cp += len2;
396 while (--argc) {
397 if (len > 0)
398 len--, *cp++ = ' ';
399 strncpy(cp, *++argv, len); cp[len] = '\0';
400 len2 = strlen(cp); len -= len2; cp += len2;
401 }
402 cfg.nopty = TRUE; /* command => no terminal */
403 cfg.ldisc_term = TRUE; /* use stdin like a line buffer */
404 break; /* done with cmdline */
405 }
406 }
407 }
408
d8426c54 409 if (!*cfg.host) {
410 usage();
411 }
d8426c54 412
67779be7 413 if (!*cfg.remote_cmd)
414 flags |= FLAG_INTERACTIVE;
415
12dc4ec0 416 /*
417 * Select protocol. This is farmed out into a table in a
418 * separate file to enable an ssh-free variant.
419 */
420 {
421 int i;
422 back = NULL;
423 for (i = 0; backends[i].backend != NULL; i++)
424 if (backends[i].protocol == cfg.protocol) {
425 back = backends[i].backend;
426 break;
427 }
428 if (back == NULL) {
429 fprintf(stderr, "Internal fault: Unsupported protocol found\n");
430 return 1;
431 }
432 }
433
434 /*
8cb9c947 435 * Select port.
436 */
437 if (portnumber != -1)
438 cfg.port = portnumber;
439
440 /*
12dc4ec0 441 * Initialise WinSock.
442 */
443 winsock_ver = MAKEWORD(2, 0);
444 if (WSAStartup(winsock_ver, &wsadata)) {
445 MessageBox(NULL, "Unable to initialise WinSock", "WinSock Error",
446 MB_OK | MB_ICONEXCLAMATION);
447 return 1;
448 }
449 if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 0) {
450 MessageBox(NULL, "WinSock version is incompatible with 2.0",
451 "WinSock Error", MB_OK | MB_ICONEXCLAMATION);
452 WSACleanup();
453 return 1;
454 }
8df7a775 455 sk_init();
12dc4ec0 456
457 /*
458 * Start up the connection.
459 */
8df7a775 460 netevent = CreateEvent(NULL, FALSE, FALSE, NULL);
12dc4ec0 461 {
462 char *error;
463 char *realhost;
464
8df7a775 465 error = back->init (cfg.host, cfg.port, &realhost);
12dc4ec0 466 if (error) {
467 fprintf(stderr, "Unable to open connection:\n%s", error);
468 return 1;
469 }
470 }
8df7a775 471 connopen = 1;
12dc4ec0 472
12dc4ec0 473 stdinevent = CreateEvent(NULL, FALSE, FALSE, NULL);
474
6f34e365 475 GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &orig_console_mode);
476 SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_PROCESSED_INPUT);
12dc4ec0 477 outhandle = GetStdHandle(STD_OUTPUT_HANDLE);
fe50e814 478 errhandle = GetStdHandle(STD_ERROR_HANDLE);
12dc4ec0 479
480 /*
12dc4ec0 481 * Turn off ECHO and LINE input modes. We don't care if this
482 * call fails, because we know we aren't necessarily running in
483 * a console.
484 */
12dc4ec0 485 handles[0] = netevent;
486 handles[1] = stdinevent;
487 sending = FALSE;
488 while (1) {
489 int n;
8cb9c947 490
491 if (!sending && back->sendok()) {
492 /*
493 * Create a separate thread to read from stdin. This is
494 * a total pain, but I can't find another way to do it:
495 *
496 * - an overlapped ReadFile or ReadFileEx just doesn't
497 * happen; we get failure from ReadFileEx, and
498 * ReadFile blocks despite being given an OVERLAPPED
499 * structure. Perhaps we can't do overlapped reads
500 * on consoles. WHY THE HELL NOT?
501 *
502 * - WaitForMultipleObjects(netevent, console) doesn't
503 * work, because it signals the console when
504 * _anything_ happens, including mouse motions and
505 * other things that don't cause data to be readable
506 * - so we're back to ReadFile blocking.
507 */
508 idata.event = stdinevent;
8df7a775 509 idata.eventback = CreateEvent(NULL, FALSE, FALSE, NULL);
8cb9c947 510 if (!CreateThread(NULL, 0, stdin_read_thread,
511 &idata, 0, &threadid)) {
512 fprintf(stderr, "Unable to create second thread\n");
513 exit(1);
514 }
515 sending = TRUE;
516 }
517
12dc4ec0 518 n = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
519 if (n == 0) {
520 WSANETWORKEVENTS things;
8df7a775 521 enum234 e;
522 SOCKET socket;
523 extern SOCKET first_socket(enum234 *), next_socket(enum234 *);
524 extern int select_result(WPARAM, LPARAM);
525 int i;
526
527 /*
528 * We must not call select_result() for any socket
529 * until we have finished enumerating within the tree.
530 * This is because select_result() may close the socket
531 * and modify the tree.
532 */
533 /* Count the active sockets. */
534 i = 0;
535 for (socket = first_socket(&e); socket != INVALID_SOCKET;
536 socket = next_socket(&e))
537 i++;
538
539 /* Expand the buffer if necessary. */
540 if (i > sksize) {
541 sksize = i+16;
542 sklist = srealloc(sklist, sksize * sizeof(*sklist));
543 }
544
545 /* Retrieve the sockets into sklist. */
546 skcount = 0;
547 for (socket = first_socket(&e); socket != INVALID_SOCKET;
548 socket = next_socket(&e)) {
549 sklist[skcount++] = socket;
12dc4ec0 550 }
8df7a775 551
552 /* Now we're done enumerating; go through the list. */
553 for (i = 0; i < skcount; i++) {
554 WPARAM wp;
555 socket = sklist[i];
556 wp = (WPARAM)socket;
557 if (!WSAEnumNetworkEvents(socket, netevent, &things)) {
558 if (things.lNetworkEvents & FD_READ)
559 connopen &= select_result(wp, (LPARAM)FD_READ);
560 if (things.lNetworkEvents & FD_CLOSE)
561 connopen &= select_result(wp, (LPARAM)FD_CLOSE);
562 if (things.lNetworkEvents & FD_OOB)
563 connopen &= select_result(wp, (LPARAM)FD_OOB);
564 if (things.lNetworkEvents & FD_WRITE)
565 connopen &= select_result(wp, (LPARAM)FD_WRITE);
566 }
567 }
12dc4ec0 568 } else if (n == 1) {
569 if (idata.len > 0) {
570 back->send(idata.buffer, idata.len);
571 } else {
572 back->special(TS_EOF);
573 }
8df7a775 574 SetEvent(idata.eventback);
12dc4ec0 575 }
8df7a775 576 if (!connopen || back->socket() == NULL)
cf6e59d6 577 break; /* we closed the connection */
12dc4ec0 578 }
579 WSACleanup();
580 return 0;
581}