First stab at an SFTP client. Currently a Unixland testing app, not
[u/mdw/putty] / psftp.c
CommitLineData
4c7f0d61 1/*
2 * psftp.c: front end for PSFTP.
3 */
4
5#include <stdio.h>
6#include <stdlib.h>
7#include <assert.h>
8#include <unistd.h>
9
10#include "sftp.h"
11#include "int64.h"
12
13#define smalloc malloc
14#define srealloc realloc
15#define sfree free
16
17/* ----------------------------------------------------------------------
18 * String handling routines.
19 */
20
21char *dupstr(char *s) {
22 int len = strlen(s);
23 char *p = smalloc(len+1);
24 strcpy(p, s);
25 return p;
26}
27
28/* ----------------------------------------------------------------------
29 * sftp client state.
30 */
31
32char *pwd, *homedir;
33
34/* ----------------------------------------------------------------------
35 * Higher-level helper functions used in commands.
36 */
37
38/*
39 * Canonify a pathname starting from the pwd.
40 */
41char *canonify(char *name) {
42 if (name[0] == '/')
43 return fxp_realpath(name, NULL);
44 else
45 return fxp_realpath(pwd, name);
46}
47
48/* ----------------------------------------------------------------------
49 * Actual sftp commands.
50 */
51struct sftp_command {
52 char **words;
53 int nwords, wordssize;
54 int (*obey)(struct sftp_command *);/* returns <0 to quit */
55};
56
57int sftp_cmd_null(struct sftp_command *cmd) {
58 return 0;
59}
60
61int sftp_cmd_unknown(struct sftp_command *cmd) {
62 printf("psftp: unknown command \"%s\"\n", cmd->words[0]);
63 return 0;
64}
65
66int sftp_cmd_quit(struct sftp_command *cmd) {
67 return -1;
68}
69
70/*
71 * List a directory. If no arguments are given, list pwd; otherwise
72 * list the directory given in words[1].
73 */
74static int sftp_ls_compare(const void *av, const void *bv) {
75 const struct fxp_name *a = (const struct fxp_name *)av;
76 const struct fxp_name *b = (const struct fxp_name *)bv;
77 return strcmp(a->filename, b->filename);
78}
79int sftp_cmd_ls(struct sftp_command *cmd) {
80 struct fxp_handle *dirh;
81 struct fxp_names *names;
82 struct fxp_name *ournames;
83 int nnames, namesize;
84 char *dir, *cdir;
85 int i;
86
87 if (cmd->nwords < 2)
88 dir = ".";
89 else
90 dir = cmd->words[1];
91
92 cdir = canonify(dir);
93 if (!cdir) {
94 printf("%s: %s\n", dir, fxp_error());
95 return 0;
96 }
97
98 printf("Listing directory %s\n", cdir);
99
100 dirh = fxp_opendir(cdir);
101 if (dirh == NULL) {
102 printf("Unable to open %s: %s\n", dir, fxp_error());
103 } else {
104 nnames = namesize = 0;
105 ournames = NULL;
106
107 while (1) {
108
109 names = fxp_readdir(dirh);
110 if (names == NULL) {
111 if (fxp_error_type() == SSH_FX_EOF)
112 break;
113 printf("Reading directory %s: %s\n", dir, fxp_error());
114 break;
115 }
116 if (names->nnames == 0) {
117 fxp_free_names(names);
118 break;
119 }
120
121 if (nnames + names->nnames >= namesize) {
122 namesize += names->nnames + 128;
123 ournames = srealloc(ournames, namesize * sizeof(*ournames));
124 }
125
126 for (i = 0; i < names->nnames; i++)
127 ournames[nnames++] = names->names[i];
128
129 names->nnames = 0; /* prevent free_names */
130 fxp_free_names(names);
131 }
132 fxp_close(dirh);
133
134 /*
135 * Now we have our filenames. Sort them by actual file
136 * name, and then output the longname parts.
137 */
138 qsort(ournames, nnames, sizeof(*ournames), sftp_ls_compare);
139
140 /*
141 * And print them.
142 */
143 for (i = 0; i < nnames; i++)
144 printf("%s\n", ournames[i].longname);
145 }
146
147 sfree(cdir);
148
149 return 0;
150}
151
152/*
153 * Change directories. We do this by canonifying the new name, then
154 * trying to OPENDIR it. Only if that succeeds do we set the new pwd.
155 */
156int sftp_cmd_cd(struct sftp_command *cmd) {
157 struct fxp_handle *dirh;
158 char *dir;
159
160 if (cmd->nwords < 2)
161 dir = fxp_realpath(".", NULL);
162 else
163 dir = canonify(cmd->words[1]);
164
165 if (!dir) {
166 printf("%s: %s\n", dir, fxp_error());
167 return 0;
168 }
169
170 dirh = fxp_opendir(dir);
171 if (!dirh) {
172 printf("Directory %s: %s\n", dir, fxp_error());
173 sfree(dir);
174 return 0;
175 }
176
177 fxp_close(dirh);
178
179 sfree(pwd);
180 pwd = dir;
181 printf("Remote directory is now %s\n", pwd);
182
183 return 0;
184}
185
186/*
187 * Get a file and save it at the local end.
188 */
189int sftp_cmd_get(struct sftp_command *cmd) {
190 struct fxp_handle *fh;
191 char *fname, *outfname;
192 uint64 offset;
193 FILE *fp;
194
195 if (cmd->nwords < 2) {
196 printf("get: expects a filename\n");
197 return 0;
198 }
199
200 fname = canonify(cmd->words[1]);
201 if (!fname) {
202 printf("%s: %s\n", cmd->words[1], fxp_error());
203 return 0;
204 }
205 outfname = (cmd->nwords == 2 ? cmd->words[1] : cmd->words[2]);
206
207 fh = fxp_open(fname, SSH_FXF_READ);
208 if (!fh) {
209 printf("%s: %s\n", fname, fxp_error());
210 sfree(fname);
211 return 0;
212 }
213 fp = fopen(outfname, "wb");
214 if (!fp) {
215 printf("local: unable to open %s\n", outfname);
216 fxp_close(fh);
217 sfree(fname);
218 return 0;
219 }
220
221 printf("remote:%s => local:%s\n", fname, outfname);
222
223 offset = uint64_make(0,0);
224
225 /*
226 * FIXME: we can use FXP_FSTAT here to get the file size, and
227 * thus put up a progress bar.
228 */
229 while (1) {
230 char buffer[4096];
231 int len;
232 int wpos, wlen;
233
234 len = fxp_read(fh, buffer, offset, sizeof(buffer));
235 if ((len == -1 && fxp_error_type() == SSH_FX_EOF) ||
236 len == 0)
237 break;
238 if (len == -1) {
239 printf("error while reading: %s\n", fxp_error());
240 break;
241 }
242
243 wpos = 0;
244 while (wpos < len) {
245 wlen = fwrite(buffer, 1, len-wpos, fp);
246 if (wlen <= 0) {
247 printf("error while writing local file\n");
248 break;
249 }
250 wpos += wlen;
251 }
252 if (wpos < len) /* we had an error */
253 break;
254 offset = uint64_add32(offset, len);
255 }
256
257 fclose(fp);
258 fxp_close(fh);
259 sfree(fname);
260
261 return 0;
262}
263
264/*
265 * Send a file and store it at the remote end.
266 */
267int sftp_cmd_put(struct sftp_command *cmd) {
268 struct fxp_handle *fh;
269 char *fname, *origoutfname, *outfname;
270 uint64 offset;
271 FILE *fp;
272
273 if (cmd->nwords < 2) {
274 printf("put: expects a filename\n");
275 return 0;
276 }
277
278 fname = cmd->words[1];
279 origoutfname = (cmd->nwords == 2 ? cmd->words[1] : cmd->words[2]);
280 outfname = canonify(origoutfname);
281~|~ if (!outfname) {
282 printf("%s: %s\n", origoutfname, fxp_error());
283 return 0;
284 }
285
286 fp = fopen(fname, "rb");
287 if (!fp) {
288 printf("local: unable to open %s\n", fname);
289 fxp_close(fh);
290 sfree(outfname);
291 return 0;
292 }
293 fh = fxp_open(outfname, SSH_FXF_WRITE | SSH_FXF_CREAT | SSH_FXF_TRUNC);
294 if (!fh) {
295 printf("%s: %s\n", outfname, fxp_error());
296 sfree(outfname);
297 return 0;
298 }
299
300 printf("local:%s => remote:%s\n", fname, outfname);
301
302 offset = uint64_make(0,0);
303
304 /*
305 * FIXME: we can use FXP_FSTAT here to get the file size, and
306 * thus put up a progress bar.
307 */
308 while (1) {
309 char buffer[4096];
310 int len;
311
312 len = fread(buffer, 1, len, fp);
313 if (len == -1) {
314 printf("error while reading local file\n");
315 break;
316 } else if (len == 0) {
317 break;
318 }
319 if (!fxp_write(fh, buffer, offset, sizeof(buffer))) {
320 printf("error while writing: %s\n", fxp_error());
321 break;
322 }
323 offset = uint64_add32(offset, len);
324 }
325
326 fxp_close(fh);
327 fclose(fp);
328 sfree(outfname);
329
330 return 0;
331}
332
333static struct sftp_cmd_lookup {
334 char *name;
335 int (*obey)(struct sftp_command *);
336} sftp_lookup[] = {
337 /*
338 * List of sftp commands. This is binary-searched so it MUST be
339 * in ASCII order.
340 */
341 {"bye", sftp_cmd_quit},
342 {"cd", sftp_cmd_cd},
343 {"exit", sftp_cmd_quit},
344 {"get", sftp_cmd_get},
345 {"ls", sftp_cmd_ls},
346 {"put", sftp_cmd_put},
347 {"quit", sftp_cmd_quit},
348};
349
350/* ----------------------------------------------------------------------
351 * Command line reading and parsing.
352 */
353struct sftp_command *sftp_getcmd(void) {
354 char *line;
355 int linelen, linesize;
356 struct sftp_command *cmd;
357 char *p, *q, *r;
358 int quoting;
359
360 printf("psftp> ");
361 fflush(stdout);
362
363 cmd = smalloc(sizeof(struct sftp_command));
364 cmd->words = NULL;
365 cmd->nwords = 0;
366 cmd->wordssize = 0;
367
368 line = NULL;
369 linesize = linelen = 0;
370 while (1) {
371 int len;
372 char *ret;
373
374 linesize += 512;
375 line = srealloc(line, linesize);
376 ret = fgets(line+linelen, linesize-linelen, stdin);
377
378 if (!ret || (linelen == 0 && line[0] == '\0')) {
379 cmd->obey = sftp_cmd_quit;
380 printf("quit\n");
381 return cmd; /* eof */
382 }
383 len = linelen + strlen(line+linelen);
384 linelen += len;
385 if (line[linelen-1] == '\n') {
386 linelen--;
387 line[linelen] = '\0';
388 break;
389 }
390 }
391
392 /*
393 * Parse the command line into words. The syntax is:
394 * - double quotes are removed, but cause spaces within to be
395 * treated as non-separating.
396 * - a double-doublequote pair is a literal double quote, inside
397 * _or_ outside quotes. Like this:
398 *
399 * firstword "second word" "this has ""quotes"" in" sodoes""this""
400 *
401 * becomes
402 *
403 * >firstword<
404 * >second word<
405 * >this has "quotes" in<
406 * >sodoes"this"<
407 */
408 p = line;
409 while (*p) {
410 /* skip whitespace */
411 while (*p && (*p == ' ' || *p == '\t')) p++;
412 /* mark start of word */
413 q = r = p; /* q sits at start, r writes word */
414 quoting = 0;
415 while (*p) {
416 if (!quoting && (*p == ' ' || *p == '\t'))
417 break; /* reached end of word */
418 else if (*p == '"' && p[1] == '"')
419 p+=2, *r++ = '"'; /* a literal quote */
420 else if (*p == '"')
421 p++, quoting = !quoting;
422 else
423 *r++ = *p++;
424 }
425 if (*p) p++; /* skip over the whitespace */
426 *r = '\0';
427 if (cmd->nwords >= cmd->wordssize) {
428 cmd->wordssize = cmd->nwords + 16;
429 cmd->words = srealloc(cmd->words, cmd->wordssize*sizeof(char *));
430 }
431 cmd->words[cmd->nwords++] = q;
432 }
433
434 /*
435 * Now parse the first word and assign a function.
436 */
437
438 if (cmd->nwords == 0)
439 cmd->obey = sftp_cmd_null;
440 else {
441 int i, j, k, cmp;
442
443 cmd->obey = sftp_cmd_unknown;
444
445 i = -1;
446 j = sizeof(sftp_lookup) / sizeof(*sftp_lookup);
447 while (j - i > 1) {
448 k = (j + i) / 2;
449 cmp = strcmp(cmd->words[0], sftp_lookup[k].name);
450 if (cmp < 0)
451 j = k;
452 else if (cmp > 0)
453 i = k;
454 else {
455 cmd->obey = sftp_lookup[k].obey;
456 break;
457 }
458 }
459 }
460
461 return cmd;
462}
463
464void do_sftp(void) {
465 /*
466 * Do protocol initialisation.
467 */
468 if (!fxp_init()) {
469 fprintf(stderr,
470 "Fatal: unable to initialise SFTP: %s\n",
471 fxp_error());
472 }
473
474 /*
475 * Find out where our home directory is.
476 */
477 homedir = fxp_realpath(".", NULL);
478 if (!homedir) {
479 fprintf(stderr,
480 "Warning: failed to resolve home directory: %s\n",
481 fxp_error());
482 homedir = dupstr(".");
483 } else {
484 printf("Remote working directory is %s\n", homedir);
485 }
486 pwd = dupstr(homedir);
487
488 /* ------------------------------------------------------------------
489 * Now we're ready to do Real Stuff.
490 */
491 while (1) {
492 struct sftp_command *cmd;
493 cmd = sftp_getcmd();
494 if (!cmd)
495 break;
496 if (cmd->obey(cmd) < 0)
497 break;
498 }
499
500 /* ------------------------------------------------------------------
501 * We've received an exit command. Tidy up and leave.
502 */
503 io_finish();
504}
505
506int main(void) {
507 io_init();
508 do_sftp();
509 return 0;
510}