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
usage(void)32 usage(void)
33 {
34 fprintf(stderr, "usage: pkg-serve basedir\n");
35 exit(EXIT_FAILURE);
36 }
37
38 int
main(int argc,char * argv[])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