You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

895 lines
14 KiB

  1. /* See LICENSE file for copyright and license details. */
  2. #include <errno.h>
  3. #include <fcntl.h>
  4. #include <limits.h>
  5. #include <locale.h>
  6. #include <netdb.h>
  7. #include <netinet/in.h>
  8. #include <signal.h>
  9. #include <stdarg.h>
  10. #include <stdio.h>
  11. #include <stdlib.h>
  12. #include <string.h>
  13. #include <unistd.h>
  14. #include <wchar.h>
  15. #include <sys/socket.h>
  16. #include <sys/stat.h>
  17. #include <sys/types.h>
  18. #include <sys/wait.h>
  19. #include "common.h"
  20. #include "config.h"
  21. static char *mainurl;
  22. static Item *mainentry;
  23. static int devnullfd;
  24. static int parent = 1;
  25. static int interactive;
  26. void
  27. die(const char *fmt, ...)
  28. {
  29. va_list arg;
  30. va_start(arg, fmt);
  31. vfprintf(stderr, fmt, arg);
  32. va_end(arg);
  33. fputc('\n', stderr);
  34. exit(1);
  35. }
  36. #ifndef asprintf
  37. int
  38. asprintf(char **s, const char *fmt, ...)
  39. {
  40. va_list ap;
  41. int n;
  42. va_start(ap, fmt);
  43. n = vsnprintf(NULL, 0, fmt, ap);
  44. va_end(ap);
  45. if (n == INT_MAX || !(*s = malloc(++n)))
  46. return -1;
  47. va_start(ap, fmt);
  48. vsnprintf(*s, n, fmt, ap);
  49. va_end(ap);
  50. return n;
  51. }
  52. #endif /* asprintf */
  53. /* print `len' columns of characters. */
  54. size_t
  55. mbsprint(const char *s, size_t len)
  56. {
  57. wchar_t wc;
  58. size_t col = 0, i, slen;
  59. int rl, w;
  60. if (!len)
  61. return col;
  62. slen = strlen(s);
  63. for (i = 0; i < slen; i += rl) {
  64. if ((rl = mbtowc(&wc, s + i, slen - i < 4 ? slen - i : 4)) <= 0)
  65. break;
  66. if ((w = wcwidth(wc)) == -1)
  67. continue;
  68. if (col + w > len || (col + w == len && s[i + rl])) {
  69. fputs("\xe2\x80\xa6", stdout);
  70. col++;
  71. break;
  72. }
  73. fwrite(s + i, 1, rl, stdout);
  74. col += w;
  75. }
  76. return col;
  77. }
  78. static void *
  79. xreallocarray(void *m, const size_t n, const size_t s)
  80. {
  81. void *nm;
  82. if (n == 0 || s == 0) {
  83. free(m);
  84. return NULL;
  85. }
  86. if (s && n > (size_t)-1/s)
  87. die("realloc: overflow");
  88. if (!(nm = realloc(m, n * s)))
  89. die("realloc: %s", strerror(errno));
  90. return nm;
  91. }
  92. static void *
  93. xmalloc(const size_t n)
  94. {
  95. void *m = malloc(n);
  96. if (!m)
  97. die("malloc: %s\n", strerror(errno));
  98. return m;
  99. }
  100. static void *
  101. xcalloc(size_t n)
  102. {
  103. char *m = xmalloc(n);
  104. while (n)
  105. m[--n] = 0;
  106. return m;
  107. }
  108. static char *
  109. xstrdup(const char *str)
  110. {
  111. char *s;
  112. if (!(s = strdup(str)))
  113. die("strdup: %s\n", strerror(errno));
  114. return s;
  115. }
  116. static void
  117. usage(void)
  118. {
  119. die("usage: sacc URL");
  120. }
  121. static void
  122. clearitem(Item *item)
  123. {
  124. Dir *dir;
  125. Item *items;
  126. char *tag;
  127. size_t i;
  128. if (!item)
  129. return;
  130. if (dir = item->dat) {
  131. items = dir->items;
  132. for (i = 0; i < dir->nitems; ++i)
  133. clearitem(&items[i]);
  134. free(items);
  135. clear(&item->dat);
  136. }
  137. if (parent && (tag = item->tag) &&
  138. !strncmp(tag, tmpdir, strlen(tmpdir)))
  139. unlink(tag);
  140. clear(&item->tag);
  141. clear(&item->raw);
  142. }
  143. const char *
  144. typedisplay(char t)
  145. {
  146. switch (t) {
  147. case '0':
  148. return "Text+";
  149. case '1':
  150. return "Dir +";
  151. case '2':
  152. return "CSO |";
  153. case '3':
  154. return "Err |";
  155. case '4':
  156. return "Macf+";
  157. case '5':
  158. return "DOSf+";
  159. case '6':
  160. return "UUEf+";
  161. case '7':
  162. return "Find+";
  163. case '8':
  164. return "Tlnt|";
  165. case '9':
  166. return "Binf+";
  167. case '+':
  168. return "Mirr+";
  169. case 'T':
  170. return "IBMt|";
  171. case 'g':
  172. return "GIF +";
  173. case 'I':
  174. return "Img +";
  175. case 'h':
  176. return "HTML+";
  177. case 'i':
  178. return " |";
  179. case 's':
  180. return "Snd |";
  181. default:
  182. return "! |";
  183. }
  184. }
  185. static void
  186. printdir(Item *item)
  187. {
  188. Dir *dir;
  189. Item *items;
  190. size_t i, nitems;
  191. if (!item || !(dir = item->dat))
  192. return;
  193. items = dir->items;
  194. nitems = dir->nitems;
  195. for (i = 0; i < nitems; ++i) {
  196. printf("%s%s\n",
  197. typedisplay(items[i].type), items[i].username);
  198. }
  199. }
  200. static void
  201. displaytextitem(Item *item)
  202. {
  203. FILE *pagerin;
  204. int pid, wpid;
  205. uicleanup();
  206. switch (pid = fork()) {
  207. case -1:
  208. uistatus("Couldn't fork.");
  209. return;
  210. case 0:
  211. parent = 0;
  212. pagerin = popen("$PAGER", "we");
  213. fputs(item->raw, pagerin);
  214. exit(pclose(pagerin));
  215. default:
  216. while ((wpid = wait(NULL)) >= 0 && wpid != pid)
  217. ;
  218. }
  219. uisetup();
  220. }
  221. static char *
  222. pickfield(char **raw, char sep)
  223. {
  224. char *c, *f = *raw;
  225. for (c = *raw; *c && *c != sep; ++c)
  226. ;
  227. *c = '\0';
  228. *raw = c+1;
  229. return f;
  230. }
  231. static char *
  232. invaliditem(char *raw)
  233. {
  234. char c;
  235. int tabs;
  236. for (tabs = 0; (c = *raw) && c != '\n'; ++raw) {
  237. if (c == '\t')
  238. ++tabs;
  239. }
  240. if (c)
  241. *raw++ = '\0';
  242. return (tabs == 3) ? NULL : raw;
  243. }
  244. static void
  245. molditem(Item *item, char **raw)
  246. {
  247. char *next;
  248. if (!*raw)
  249. return;
  250. if ((next = invaliditem(*raw))) {
  251. item->username = *raw;
  252. *raw = next;
  253. return;
  254. }
  255. item->type = *raw[0]++;
  256. item->username = pickfield(raw, '\t');
  257. item->selector = pickfield(raw, '\t');
  258. item->host = pickfield(raw, '\t');
  259. item->port = pickfield(raw, '\r');
  260. if (!*raw[0])
  261. ++*raw;
  262. }
  263. static Dir *
  264. molddiritem(char *raw)
  265. {
  266. Item *items = NULL;
  267. char *s, *nl, *p;
  268. Dir *dir;
  269. size_t i, nitems;
  270. for (s = nl = raw, nitems = 0; p = strchr(nl, '\n'); ++nitems) {
  271. s = nl;
  272. nl = p+1;
  273. }
  274. if (!strcmp(s, ".\r\n") || !strcmp(s, ".\n"))
  275. --nitems;
  276. if (!nitems) {
  277. uistatus("Couldn't parse dir item");
  278. return NULL;
  279. }
  280. dir = xmalloc(sizeof(Dir));
  281. items = xreallocarray(items, nitems, sizeof(Item));
  282. memset(items, 0, nitems * sizeof(Item));
  283. for (i = 0; i < nitems; ++i)
  284. molditem(&items[i], &raw);
  285. dir->items = items;
  286. dir->nitems = nitems;
  287. dir->printoff = dir->curline = 0;
  288. return dir;
  289. }
  290. static char *
  291. getrawitem(int sock)
  292. {
  293. char *raw, *buf;
  294. size_t bn, bs;
  295. ssize_t n;
  296. raw = buf = NULL;
  297. bn = bs = n = 0;
  298. do {
  299. bs -= n;
  300. buf += n;
  301. if (bs < 1) {
  302. raw = xreallocarray(raw, ++bn, BUFSIZ);
  303. buf = raw + (bn-1) * BUFSIZ;
  304. bs = BUFSIZ;
  305. }
  306. } while ((n = read(sock, buf, bs)) > 0);
  307. *buf = '\0';
  308. if (n < 0) {
  309. uistatus("Can't read socket: %s", strerror(errno));
  310. clear(&raw);
  311. }
  312. return raw;
  313. }
  314. static int
  315. sendselector(int sock, const char *selector)
  316. {
  317. char *msg, *p;
  318. size_t ln;
  319. ssize_t n;
  320. ln = strlen(selector) + 3;
  321. msg = p = xmalloc(ln);
  322. snprintf(msg, ln--, "%s\r\n", selector);
  323. while ((n = write(sock, p, ln)) != -1 && n != 0) {
  324. ln -= n;
  325. p += n;
  326. }
  327. free(msg);
  328. if (n == -1)
  329. uistatus("Can't send message: %s", strerror(errno));
  330. return n;
  331. }
  332. static int
  333. connectto(const char *host, const char *port)
  334. {
  335. static const struct addrinfo hints = {
  336. .ai_family = AF_UNSPEC,
  337. .ai_socktype = SOCK_STREAM,
  338. .ai_protocol = IPPROTO_TCP,
  339. };
  340. struct addrinfo *addrs, *addr;
  341. int sock, r;
  342. if (r = getaddrinfo(host, port, &hints, &addrs)) {
  343. uistatus("Can't resolve hostname \"%s\": %s",
  344. host, gai_strerror(r));
  345. return -1;
  346. }
  347. for (addr = addrs; addr; addr = addr->ai_next) {
  348. if ((sock = socket(addr->ai_family, addr->ai_socktype,
  349. addr->ai_protocol)) < 0)
  350. continue;
  351. if ((r = connect(sock, addr->ai_addr, addr->ai_addrlen)) < 0) {
  352. close(sock);
  353. continue;
  354. }
  355. break;
  356. }
  357. if (sock < 0) {
  358. uistatus("Can't open socket: %s", strerror(errno));
  359. return -1;
  360. }
  361. if (r < 0) {
  362. uistatus("Can't connect to: %s:%s: %s",
  363. host, port, strerror(errno));
  364. return -1;
  365. }
  366. freeaddrinfo(addrs);
  367. return sock;
  368. }
  369. static int
  370. download(Item *item, int dest)
  371. {
  372. char buf[BUFSIZ];
  373. ssize_t r, w;
  374. int src;
  375. if (!item->tag) {
  376. if ((src = connectto(item->host, item->port)) < 0 ||
  377. sendselector(src, item->selector) < 0)
  378. return 0;
  379. } else if ((src = open(item->tag, O_RDONLY)) < 0) {
  380. printf("Can't open source file %s: %s",
  381. item->tag, strerror(errno));
  382. errno = 0;
  383. return 0;
  384. }
  385. while ((r = read(src, buf, BUFSIZ)) > 0) {
  386. while ((w = write(dest, buf, r)) > 0)
  387. r -= w;
  388. }
  389. if (r < 0 || w < 0) {
  390. printf("Error downloading file %s: %s",
  391. item->selector, strerror(errno));
  392. errno = 0;
  393. }
  394. close(src);
  395. return (r == 0 && w == 0);
  396. }
  397. static void
  398. downloaditem(Item *item)
  399. {
  400. char *file, *path, *tag;
  401. mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP;
  402. int dest;
  403. if (file = strrchr(item->selector, '/'))
  404. ++file;
  405. else
  406. file = item->selector;
  407. if (!(path = uiprompt("Download to [%s] (^D cancel): ", file)))
  408. return;
  409. if (!path[0])
  410. path = xstrdup(file);
  411. if (tag = item->tag) {
  412. if (access(tag, R_OK) < 0) {
  413. clear(&item->tag);
  414. } else if (!strcmp(tag, path)) {
  415. goto cleanup;
  416. }
  417. }
  418. if ((dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
  419. uistatus("Can't open destination file %s: %s",
  420. path, strerror(errno));
  421. errno = 0;
  422. goto cleanup;
  423. }
  424. if (!download(item, dest))
  425. goto cleanup;
  426. if (!item->tag)
  427. item->tag = path;
  428. return;
  429. cleanup:
  430. free(path);
  431. return;
  432. }
  433. static int
  434. fetchitem(Item *item)
  435. {
  436. int sock;
  437. if ((sock = connectto(item->host, item->port)) < 0 ||
  438. sendselector(sock, item->selector) < 0)
  439. return 0;
  440. item->raw = getrawitem(sock);
  441. close(sock);
  442. if (item->raw && !*item->raw) {
  443. uistatus("Empty response from server");
  444. clear(&item->raw);
  445. }
  446. return (item->raw != NULL);
  447. }
  448. static void
  449. plumb(char *url)
  450. {
  451. switch (fork()) {
  452. case -1:
  453. uistatus("Couldn't fork.");
  454. return;
  455. case 0:
  456. parent = 0;
  457. dup2(devnullfd, 1);
  458. dup2(devnullfd, 2);
  459. if (execlp(plumber, plumber, url, NULL) < 0)
  460. uistatus("execlp: plumb(%s): %s", url, strerror(errno));
  461. }
  462. uistatus("Plumbed \"%s\"", url);
  463. }
  464. static void
  465. plumbitem(Item *item)
  466. {
  467. char *file, *path, *tag;
  468. mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP;
  469. int n, dest, plumbitem;
  470. if (file = strrchr(item->selector, '/'))
  471. ++file;
  472. else
  473. file = item->selector;
  474. path = uiprompt("Download %s to (^D cancel, <empty> plumb): ",
  475. file);
  476. if (!path)
  477. return;
  478. if ((tag = item->tag) && access(tag, R_OK) < 0) {
  479. clear(&item->tag);
  480. tag = NULL;
  481. }
  482. plumbitem = path[0] ? 0 : 1;
  483. if (!path[0]) {
  484. clear(&path);
  485. if (!tag) {
  486. if (asprintf(&path, "%s/%s", tmpdir, file) < 0)
  487. die("Can't generate tmpdir path: ",
  488. strerror(errno));
  489. }
  490. }
  491. if (path && (!tag || strcmp(tag, path))) {
  492. if ((dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
  493. uistatus("Can't open destination file %s: %s",
  494. path, strerror(errno));
  495. errno = 0;
  496. goto cleanup;
  497. }
  498. if (!download(item, dest) || tag)
  499. goto cleanup;
  500. }
  501. if (!tag)
  502. item->tag = path;
  503. if (plumbitem)
  504. plumb(item->tag);
  505. return;
  506. cleanup:
  507. free(path);
  508. return;
  509. }
  510. static int
  511. dig(Item *entry, Item *item)
  512. {
  513. char *plumburi = NULL;
  514. if (item->raw) /* already in cache */
  515. return item->type;
  516. if (!item->entry)
  517. item->entry = entry ? entry : item;
  518. switch (item->type) {
  519. case 'h': /* fallthrough */
  520. if (!strncmp(item->selector, "URL:", 4)) {
  521. plumb(item->selector+4);
  522. return 0;
  523. }
  524. case '0':
  525. if (!fetchitem(item))
  526. return 0;
  527. break;
  528. case '1':
  529. case '+':
  530. case '7':
  531. if (!fetchitem(item) || !(item->dat = molddiritem(item->raw)))
  532. return 0;
  533. break;
  534. case '4':
  535. case '5':
  536. case '6':
  537. case '9':
  538. downloaditem(item);
  539. return 0;
  540. case '8':
  541. if (asprintf(&plumburi, "telnet://%s@%s:%s", item->selector,
  542. item->host, item->port) < 0)
  543. return 0;
  544. plumb(plumburi);
  545. free(plumburi);
  546. return 0;
  547. case 'g':
  548. case 'I':
  549. plumbitem(item);
  550. return 0;
  551. case 'T':
  552. if (asprintf(&plumburi, "tn3270://%s@%s:%s", item->selector,
  553. item->host, item->port) < 0)
  554. return 0;
  555. plumb(plumburi);
  556. free(plumburi);
  557. return 0;
  558. default:
  559. uistatus("Type %c (%s) not supported",
  560. item->type, typedisplay(item->type));
  561. return 0;
  562. }
  563. return item->type;
  564. }
  565. static char *
  566. searchselector(Item *item)
  567. {
  568. char *pexp, *exp, *tag, *selector = item->selector;
  569. size_t n = strlen(selector);
  570. if ((tag = item->tag) && !strncmp(tag, selector, n))
  571. pexp = tag + n+1;
  572. else
  573. pexp = "";
  574. if (!(exp = uiprompt("Enter search string (^D cancel) [%s]: ", pexp)))
  575. return NULL;
  576. if (exp[0] && strcmp(exp, pexp)) {
  577. n += strlen(exp) + 2;
  578. tag = xmalloc(n);
  579. snprintf(tag, n, "%s\t%s", selector, exp);
  580. }
  581. free(exp);
  582. return tag;
  583. }
  584. static int
  585. searchitem(Item *entry, Item *item)
  586. {
  587. char *sel, *selector;
  588. if (!(sel = searchselector(item)))
  589. return 0;
  590. if (sel != item->tag) {
  591. clearitem(item);
  592. selector = item->selector;
  593. item->selector = item->tag = sel;
  594. dig(entry, item);
  595. item->selector = selector;
  596. }
  597. return (item->dat != NULL);
  598. }
  599. static void
  600. printout(Item *hole)
  601. {
  602. if (!hole)
  603. return;
  604. switch (hole->type) {
  605. case '0':
  606. if (dig(hole, hole))
  607. fputs(hole->raw, stdout);
  608. return;
  609. case '1':
  610. case '+':
  611. if (dig(hole, hole))
  612. printdir(hole);
  613. default:
  614. return;
  615. }
  616. }
  617. static void
  618. delve(Item *hole)
  619. {
  620. Item *entry = NULL;
  621. while (hole) {
  622. switch (hole->type) {
  623. case 'h':
  624. case '0':
  625. if (dig(entry, hole))
  626. displaytextitem(hole);
  627. break;
  628. case '1':
  629. case '+':
  630. if (dig(entry, hole) && hole->dat)
  631. entry = hole;
  632. break;
  633. case '7':
  634. if (searchitem(entry, hole))
  635. entry = hole;
  636. break;
  637. case '4':
  638. case '5':
  639. case '6': /* TODO decode? */
  640. case '8':
  641. case '9':
  642. case 'g':
  643. case 'I':
  644. case 'T':
  645. dig(entry, hole);
  646. break;
  647. case 0:
  648. uistatus("Couldn't get %s:%s/%c%s", hole->host,
  649. hole->port, hole->type, hole->selector);
  650. }
  651. if (!entry)
  652. return;
  653. do {
  654. uidisplay(entry);
  655. hole = uiselectitem(entry);
  656. } while (hole == entry);
  657. }
  658. }
  659. static Item *
  660. moldentry(char *url)
  661. {
  662. Item *entry;
  663. char *p, *host = url, *port = "70", *gopherpath = "1";
  664. int parsed, ipv6;
  665. if (p = strstr(url, "://")) {
  666. if (strncmp(url, "gopher", p - url))
  667. die("Protocol not supported: %.*s", p - url, url);
  668. host = p + 3;
  669. }
  670. if (*host == '[') {
  671. ipv6 = 1;
  672. ++host;
  673. } else {
  674. ipv6 = 0;
  675. }
  676. for (parsed = 0, p = host; !parsed && *p; ++p) {
  677. switch (*p) {
  678. case ']':
  679. if (ipv6) {
  680. *p = '\0';
  681. ipv6 = 0;
  682. }
  683. continue;
  684. case ':':
  685. if (!ipv6) {
  686. *p = '\0';
  687. port = p+1;
  688. }
  689. continue;
  690. case '/':
  691. *p = '\0';
  692. parsed = 1;
  693. continue;
  694. }
  695. }
  696. if (*host == '\0' || *port == '\0' || ipv6)
  697. die("Can't parse url");
  698. if (*p != '\0')
  699. gopherpath = p;
  700. entry = xcalloc(sizeof(Item));
  701. entry->type = gopherpath[0];
  702. entry->username = entry->selector = ++gopherpath;
  703. entry->host = host;
  704. entry->port = port;
  705. entry->entry = entry;
  706. return entry;
  707. }
  708. static void
  709. cleanup(void)
  710. {
  711. clearitem(mainentry);
  712. if (parent)
  713. rmdir(tmpdir);
  714. free(mainentry);
  715. free(mainurl);
  716. if (interactive)
  717. uicleanup();
  718. }
  719. static void
  720. setup(void)
  721. {
  722. struct sigaction sa;
  723. int fd;
  724. setlocale(LC_CTYPE, "");
  725. setenv("PAGER", "more", 0);
  726. atexit(cleanup);
  727. /* reopen stdin in case we're reading from a pipe */
  728. if ((fd = open("/dev/tty", O_RDONLY)) < 0)
  729. die("open: /dev/tty: %s", strerror(errno));
  730. if (dup2(fd, 0) < 0)
  731. die("dup2: /dev/tty, stdin: %s", strerror(errno));
  732. close(fd);
  733. if ((devnullfd = open("/dev/null", O_WRONLY)) < 0)
  734. die("open: /dev/null: %s", strerror(errno));
  735. if (mkdir(tmpdir, S_IRWXU) < 0 && errno != EEXIST)
  736. die("mkdir: %s: %s", tmpdir, strerror(errno));
  737. if(interactive = isatty(1)) {
  738. uisetup();
  739. sigemptyset(&sa.sa_mask);
  740. sa.sa_handler = uisigwinch;
  741. sa.sa_flags = SA_RESTART;
  742. sigaction(SIGWINCH, &sa, NULL);
  743. }
  744. }
  745. int
  746. main(int argc, char *argv[])
  747. {
  748. if (argc != 2)
  749. usage();
  750. setup();
  751. mainurl = xstrdup(argv[1]);
  752. mainentry = moldentry(mainurl);
  753. if (interactive)
  754. delve(mainentry);
  755. else
  756. printout(mainentry);
  757. exit(0);
  758. }