admins.html: simplify Libera.Chat URL
[girocco.git] / src / peek_packet.c
bloba72c25131b5ea613ab7ce7c010d0784aa48c3dbc
1 /*
3 peek_packet.c -- peek_packet utility to peek at incoming git-daemon request
4 Copyright (C) 2015,2020 Kyle J. McKay. All rights reserved.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; either version 2
9 of the License, or (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 This utility is intended to be used by a script front end to git daemon
24 running in inetd mode. The first thing the script does is call this utility
25 which attempts to peek the first incoming Git packet off the connection
26 and then output contents after the initial 4-character hex length upto but
27 excluding the first \0 character.
29 At that point the script can validate the incoming request and if it chooses
30 to allow it then exec git daemon to process it. Since the packet was peeked
31 it's still there to be read by git daemon.
33 Note that there is a hard-coded timeout of 30 seconds and a hard-coded limit
34 of PATH_MAX for the length of the initial packet.
36 On failure a non-zero exit code is returned. On success a 0 exit code is
37 returned and peeked text is output to stdout.
39 The connection to be peeked must be fd 0 and will have SO_KEEPALIVE set on it.
41 This utility does not take any arguments and ignores any that are given.
43 The output of a successful peek should be one of these:
45 git-upload-pack /<...>
46 git-upload-archive /<...>
47 git-receive-pack /<...>
49 where "<...>" is replaced with the repository path so, for example, doing
50 "git ls-remote git://example.com/foo.git" would result in this output:
52 git-upload-pack /foo.git
54 Note that the first character could be a ~ instead of a /, but it's
55 probably best to reject those.
57 The output is guaranteed to not contain any bytes with a value less than
58 %x20 except for possibly tab (%x09) characters. Any request that does
59 will produce no output and return a non-zero exit code.
61 If the extra optional "host=" parameter is present, then an additional
62 second line is output in the format:
64 host=<hostname>
66 where <hostname> is the host name only from the extra "host=" parameter
67 that's been lowercased and had a trailing "." removed (but only if that
68 doesn't create the empty string) and had any surrounding '[' and ']' removed
69 from a literal IPv6 address. The <hostname> is guaranteed to only contain
70 bytes in the range %x21-%xFF.
72 If the extra optional "host=" parameter contains a ":" <portnum> suffix
73 then an additional third line will be output of the format:
75 port=<portnum>
77 If just the ":" was present <portnum> will be the empty string otherwise
78 it's guaranteed to be a decimal number with no leading zeros in the range
79 1..65535.
81 For example, this git command:
83 git ls-remote git://example.com/repo.git
85 Will result in peek_packet producing these two lines (unless a very, very
86 old version of Git was used in which case only the first line):
88 git-upload-pack /repo.git
89 host=example.com
91 This git command:
93 git ls-remote git://[::1]:8765/repo.git
95 Will result in peek_packet producing these three lines (unless a very, very
96 old version of Git was used in which case only the first line):
98 git-upload-pack /repo.git
99 host=::1
100 port=8765
102 If the incoming packet looks invalid (or a timeout occurs) then no output
103 is produced, but a non-zero exit code is set.
105 If the remote address is available (getpeername) then a line of the form
107 remote_addr=<address>
109 will be output where <address> is either a dotted IPv4 or an IPv6
110 (without brackets).
112 If the local address is available (getsockname) then two lines of the form
114 server_addr=<address>
115 server_port=<port>
117 will be output where <address> is in the same format as remote_addr and
118 server_port is in the same format as port.
124 ;; The Git packet protocol is defined as follows in RFC 5234+7405 syntax
127 BYTE = %x00-FF
129 DIGIT = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
131 ; Note that hexdigits are case insensitive
132 HEX-DIGIT = DIGIT / "a" / "b" / "c" / "d" / "e" / "f"
134 PKT-LENGTH = 4HEX-DIGIT ; represents a hexadecimal big-endian non-negative
135 ; value. a length of "0002" or "0003" is invalid.
136 ; lengths "0000" and "0001" have special meaning.
138 PKT-DATA = *BYTE ; first 4 <BYTE>s MUST NOT be %s"ERR "
140 PKT = FLUSH-PKT / DELIM-PKT / ERR-PKT / PROTOCOL-PKT
142 FLUSH-PKT = "0000"
144 DELIM-PKT = "0001"
146 PROTOCOL-PKT = PKT-LENGTH PKT-DATA ; PKT-DATA must contain exactly
147 ; PKT-LENGTH - 4 bytes
149 ERR-PKT = PKT-LENGTH ERR-DATA; ERR-DATA must contain exactly PKT-LENGTH - 4 bytes
151 ERR-DATA = %s"ERR " *BYTE ; Note that "ERR " *IS* case sensitive
154 ;; The first packet sent by a client connecting to a "Git Transport" server
155 ;; has the <GIT-REQUEST-PKT> format
158 GIT-REQUEST-PKT = PKT-LENGTH GIT-REQUEST-DATA ; GIT-REQUEST-DATA must contain
159 ; exactly PKT-LENGTH - 4 bytes
161 ; Normally if %x0A is present it's the final byte (very old Git versions)
162 ; Normally if %x00 is present then %x0A is not (modern Git versions)
163 ; But the current Git versions do parse the %x0A.00 form correctly
164 GIT-REQUEST-DATA = GIT-COMMAND [EXTRA-ARGS]
166 GIT-COMMAND = REQUEST-COMMAND %20 PATHNAME [%0A]
168 ; these are all case sensitive
169 REQUEST-COMMAND = %s"git-upload-pack" /
170 %s"git-receive-pack" /
171 %s"git-upload-archive"
173 PATHNAME = NON-NULL-BYTES
175 EXTRA-ARGS = %x00 HOST-ARG-TRUNCATED /
176 %x00 [HOST-ARG] [%x00 EXTRA-PARMS]
178 HOST-ARG-TRUNCATED = HOST-PARAM HOST-NAME [ ":" [PORTNUM] ]
180 HOST-ARG = HOST-ARG-TRUNCATED %x00
182 ; "host=" is case insensitive
183 HOST-PARAM = "host="
185 HOST-NAME = NON-NULL-BYTES ; should be a valid DNS name
186 ; or IPv4 literal
187 ; or "[" IPv6 literal "]"
188 ; a ":" is only allowed between
189 ; the "[" and "]" of an IPv6 literal
191 ; PORTNUM matches 1..65535 with no leading zeros allowed
192 PORTNUM = ( "1" / "2" / "3" / "4" / "5" ) *4DIGIT /
193 "6" /
194 "6" ( "0" / "1" / "2" / "3" / "4" ) *3DIGIT /
195 "65" ( "0" / "1" / "2" / "3" / "4" ) *2DIGIT /
196 "655" ( "0" / "1" / "2" ) *1DIGIT /
197 "6553" ( "0" / "1" / "2" / "3" / "4" / "5" ) /
198 "6" ( "6" / "7" / "8" / "9" ) *2DIGIT /
199 ( "7" / "8" / "9" ) *3DIGIT
201 EXTRA-PARAMS = *EXTRA-PARAM [EXTRA-PARAM-TRUNCATED]
203 EXTRA-PARAM-TRUNCATED = NON-NULL-BYTES
205 EXTRA-PARAM = EXTRA-PARAM-TRUNCATED %x00
207 NON-NULL-BYTES = *NON-NULL-BYTE
209 NON-NULL-BYTE = %x01-FF
213 #define HAVE_STDINT_H 1
214 #define HAVE_IPV6 1
215 #define HAVE_INET_NTOP 1
216 #define HAVE_SNPRINTF 1
217 #ifdef CONFIG_H
218 #include CONFIG_H
219 #endif
221 #include <stddef.h>
222 #ifdef HAVE_STDINT_H
223 #include <stdint.h>
224 #endif
225 #include <stdio.h>
226 #include <string.h>
227 #include <stdlib.h>
228 #include <unistd.h>
229 #include <limits.h>
230 #include <time.h>
231 #include <signal.h>
232 #include <sys/types.h>
233 #include <sys/socket.h>
234 #include <netinet/in.h>
235 #include <arpa/inet.h>
236 #ifdef HAVE_IPV6
237 #include <net/if.h>
238 #endif
239 #ifndef HAVE_STDINT_H
240 typedef unsigned int uint32_t;
241 typedef unsigned short uint16_t;
242 #endif
244 #ifdef HAVE_IPV6
245 #define IPTOASIZE (INET6_ADDRSTRLEN+IF_NAMESIZE+INET6_ADDRSTRLEN+IF_NAMESIZE+1)
246 #else
247 #define IPTOASIZE (15+1) /* IPv4 only */
248 #endif
249 static const char *iptoa(const struct sockaddr *ip, char *outstr, size_t s);
250 static uint16_t xsockport(const struct sockaddr *ip);
252 /* Note that mod_reqtimeout has a default configuration of 20 seconds
253 * maximum to wait for the first byte of the initial request line and
254 * then no more than 40 seconds total, but after the first byte is
255 * received the rest must arrive at 500 bytes/sec or faster. That
256 * means 10000 bytes minimum in 40 seconds. We do not allow the
257 * initial Git packet to be longer than PATH_MAX (which is typically
258 * either 1024 or 4096). And since 20 + 1024/500 = 22.048 and
259 * 20 + 4096/500 = 28.192 using 30 seconds for a total timeout is
260 * quite reasonable in comparison to mod_reqtimeout's default conf.
263 #define TIMEOUT_SECS 30 /* no more than 30 seconds for initial packet */
265 #define POLL_QUANTUM 100000U /* how often to poll in microseconds */
267 #if !defined(PATH_MAX) || PATH_MAX+0 < 4096
268 #define BUFF_MAX 4096
269 #else
270 #define BUFF_MAX PATH_MAX
271 #endif
273 /* avoid requiring C99 library */
274 static size_t xstrnlen(const char *s, size_t maxlen)
276 size_t l = 0;
277 if (!s) return l;
278 while (l < maxlen && *s) { ++s; ++l; }
279 return l;
282 /* avoid requiring C99 library */
283 #ifdef HAVE_IPV6
284 static char *xstrncat(char *s1, const char *s2, size_t n)
286 char *p;
287 if (!s1 || !s2 || !n) return s1;
288 p = s1 + strlen(s1);
289 while (n-- && *s2) { *p++ = *s2++; }
290 *p = '\0';
291 return s1;
293 #endif /* HAVE_IPV6 */
295 #define LC(c) (((c)<'A'||(c)>'Z')?(c):((c)+('a'-'A')))
297 /* returns >0 if m1 and m2 are NOT equal comparing first len bytes
298 ** returns 0 if m1 and m2 ARE equal but ignoring case (POSIX locale)
299 ** essentially the same as the non-existent memcasecmp except that only
300 ** a 0 or >0 result is possible and a >0 result only means not-equal */
301 static size_t xmemcaseneql(const char *m1, const char *m2, size_t len)
303 for (; len; --len, ++m1, ++m2) {
304 char c1 = *m1;
305 char c2 = *m2;
306 c1 = LC(c1);
307 c2 = LC(c2);
308 if (c1 != c2) break;
310 return len;
313 static int xdig(char c)
315 if ('0' <= c && c <= '9')
316 return c - '0';
317 if ('a' <= c && c <= 'f')
318 return c - 'a' + 10;
319 if ('A' <= c && c <= 'F')
320 return c - 'A' + 10;
321 return -1;
324 static char buffer[BUFF_MAX];
325 static time_t expiry;
327 /* Ideally we could just use MSG_PEEK + MSG_WAITALL, and that works nicely
328 * on BSD-type distros. Unfortunately very bad things happen on Linux with
329 * that combination -- a CPU core runs at 100% until all the data arrives.
330 * So instead we omit the MSG_WAITALL and poll every POLL_QUANTUM interval
331 * to see if we've satisfied the requested amount yet.
333 static int recv_peekall(int fd, void *buff, size_t len)
335 int ans;
336 while ((ans = recv(fd, buff, len, MSG_PEEK)) > 0 && (size_t)ans < len) {
337 if (time(NULL) > expiry)
338 exit(2);
339 usleep(POLL_QUANTUM);
341 return ans < 0 ? -1 : (int)len;
344 static void handle_sigalrm(int s)
346 (void)s;
347 _exit(2);
350 static void clear_alarm(void)
352 alarm(0);
355 static int parse_host_and_port(char *ptr, size_t zlen, char **host,
356 size_t *hlen, const char **port, size_t *plen);
358 static size_t has_controls(const void *_ptr, size_t zlen);
360 typedef union {
361 struct sockaddr sa;
362 struct sockaddr_in in;
363 #ifdef HAVE_IPV6
364 struct sockaddr_in6 in6;
365 #endif
366 char padding[128];
367 } sockaddr_univ_t;
369 int main(int argc, char *argv[])
371 int len;
372 int xvals[4];
373 char hexlen[4];
374 size_t pktlen, zlen, gitlen, hlen=0, plen=0;
375 char *ptr, *gitcmd, *host=NULL;
376 const char *pktend, *port=NULL;
377 int optval;
378 sockaddr_univ_t sockname;
379 socklen_t socknamelen;
380 char ipstr[IPTOASIZE];
382 (void)argc;
383 (void)argv;
385 /* Ideally calling recv with MSG_PEEK would never, ever hang. However
386 * even with MSG_PEEK, recv still waits for at least the first message
387 * to arrive on the socket (unless it's non-blocking). For this reason
388 * we set an alarm timer at TIMEOUT_SECS + 2 to make sure we don't
389 * remain stuck in the recv call waiting for the first message.
391 signal(SIGALRM, handle_sigalrm);
392 alarm(TIMEOUT_SECS + 2); /* Some slop as this shouldn't be needed */
393 atexit(clear_alarm); /* Probably not necessary, but do it anyway */
395 expiry = time(NULL) + TIMEOUT_SECS;
397 optval = 1;
398 if (setsockopt(0, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)))
399 return 1;
401 len = recv_peekall(0, hexlen, 4);
402 if (len != 4)
403 return 1;
405 if ((xvals[0]=xdig(hexlen[0])) < 0 ||
406 (xvals[1]=xdig(hexlen[1])) < 0 ||
407 (xvals[2]=xdig(hexlen[2])) < 0 ||
408 (xvals[3]=xdig(hexlen[3])) < 0)
409 return 1;
410 pktlen = ((unsigned)xvals[0] << 12) |
411 ((unsigned)xvals[1] << 8) |
412 ((unsigned)xvals[2] << 4) |
413 (unsigned)xvals[3];
414 if (pktlen < 22 || pktlen > sizeof(buffer))
415 return 1;
417 len = recv_peekall(0, buffer, pktlen);
418 if (len != (int)pktlen)
419 return 1;
421 /* skip over 4-byte <PKT-LENGTH> */
422 pktend = buffer + pktlen;
423 ptr = buffer + 4;
425 /* thanks to check above, pktend - ptr always >= 18 */
426 if (memcmp(ptr, "git-", 4)) /* quick sanity check */
427 return 1;
429 /* validate the entire packet format now */
431 /* find length of <GIT-COMMAND> */
432 gitlen = xstrnlen(ptr, pktend - ptr);
433 /* thanks to the quick sanity check, gitlen always >= 4 */
434 gitcmd = ptr;
435 /* skip over <GIT-COMMAND> */
436 ptr += gitlen + 1; /* not a problem if ptr > pktend */
437 if (gitcmd[gitlen-1] == '\n') {
438 /* strip trailing \n from <GIT-COMMAND> */
439 gitcmd[--gitlen] = '\0';
441 if (has_controls(gitcmd, gitlen))
442 return 1; /* bad bytes in command */
444 /* now comes the optional <HOST-ARG> */
445 if (ptr < pktend && (pktend - ptr) >= 5 &&
446 !xmemcaseneql(ptr, "host=", 5)) {
447 /* skip over <HOST-PARAM> part */
448 ptr += 5;
449 zlen = xstrnlen(ptr, pktend - ptr);
450 if (!parse_host_and_port(ptr, zlen, &host, &hlen, &port, &plen))
451 /* failed to parse rest of <HOST-ARG-TRUNCATED> */
452 return 1;
453 /* skip over rest of <HOST-ARG>, okay if ptr ends up > pktend */
454 ptr += zlen + 1;
457 if (ptr < pktend && *ptr)
458 return 1; /* invalid, missing required %x00 before <EXTRA-PARMS> */
459 ++ptr; /* skip over %x00 */
461 /* now skip over the rest of the extra args with minimal validation */
462 while (ptr < pktend) {
463 zlen = xstrnlen(ptr, pktend - ptr);
464 /* if (zlen) process_arg(ptr, zlen); */
465 ptr += zlen + 1; /* okay if ptr ends up > pktend */
468 if (ptr < pktend)
469 return 1; /* not a valid <GIT-REQUEST-PKT> */
471 printf("%.*s\n", (int)gitlen, gitcmd);
472 if (host != NULL)
473 printf("host=%.*s\n", (int)hlen, host);
474 if (port != NULL)
475 printf("port=%.*s\n", (int)plen, port);
477 socknamelen = (socklen_t)sizeof(sockname);
478 if (!getpeername(0, &sockname.sa, &socknamelen) &&
479 iptoa(&sockname.sa, ipstr, sizeof(ipstr)) && ipstr[0]) {
480 uint16_t p = xsockport(&sockname.sa);
481 printf("remote_addr=%s\n", ipstr);
482 if (p)
483 printf("remote_port=%u\n", (unsigned)p);
485 socknamelen = (socklen_t)sizeof(sockname);
486 if (!getsockname(0, &sockname.sa, &socknamelen) &&
487 iptoa(&sockname.sa, ipstr, sizeof(ipstr)) && ipstr[0]) {
488 uint16_t p = xsockport(&sockname.sa);
489 printf("server_addr=%s\n", ipstr);
490 if (p)
491 printf("server_port=%u\n", (unsigned)p);
494 return 0;
497 static size_t has_controls_or_spaces(const void *ptr, size_t zlen);
499 static int parse_host_and_port(char *ptr, size_t zlen, char **host,
500 size_t *hlen, const char **port, size_t *plen)
502 const char *colon = NULL;
503 if (!ptr) return 0; /* bogus ptr argument */
504 if (has_controls_or_spaces(ptr, zlen)) return 0; /* bogus host= value */
505 if (zlen >= 1 && *ptr == '[') {
506 /* IPv6 literal */
507 const char *ebrkt = (const char *)memchr(ptr, ']', zlen);
508 if (!ebrkt) return 0; /* missing closing ']' */
509 *host = ptr + 1;
510 *hlen = ebrkt - ptr - 1; /* yes, could be 0 */
511 if ((size_t)(++ebrkt - ptr) < zlen) {
512 if (*ebrkt != ':') return 0; /* missing ':' after ']' */
513 colon = ebrkt;
515 } else {
516 colon = (const char *)memchr(ptr, ':', zlen);
517 *host = ptr;
518 *hlen = colon ? ((size_t)(colon - ptr)) : zlen;
519 if (*hlen > 1 && ptr[*hlen - 1] == '.')
520 --*hlen;
522 if (colon) {
523 zlen = (ptr + zlen) - ++colon;
524 if (zlen > 5) return 0; /* invalid port number */
525 if (zlen == 0) {
526 /* empty port */
527 *port = colon;
528 *plen = 0;
529 } else {
530 unsigned pval;
531 const char *pptr;
532 size_t pl;
533 while (zlen > 1 && *colon == '0') {
534 ++colon;
535 --zlen;
537 pptr = colon;
538 pl = zlen;
539 pval = 0;
540 while (zlen) {
541 if (*colon < '0' || *colon > '9')
542 return 0; /* invalid port number */
543 pval *= 10;
544 pval += (*colon++) - '0';
545 --zlen;
547 if (!pval || pval > 65535)
548 return 0; /* invalid port number */
549 *port = pptr;
550 *plen = pl;
552 } else {
553 *port = NULL;
554 *plen = 0;
556 ptr = *host;
557 zlen = *hlen;
558 while (zlen) {
559 char c = *ptr;
560 c = LC(c);
561 *ptr++ = c;
562 --zlen;
564 return 1;
567 /* the tab character %x09 is not considered a control here */
568 static size_t has_controls(const void *_ptr, size_t zlen)
570 const unsigned char *ptr = (const unsigned char *)_ptr;
571 if (!ptr) return 0;
572 while (zlen && (*ptr >= ' ' || *ptr == '\t')) {
573 ++ptr;
574 --zlen;
576 return zlen;
579 static size_t has_controls_or_spaces(const void *_ptr, size_t zlen)
581 const unsigned char *ptr = (const unsigned char *)_ptr;
582 if (!ptr) return 0;
583 while (zlen && *ptr > ' ') {
584 ++ptr;
585 --zlen;
587 return zlen;
590 static const char *iptoa(const struct sockaddr *ip, char *outstr, size_t s)
592 if (outstr)
593 *outstr = '\0';
594 if (ip && outstr && s) {
595 if (ip->sa_family == AF_INET) {
596 const struct sockaddr_in *sin = (const struct sockaddr_in *)ip;
597 inet_ntop(AF_INET, &sin->sin_addr, outstr, (socklen_t)s);
599 #if defined(HAVE_IPV6) && defined(HAVE_INET_NTOP)
600 else if (ip->sa_family == AF_INET6) {
601 const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)ip;
602 inet_ntop(AF_INET6, sin6->sin6_addr.s6_addr, outstr, (socklen_t)s);
603 if (sin6->sin6_scope_id) {
604 size_t outlen = strlen(outstr);
605 #if IF_NAMESIZE < 9
606 #define XIF_NAMESIZE 9
607 #else
608 #define XIF_NAMESIZE IF_NAMESIZE
609 #endif
610 char scope[XIF_NAMESIZE+1];
611 char *ifname = if_indextoname(sin6->sin6_scope_id, scope+1);
612 if (ifname) {
613 scope[0] = '%';
614 } else {
615 /* This can happen on odd systems */
616 #ifdef HAVE_SNPRINTF
617 snprintf(scope, sizeof(scope), "%%%d", (int)sin6->sin6_scope_id);
618 #else
619 /* 0xFFFFFFFF only requires 9 digits and XIF_NAMESIZE is >= 9
620 * therefore it's guaranteed to fit and not overflow */
621 sprintf(scope, "%%%u", (unsigned)(sin6->sin6_scope_id & 0xFFFFFFFF));
622 #endif
624 scope[sizeof(scope)-1] = 0;
625 if (outlen+1 < s)
626 xstrncat(outstr, scope, s-outlen-1);
629 #endif /* HAVE_IPV6 */
631 return outstr;
634 static uint16_t xsockport(const struct sockaddr *ip)
636 if (ip->sa_family == AF_INET) {
637 const struct sockaddr_in *sin = (const struct sockaddr_in *)ip;
638 return (uint16_t)ntohs(sin->sin_port);
640 #ifdef HAVE_IPV6
641 if (ip->sa_family == AF_INET6) {
642 const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)ip;
643 return (uint16_t)ntohs(sin6->sin6_port);
645 #endif
646 return 0;