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:
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:
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
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
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
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
112 If the local address is available (getsockname) then two lines of the form
114 server_addr=<address>
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
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
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
185 HOST-NAME = NON-NULL-BYTES ; should be a valid DNS name
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 /
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
215 #define HAVE_INET_NTOP 1
216 #define HAVE_SNPRINTF 1
232 #include <sys/types.h>
233 #include <sys/socket.h>
234 #include <netinet/in.h>
235 #include <arpa/inet.h>
239 #ifndef HAVE_STDINT_H
240 typedef unsigned int uint32_t;
241 typedef unsigned short uint16_t;
245 #define IPTOASIZE (INET6_ADDRSTRLEN+IF_NAMESIZE+INET6_ADDRSTRLEN+IF_NAMESIZE+1)
247 #define IPTOASIZE (15+1) /* IPv4 only */
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
270 #define BUFF_MAX PATH_MAX
273 /* avoid requiring C99 library */
274 static size_t xstrnlen(const char *s
, size_t maxlen
)
278 while (l
< maxlen
&& *s
) { ++s
; ++l
; }
282 /* avoid requiring C99 library */
284 static char *xstrncat(char *s1
, const char *s2
, size_t n
)
287 if (!s1
|| !s2
|| !n
) return s1
;
289 while (n
-- && *s2
) { *p
++ = *s2
++; }
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
) {
313 static int xdig(char c
)
315 if ('0' <= c
&& c
<= '9')
317 if ('a' <= c
&& c
<= 'f')
319 if ('A' <= c
&& c
<= 'F')
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
)
336 while ((ans
= recv(fd
, buff
, len
, MSG_PEEK
)) > 0 && (size_t)ans
< len
) {
337 if (time(NULL
) > expiry
)
339 usleep(POLL_QUANTUM
);
341 return ans
< 0 ? -1 : (int)len
;
344 static void handle_sigalrm(int s
)
350 static void clear_alarm(void)
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
);
362 struct sockaddr_in in
;
364 struct sockaddr_in6 in6
;
369 int main(int argc
, char *argv
[])
374 size_t pktlen
, zlen
, gitlen
, hlen
=0, plen
=0;
375 char *ptr
, *gitcmd
, *host
=NULL
;
376 const char *pktend
, *port
=NULL
;
378 sockaddr_univ_t sockname
;
379 socklen_t socknamelen
;
380 char ipstr
[IPTOASIZE
];
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
;
398 if (setsockopt(0, SOL_SOCKET
, SO_KEEPALIVE
, &optval
, sizeof(optval
)))
401 len
= recv_peekall(0, hexlen
, 4);
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)
410 pktlen
= ((unsigned)xvals
[0] << 12) |
411 ((unsigned)xvals
[1] << 8) |
412 ((unsigned)xvals
[2] << 4) |
414 if (pktlen
< 22 || pktlen
> sizeof(buffer
))
417 len
= recv_peekall(0, buffer
, pktlen
);
418 if (len
!= (int)pktlen
)
421 /* skip over 4-byte <PKT-LENGTH> */
422 pktend
= buffer
+ pktlen
;
425 /* thanks to check above, pktend - ptr always >= 18 */
426 if (memcmp(ptr
, "git-", 4)) /* quick sanity check */
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 */
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 */
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> */
453 /* skip over rest of <HOST-ARG>, okay if ptr ends up > pktend */
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 */
469 return 1; /* not a valid <GIT-REQUEST-PKT> */
471 printf("%.*s\n", (int)gitlen
, gitcmd
);
473 printf("host=%.*s\n", (int)hlen
, host
);
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
);
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
);
491 printf("server_port=%u\n", (unsigned)p
);
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
== '[') {
507 const char *ebrkt
= (const char *)memchr(ptr
, ']', zlen
);
508 if (!ebrkt
) return 0; /* missing closing ']' */
510 *hlen
= ebrkt
- ptr
- 1; /* yes, could be 0 */
511 if ((size_t)(++ebrkt
- ptr
) < zlen
) {
512 if (*ebrkt
!= ':') return 0; /* missing ':' after ']' */
516 colon
= (const char *)memchr(ptr
, ':', zlen
);
518 *hlen
= colon
? ((size_t)(colon
- ptr
)) : zlen
;
519 if (*hlen
> 1 && ptr
[*hlen
- 1] == '.')
523 zlen
= (ptr
+ zlen
) - ++colon
;
524 if (zlen
> 5) return 0; /* invalid port number */
533 while (zlen
> 1 && *colon
== '0') {
541 if (*colon
< '0' || *colon
> '9')
542 return 0; /* invalid port number */
544 pval
+= (*colon
++) - '0';
547 if (!pval
|| pval
> 65535)
548 return 0; /* invalid port number */
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
;
572 while (zlen
&& (*ptr
>= ' ' || *ptr
== '\t')) {
579 static size_t has_controls_or_spaces(const void *_ptr
, size_t zlen
)
581 const unsigned char *ptr
= (const unsigned char *)_ptr
;
583 while (zlen
&& *ptr
> ' ') {
590 static const char *iptoa(const struct sockaddr
*ip
, char *outstr
, size_t s
)
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
);
606 #define XIF_NAMESIZE 9
608 #define XIF_NAMESIZE IF_NAMESIZE
610 char scope
[XIF_NAMESIZE
+1];
611 char *ifname
= if_indextoname(sin6
->sin6_scope_id
, scope
+1);
615 /* This can happen on odd systems */
617 snprintf(scope
, sizeof(scope
), "%%%d", (int)sin6
->sin6_scope_id
);
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));
624 scope
[sizeof(scope
)-1] = 0;
626 xstrncat(outstr
, scope
, s
-outlen
-1);
629 #endif /* HAVE_IPV6 */
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
);
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
);