1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2026 Baptiste Daroussin <bapt@FreeBSD.org> 5 */ 6 7 /* 8 * Speaks the same protocol as "pkg ssh" (see pkg-ssh(8)): 9 * -> ok: pkg-serve <version> 10 * <- get <file> <mtime> 11 * -> ok: <size>\n<data> or ok: 0\n or ko: <error>\n 12 * <- quit 13 */ 14 15 #include <sys/capsicum.h> 16 #include <sys/stat.h> 17 18 #include <ctype.h> 19 #include <err.h> 20 #include <errno.h> 21 #include <fcntl.h> 22 #include <inttypes.h> 23 #include <stdio.h> 24 #include <stdlib.h> 25 #include <string.h> 26 #include <unistd.h> 27 28 #define VERSION "0.1" 29 #define BUFSZ 32768 30 31 static void 32 usage(void) 33 { 34 fprintf(stderr, "usage: pkg-serve basedir\n"); 35 exit(EXIT_FAILURE); 36 } 37 38 int 39 main(int argc, char *argv[]) 40 { 41 struct stat st; 42 cap_rights_t rights; 43 char *line = NULL; 44 char *file, *age; 45 size_t linecap = 0, r, toread; 46 ssize_t linelen; 47 off_t remaining; 48 time_t mtime; 49 char *end; 50 int fd, ffd; 51 char buf[BUFSZ]; 52 const char *basedir; 53 54 if (argc != 2) 55 usage(); 56 57 basedir = argv[1]; 58 59 if ((fd = open(basedir, O_DIRECTORY | O_RDONLY | O_CLOEXEC)) < 0) 60 err(EXIT_FAILURE, "open(%s)", basedir); 61 62 cap_rights_init(&rights, CAP_READ, CAP_FSTATAT, CAP_LOOKUP, 63 CAP_FCNTL); 64 if (cap_rights_limit(fd, &rights) < 0 && errno != ENOSYS) 65 err(EXIT_FAILURE, "cap_rights_limit"); 66 67 if (cap_enter() < 0 && errno != ENOSYS) 68 err(EXIT_FAILURE, "cap_enter"); 69 70 printf("ok: pkg-serve " VERSION "\n"); 71 fflush(stdout); 72 73 while ((linelen = getline(&line, &linecap, stdin)) > 0) { 74 /* trim newline */ 75 if (linelen > 0 && line[linelen - 1] == '\n') 76 line[--linelen] = '\0'; 77 78 if (linelen == 0) 79 continue; 80 81 if (strcmp(line, "quit") == 0) 82 break; 83 84 if (strncmp(line, "get ", 4) != 0) { 85 printf("ko: unknown command '%s'\n", line); 86 fflush(stdout); 87 continue; 88 } 89 90 file = line + 4; 91 92 if (*file == '\0') { 93 printf("ko: bad command get, expecting 'get file age'\n"); 94 fflush(stdout); 95 continue; 96 } 97 98 /* skip leading slash */ 99 if (*file == '/') 100 file++; 101 102 /* find the age argument */ 103 age = file; 104 while (*age != '\0' && !isspace((unsigned char)*age)) 105 age++; 106 107 if (*age == '\0') { 108 printf("ko: bad command get, expecting 'get file age'\n"); 109 fflush(stdout); 110 continue; 111 } 112 113 *age++ = '\0'; 114 115 /* skip whitespace */ 116 while (isspace((unsigned char)*age)) 117 age++; 118 119 if (*age == '\0') { 120 printf("ko: bad command get, expecting 'get file age'\n"); 121 fflush(stdout); 122 continue; 123 } 124 125 errno = 0; 126 mtime = (time_t)strtoimax(age, &end, 10); 127 if (errno != 0 || *end != '\0' || end == age) { 128 printf("ko: bad number %s\n", age); 129 fflush(stdout); 130 continue; 131 } 132 133 if (fstatat(fd, file, &st, AT_RESOLVE_BENEATH) == -1) { 134 printf("ko: file not found\n"); 135 fflush(stdout); 136 continue; 137 } 138 139 if (!S_ISREG(st.st_mode)) { 140 printf("ko: not a file\n"); 141 fflush(stdout); 142 continue; 143 } 144 145 if (st.st_mtime <= mtime) { 146 printf("ok: 0\n"); 147 fflush(stdout); 148 continue; 149 } 150 151 if ((ffd = openat(fd, file, O_RDONLY | O_RESOLVE_BENEATH)) == -1) { 152 printf("ko: file not found\n"); 153 fflush(stdout); 154 continue; 155 } 156 157 printf("ok: %" PRIdMAX "\n", (intmax_t)st.st_size); 158 fflush(stdout); 159 160 remaining = st.st_size; 161 while (remaining > 0) { 162 toread = sizeof(buf); 163 if ((off_t)toread > remaining) 164 toread = (size_t)remaining; 165 r = read(ffd, buf, toread); 166 if (r <= 0) 167 break; 168 if (fwrite(buf, 1, r, stdout) != r) 169 break; 170 remaining -= r; 171 } 172 close(ffd); 173 if (remaining > 0) 174 errx(EXIT_FAILURE, "%s: file truncated during transfer", 175 file); 176 fflush(stdout); 177 } 178 179 return (EXIT_SUCCESS); 180 } 181