ViewVC Help
View File | Revision Log | Show Annotations | Download File | View Changeset | Root Listing
root/src/trunk/usr.sbin/portsnap/phttpget/phttpget.c
Revision: 12167
Committed: Sat Feb 9 18:20:51 2019 UTC (5 years, 2 months ago) by laffer1
Content type: text/plain
File size: 18385 byte(s)
Log Message:
add portsnap

File Contents

# Content
1 /* $MidnightBSD$ */
2 /*-
3 * Copyright 2005 Colin Percival
4 * All rights reserved
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted providing that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
19 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
23 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
24 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 * POSSIBILITY OF SUCH DAMAGE.
26 */
27
28 #include <sys/cdefs.h>
29 __FBSDID("$FreeBSD: stable/10/usr.sbin/portsnap/phttpget/phttpget.c 306428 2016-09-29 01:53:29Z emaste $");
30
31 #include <sys/types.h>
32 #include <sys/time.h>
33 #include <sys/socket.h>
34
35 #include <ctype.h>
36 #include <err.h>
37 #include <errno.h>
38 #include <fcntl.h>
39 #include <limits.h>
40 #include <netdb.h>
41 #include <stdint.h>
42 #include <stdio.h>
43 #include <stdlib.h>
44 #include <string.h>
45 #include <sysexits.h>
46 #include <unistd.h>
47
48 static const char * env_HTTP_PROXY;
49 static char * env_HTTP_PROXY_AUTH;
50 static const char * env_HTTP_USER_AGENT;
51 static char * env_HTTP_TIMEOUT;
52 static const char * proxyport;
53 static char * proxyauth;
54
55 static struct timeval timo = { 15, 0};
56
57 static void
58 usage(void)
59 {
60
61 fprintf(stderr, "usage: phttpget server [file ...]\n");
62 exit(EX_USAGE);
63 }
64
65 /*
66 * Base64 encode a string; the string returned, if non-NULL, is
67 * allocated using malloc() and must be freed by the caller.
68 */
69 static char *
70 b64enc(const char *ptext)
71 {
72 static const char base64[] =
73 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
74 "abcdefghijklmnopqrstuvwxyz"
75 "0123456789+/";
76 const char *pt;
77 char *ctext, *pc;
78 size_t ptlen, ctlen;
79 uint32_t t;
80 unsigned int j;
81
82 /*
83 * Encoded length is 4 characters per 3-byte block or partial
84 * block of plaintext, plus one byte for the terminating NUL
85 */
86 ptlen = strlen(ptext);
87 if (ptlen > ((SIZE_MAX - 1) / 4) * 3 - 2)
88 return NULL; /* Possible integer overflow */
89 ctlen = 4 * ((ptlen + 2) / 3) + 1;
90 if ((ctext = malloc(ctlen)) == NULL)
91 return NULL;
92 ctext[ctlen - 1] = 0;
93
94 /*
95 * Scan through ptext, reading up to 3 bytes from ptext and
96 * writing 4 bytes to ctext, until we run out of input.
97 */
98 for (pt = ptext, pc = ctext; ptlen; ptlen -= 3, pc += 4) {
99 /* Read 3 bytes */
100 for (t = j = 0; j < 3; j++) {
101 t <<= 8;
102 if (j < ptlen)
103 t += *pt++;
104 }
105
106 /* Write 4 bytes */
107 for (j = 0; j < 4; j++) {
108 if (j <= ptlen + 1)
109 pc[j] = base64[(t >> 18) & 0x3f];
110 else
111 pc[j] = '=';
112 t <<= 6;
113 }
114
115 /* If we're done, exit the loop */
116 if (ptlen <= 3)
117 break;
118 }
119
120 return (ctext);
121 }
122
123 static void
124 readenv(void)
125 {
126 char *proxy_auth_userpass, *proxy_auth_userpass64, *p;
127 char *proxy_auth_user = NULL;
128 char *proxy_auth_pass = NULL;
129 long http_timeout;
130
131 env_HTTP_PROXY = getenv("HTTP_PROXY");
132 if (env_HTTP_PROXY == NULL)
133 env_HTTP_PROXY = getenv("http_proxy");
134 if (env_HTTP_PROXY != NULL) {
135 if (strncmp(env_HTTP_PROXY, "http://", 7) == 0)
136 env_HTTP_PROXY += 7;
137 p = strchr(env_HTTP_PROXY, '/');
138 if (p != NULL)
139 *p = 0;
140 p = strchr(env_HTTP_PROXY, ':');
141 if (p != NULL) {
142 *p = 0;
143 proxyport = p + 1;
144 } else
145 proxyport = "3128";
146 }
147
148 env_HTTP_PROXY_AUTH = getenv("HTTP_PROXY_AUTH");
149 if ((env_HTTP_PROXY != NULL) &&
150 (env_HTTP_PROXY_AUTH != NULL) &&
151 (strncasecmp(env_HTTP_PROXY_AUTH, "basic:" , 6) == 0)) {
152 /* Ignore authentication scheme */
153 (void) strsep(&env_HTTP_PROXY_AUTH, ":");
154
155 /* Ignore realm */
156 (void) strsep(&env_HTTP_PROXY_AUTH, ":");
157
158 /* Obtain username and password */
159 proxy_auth_user = strsep(&env_HTTP_PROXY_AUTH, ":");
160 proxy_auth_pass = env_HTTP_PROXY_AUTH;
161 }
162
163 if ((proxy_auth_user != NULL) && (proxy_auth_pass != NULL)) {
164 asprintf(&proxy_auth_userpass, "%s:%s",
165 proxy_auth_user, proxy_auth_pass);
166 if (proxy_auth_userpass == NULL)
167 err(1, "asprintf");
168
169 proxy_auth_userpass64 = b64enc(proxy_auth_userpass);
170 if (proxy_auth_userpass64 == NULL)
171 err(1, "malloc");
172
173 asprintf(&proxyauth, "Proxy-Authorization: Basic %s\r\n",
174 proxy_auth_userpass64);
175 if (proxyauth == NULL)
176 err(1, "asprintf");
177
178 free(proxy_auth_userpass);
179 free(proxy_auth_userpass64);
180 } else
181 proxyauth = NULL;
182
183 env_HTTP_USER_AGENT = getenv("HTTP_USER_AGENT");
184 if (env_HTTP_USER_AGENT == NULL)
185 env_HTTP_USER_AGENT = "phttpget/0.1";
186
187 env_HTTP_TIMEOUT = getenv("HTTP_TIMEOUT");
188 if (env_HTTP_TIMEOUT != NULL) {
189 http_timeout = strtol(env_HTTP_TIMEOUT, &p, 10);
190 if ((*env_HTTP_TIMEOUT == '\0') || (*p != '\0') ||
191 (http_timeout < 0))
192 warnx("HTTP_TIMEOUT (%s) is not a positive integer",
193 env_HTTP_TIMEOUT);
194 else
195 timo.tv_sec = http_timeout;
196 }
197 }
198
199 static int
200 makerequest(char ** buf, char * path, char * server, int connclose)
201 {
202 int buflen;
203
204 buflen = asprintf(buf,
205 "GET %s%s/%s HTTP/1.1\r\n"
206 "Host: %s\r\n"
207 "User-Agent: %s\r\n"
208 "%s"
209 "%s"
210 "\r\n",
211 env_HTTP_PROXY ? "http://" : "",
212 env_HTTP_PROXY ? server : "",
213 path, server, env_HTTP_USER_AGENT,
214 proxyauth ? proxyauth : "",
215 connclose ? "Connection: Close\r\n" : "Connection: Keep-Alive\r\n");
216 if (buflen == -1)
217 err(1, "asprintf");
218 return(buflen);
219 }
220
221 static int
222 readln(int sd, char * resbuf, int * resbuflen, int * resbufpos)
223 {
224 ssize_t len;
225
226 while (strnstr(resbuf + *resbufpos, "\r\n",
227 *resbuflen - *resbufpos) == NULL) {
228 /* Move buffered data to the start of the buffer */
229 if (*resbufpos != 0) {
230 memmove(resbuf, resbuf + *resbufpos,
231 *resbuflen - *resbufpos);
232 *resbuflen -= *resbufpos;
233 *resbufpos = 0;
234 }
235
236 /* If the buffer is full, complain */
237 if (*resbuflen == BUFSIZ)
238 return -1;
239
240 /* Read more data into the buffer */
241 len = recv(sd, resbuf + *resbuflen, BUFSIZ - *resbuflen, 0);
242 if ((len == 0) ||
243 ((len == -1) && (errno != EINTR)))
244 return -1;
245
246 if (len != -1)
247 *resbuflen += len;
248 }
249
250 return 0;
251 }
252
253 static int
254 copybytes(int sd, int fd, off_t copylen, char * resbuf, int * resbuflen,
255 int * resbufpos)
256 {
257 ssize_t len;
258
259 while (copylen) {
260 /* Write data from resbuf to fd */
261 len = *resbuflen - *resbufpos;
262 if (copylen < len)
263 len = copylen;
264 if (len > 0) {
265 if (fd != -1)
266 len = write(fd, resbuf + *resbufpos, len);
267 if (len == -1)
268 err(1, "write");
269 *resbufpos += len;
270 copylen -= len;
271 continue;
272 }
273
274 /* Read more data into buffer */
275 len = recv(sd, resbuf, BUFSIZ, 0);
276 if (len == -1) {
277 if (errno == EINTR)
278 continue;
279 return -1;
280 } else if (len == 0) {
281 return -2;
282 } else {
283 *resbuflen = len;
284 *resbufpos = 0;
285 }
286 }
287
288 return 0;
289 }
290
291 int
292 main(int argc, char *argv[])
293 {
294 struct addrinfo hints; /* Hints to getaddrinfo */
295 struct addrinfo *res; /* Pointer to server address being used */
296 struct addrinfo *res0; /* Pointer to server addresses */
297 char * resbuf = NULL; /* Response buffer */
298 int resbufpos = 0; /* Response buffer position */
299 int resbuflen = 0; /* Response buffer length */
300 char * eolp; /* Pointer to "\r\n" within resbuf */
301 char * hln; /* Pointer within header line */
302 char * servername; /* Name of server */
303 char * fname = NULL; /* Name of downloaded file */
304 char * reqbuf = NULL; /* Request buffer */
305 int reqbufpos = 0; /* Request buffer position */
306 int reqbuflen = 0; /* Request buffer length */
307 ssize_t len; /* Length sent or received */
308 int nreq = 0; /* Number of next request to send */
309 int nres = 0; /* Number of next reply to receive */
310 int pipelined = 0; /* != 0 if connection in pipelined mode. */
311 int keepalive; /* != 0 if HTTP/1.0 keep-alive rcvd. */
312 int sd = -1; /* Socket descriptor */
313 int sdflags = 0; /* Flags on the socket sd */
314 int fd = -1; /* Descriptor for download target file */
315 int error; /* Error code */
316 int statuscode; /* HTTP Status code */
317 off_t contentlength; /* Value from Content-Length header */
318 int chunked; /* != if transfer-encoding is chunked */
319 off_t clen; /* Chunk length */
320 int firstreq = 0; /* # of first request for this connection */
321 int val; /* Value used for setsockopt call */
322
323 /* Check that the arguments are sensible */
324 if (argc < 2)
325 usage();
326
327 /* Read important environment variables */
328 readenv();
329
330 /* Get server name and adjust arg[cv] to point at file names */
331 servername = argv[1];
332 argv += 2;
333 argc -= 2;
334
335 /* Allocate response buffer */
336 resbuf = malloc(BUFSIZ);
337 if (resbuf == NULL)
338 err(1, "malloc");
339
340 /* Look up server */
341 memset(&hints, 0, sizeof(hints));
342 hints.ai_family = PF_UNSPEC;
343 hints.ai_socktype = SOCK_STREAM;
344 error = getaddrinfo(env_HTTP_PROXY ? env_HTTP_PROXY : servername,
345 env_HTTP_PROXY ? proxyport : "http", &hints, &res0);
346 if (error)
347 errx(1, "host = %s, port = %s: %s",
348 env_HTTP_PROXY ? env_HTTP_PROXY : servername,
349 env_HTTP_PROXY ? proxyport : "http",
350 gai_strerror(error));
351 if (res0 == NULL)
352 errx(1, "could not look up %s", servername);
353 res = res0;
354
355 /* Do the fetching */
356 while (nres < argc) {
357 /* Make sure we have a connected socket */
358 for (; sd == -1; res = res->ai_next) {
359 /* No addresses left to try :-( */
360 if (res == NULL)
361 errx(1, "Could not connect to %s", servername);
362
363 /* Create a socket... */
364 sd = socket(res->ai_family, res->ai_socktype,
365 res->ai_protocol);
366 if (sd == -1)
367 continue;
368
369 /* ... set 15-second timeouts ... */
370 setsockopt(sd, SOL_SOCKET, SO_SNDTIMEO,
371 (void *)&timo, (socklen_t)sizeof(timo));
372 setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO,
373 (void *)&timo, (socklen_t)sizeof(timo));
374
375 /* ... disable SIGPIPE generation ... */
376 val = 1;
377 setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE,
378 (void *)&val, sizeof(int));
379
380 /* ... and connect to the server. */
381 if(connect(sd, res->ai_addr, res->ai_addrlen)) {
382 close(sd);
383 sd = -1;
384 continue;
385 }
386
387 firstreq = nres;
388 }
389
390 /*
391 * If in pipelined HTTP mode, put socket into non-blocking
392 * mode, since we're probably going to want to try to send
393 * several HTTP requests.
394 */
395 if (pipelined) {
396 sdflags = fcntl(sd, F_GETFL);
397 if (fcntl(sd, F_SETFL, sdflags | O_NONBLOCK) == -1)
398 err(1, "fcntl");
399 }
400
401 /* Construct requests and/or send them without blocking */
402 while ((nreq < argc) && ((reqbuf == NULL) || pipelined)) {
403 /* If not in the middle of a request, make one */
404 if (reqbuf == NULL) {
405 reqbuflen = makerequest(&reqbuf, argv[nreq],
406 servername, (nreq == argc - 1));
407 reqbufpos = 0;
408 }
409
410 /* If in pipelined mode, try to send the request */
411 if (pipelined) {
412 while (reqbufpos < reqbuflen) {
413 len = send(sd, reqbuf + reqbufpos,
414 reqbuflen - reqbufpos, 0);
415 if (len == -1)
416 break;
417 reqbufpos += len;
418 }
419 if (reqbufpos < reqbuflen) {
420 if (errno != EAGAIN)
421 goto conndied;
422 break;
423 } else {
424 free(reqbuf);
425 reqbuf = NULL;
426 nreq++;
427 }
428 }
429 }
430
431 /* Put connection back into blocking mode */
432 if (pipelined) {
433 if (fcntl(sd, F_SETFL, sdflags) == -1)
434 err(1, "fcntl");
435 }
436
437 /* Do we need to blocking-send a request? */
438 if (nres == nreq) {
439 while (reqbufpos < reqbuflen) {
440 len = send(sd, reqbuf + reqbufpos,
441 reqbuflen - reqbufpos, 0);
442 if (len == -1)
443 goto conndied;
444 reqbufpos += len;
445 }
446 free(reqbuf);
447 reqbuf = NULL;
448 nreq++;
449 }
450
451 /* Scan through the response processing headers. */
452 statuscode = 0;
453 contentlength = -1;
454 chunked = 0;
455 keepalive = 0;
456 do {
457 /* Get a header line */
458 error = readln(sd, resbuf, &resbuflen, &resbufpos);
459 if (error)
460 goto conndied;
461 hln = resbuf + resbufpos;
462 eolp = strnstr(hln, "\r\n", resbuflen - resbufpos);
463 resbufpos = (eolp - resbuf) + 2;
464 *eolp = '\0';
465
466 /* Make sure it doesn't contain a NUL character */
467 if (strchr(hln, '\0') != eolp)
468 goto conndied;
469
470 if (statuscode == 0) {
471 /* The first line MUST be HTTP/1.x xxx ... */
472 if ((strncmp(hln, "HTTP/1.", 7) != 0) ||
473 ! isdigit(hln[7]))
474 goto conndied;
475
476 /*
477 * If the minor version number isn't zero,
478 * then we can assume that pipelining our
479 * requests is OK -- as long as we don't
480 * see a "Connection: close" line later
481 * and we either have a Content-Length or
482 * Transfer-Encoding: chunked header to
483 * tell us the length.
484 */
485 if (hln[7] != '0')
486 pipelined = 1;
487
488 /* Skip over the minor version number */
489 hln = strchr(hln + 7, ' ');
490 if (hln == NULL)
491 goto conndied;
492 else
493 hln++;
494
495 /* Read the status code */
496 while (isdigit(*hln)) {
497 statuscode = statuscode * 10 +
498 *hln - '0';
499 hln++;
500 }
501
502 if (statuscode < 100 || statuscode > 599)
503 goto conndied;
504
505 /* Ignore the rest of the line */
506 continue;
507 }
508
509 /*
510 * Check for "Connection: close" or
511 * "Connection: Keep-Alive" header
512 */
513 if (strncasecmp(hln, "Connection:", 11) == 0) {
514 hln += 11;
515 if (strcasestr(hln, "close") != NULL)
516 pipelined = 0;
517 if (strcasestr(hln, "Keep-Alive") != NULL)
518 keepalive = 1;
519
520 /* Next header... */
521 continue;
522 }
523
524 /* Check for "Content-Length:" header */
525 if (strncasecmp(hln, "Content-Length:", 15) == 0) {
526 hln += 15;
527 contentlength = 0;
528
529 /* Find the start of the length */
530 while (!isdigit(*hln) && (*hln != '\0'))
531 hln++;
532
533 /* Compute the length */
534 while (isdigit(*hln)) {
535 if (contentlength >= OFF_MAX / 10) {
536 /* Nasty people... */
537 goto conndied;
538 }
539 contentlength = contentlength * 10 +
540 *hln - '0';
541 hln++;
542 }
543
544 /* Next header... */
545 continue;
546 }
547
548 /* Check for "Transfer-Encoding: chunked" header */
549 if (strncasecmp(hln, "Transfer-Encoding:", 18) == 0) {
550 hln += 18;
551 if (strcasestr(hln, "chunked") != NULL)
552 chunked = 1;
553
554 /* Next header... */
555 continue;
556 }
557
558 /* We blithely ignore any other header lines */
559
560 /* No more header lines */
561 if (strlen(hln) == 0) {
562 /*
563 * If the status code was 1xx, then there will
564 * be a real header later. Servers may emit
565 * 1xx header blocks at will, but since we
566 * don't expect one, we should just ignore it.
567 */
568 if (100 <= statuscode && statuscode <= 199) {
569 statuscode = 0;
570 continue;
571 }
572
573 /* End of header; message body follows */
574 break;
575 }
576 } while (1);
577
578 /* No message body for 204 or 304 */
579 if (statuscode == 204 || statuscode == 304) {
580 nres++;
581 continue;
582 }
583
584 /*
585 * There should be a message body coming, but we only want
586 * to send it to a file if the status code is 200
587 */
588 if (statuscode == 200) {
589 /* Generate a file name for the download */
590 fname = strrchr(argv[nres], '/');
591 if (fname == NULL)
592 fname = argv[nres];
593 else
594 fname++;
595 if (strlen(fname) == 0)
596 errx(1, "Cannot obtain file name from %s\n",
597 argv[nres]);
598
599 fd = open(fname, O_CREAT | O_TRUNC | O_WRONLY, 0644);
600 if (fd == -1)
601 errx(1, "open(%s)", fname);
602 }
603
604 /* Read the message and send data to fd if appropriate */
605 if (chunked) {
606 /* Handle a chunked-encoded entity */
607
608 /* Read chunks */
609 do {
610 error = readln(sd, resbuf, &resbuflen,
611 &resbufpos);
612 if (error)
613 goto conndied;
614 hln = resbuf + resbufpos;
615 eolp = strstr(hln, "\r\n");
616 resbufpos = (eolp - resbuf) + 2;
617
618 clen = 0;
619 while (isxdigit(*hln)) {
620 if (clen >= OFF_MAX / 16) {
621 /* Nasty people... */
622 goto conndied;
623 }
624 if (isdigit(*hln))
625 clen = clen * 16 + *hln - '0';
626 else
627 clen = clen * 16 + 10 +
628 tolower(*hln) - 'a';
629 hln++;
630 }
631
632 error = copybytes(sd, fd, clen, resbuf,
633 &resbuflen, &resbufpos);
634 if (error) {
635 goto conndied;
636 }
637 } while (clen != 0);
638
639 /* Read trailer and final CRLF */
640 do {
641 error = readln(sd, resbuf, &resbuflen,
642 &resbufpos);
643 if (error)
644 goto conndied;
645 hln = resbuf + resbufpos;
646 eolp = strstr(hln, "\r\n");
647 resbufpos = (eolp - resbuf) + 2;
648 } while (hln != eolp);
649 } else if (contentlength != -1) {
650 error = copybytes(sd, fd, contentlength, resbuf,
651 &resbuflen, &resbufpos);
652 if (error)
653 goto conndied;
654 } else {
655 /*
656 * Not chunked, and no content length header.
657 * Read everything until the server closes the
658 * socket.
659 */
660 error = copybytes(sd, fd, OFF_MAX, resbuf,
661 &resbuflen, &resbufpos);
662 if (error == -1)
663 goto conndied;
664 pipelined = 0;
665 }
666
667 if (fd != -1) {
668 close(fd);
669 fd = -1;
670 }
671
672 fprintf(stderr, "http://%s/%s: %d ", servername, argv[nres],
673 statuscode);
674 if (statuscode == 200)
675 fprintf(stderr, "OK\n");
676 else if (statuscode < 300)
677 fprintf(stderr, "Successful (ignored)\n");
678 else if (statuscode < 400)
679 fprintf(stderr, "Redirection (ignored)\n");
680 else
681 fprintf(stderr, "Error (ignored)\n");
682
683 /* We've finished this file! */
684 nres++;
685
686 /*
687 * If necessary, clean up this connection so that we
688 * can start a new one.
689 */
690 if (pipelined == 0 && keepalive == 0)
691 goto cleanupconn;
692 continue;
693
694 conndied:
695 /*
696 * Something went wrong -- our connection died, the server
697 * sent us garbage, etc. If this happened on the first
698 * request we sent over this connection, give up. Otherwise,
699 * close this connection, open a new one, and reissue the
700 * request.
701 */
702 if (nres == firstreq)
703 errx(1, "Connection failure");
704
705 cleanupconn:
706 /*
707 * Clean up our connection and keep on going
708 */
709 shutdown(sd, SHUT_RDWR);
710 close(sd);
711 sd = -1;
712 if (fd != -1) {
713 close(fd);
714 fd = -1;
715 }
716 if (reqbuf != NULL) {
717 free(reqbuf);
718 reqbuf = NULL;
719 }
720 nreq = nres;
721 res = res0;
722 pipelined = 0;
723 resbufpos = resbuflen = 0;
724 continue;
725 }
726
727 free(resbuf);
728 freeaddrinfo(res0);
729
730 return 0;
731 }

Properties

Name Value
svn:eol-style native
svn:keywords MidnightBSD=%H
svn:mime-type text/plain