xref: /src/libexec/pkg-serve/pkg-serve.c (revision b42e852e89cb04cceb6e0226d6a08cab13fb6e90)
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