chiark / gitweb /
Suppress no-collection log message for scratches
[disorder] / lib / mime.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2005, 2007, 2008 Richard Kettlewell
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  * 
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 /** @file lib/mime.c
19  * @brief Support for MIME and allied protocols
20  */
21
22 #include "common.h"
23
24 #include <ctype.h>
25
26 #include "mem.h"
27 #include "mime.h"
28 #include "vector.h"
29 #include "hex.h"
30 #include "log.h"
31 #include "base64.h"
32 #include "kvp.h"
33
34 /** @brief Match whitespace characters */
35 static int whitespace(int c) {
36   switch(c) {
37   case ' ':
38   case '\t':
39   case '\r':
40   case '\n':
41     return 1;
42   default:
43     return 0;
44   }
45 }
46
47 /** @brief Match RFC2045 tspecial characters */
48 int mime_tspecial(int c) {
49   switch(c) {
50   case '(':
51   case ')':
52   case '<':
53   case '>':
54   case '@':
55   case ',':
56   case ';':
57   case ':':
58   case '\\':
59   case '"':
60   case '/':
61   case '[':
62   case ']':
63   case '?':
64   case '=':
65     return 1;
66   default:
67     return 0;
68   }
69 }
70
71 /** @brief Match RFC2616 separator characters */
72 int mime_http_separator(int c) {
73   switch(c) {
74   case '(':
75   case ')':
76   case '<':
77   case '>':
78   case '@':
79   case ',':
80   case ';':
81   case ':':
82   case '\\':
83   case '"':
84   case '/':
85   case '[':
86   case ']':
87   case '?':
88   case '=':
89   case '{':
90   case '}':
91   case ' ':
92   case '\t':
93     return 1;
94   default:
95     return 0;
96   }
97 }
98
99 /** @brief Match CRLF */
100 static int iscrlf(const char *ptr) {
101   return ptr[0] == '\r' && ptr[1] == '\n';
102 }
103
104 /** @brief Skip whitespace
105  * @param s Pointer into string
106  * @param rfc822_comments If true, skip RFC822 nested comments
107  * @return Pointer into string after whitespace
108  */
109 static const char *skipwhite(const char *s, int rfc822_comments) {
110   int c, depth;
111   
112   for(;;) {
113     switch(c = *s) {
114     case ' ':
115     case '\t':
116     case '\r':
117     case '\n':
118       ++s;
119       break;
120     case '(':
121       if(!rfc822_comments)
122         return s;
123       ++s;
124       depth = 1;
125       while(*s && depth) {
126         c = *s++;
127         switch(c) {
128         case '(': ++depth; break;
129         case ')': --depth; break;
130         case '\\':
131           if(!*s)
132             return 0;
133           ++s;
134           break;
135         }
136       }
137       if(depth)
138         return 0;
139       break;
140     default:
141       return s;
142     }
143   }
144 }
145
146 /** @brief Test for a word character
147  * @param c Character to test
148  * @param special mime_tspecial() (MIME/RFC2405) or mime_http_separator() (HTTP/RFC2616)
149  * @return 1 if @p c is a word character, else 0
150  */
151 static int iswordchar(int c, int (*special)(int)) {
152   return !(c <= ' ' || c > '~' || special(c));
153 }
154
155 /** @brief Parse an RFC1521/RFC2616 word
156  * @param s Pointer to start of word
157  * @param valuep Where to store value
158  * @param special mime_tspecial() (MIME/RFC2405) or mime_http_separator() (HTTP/RFC2616)
159  * @return Pointer just after end of word or NULL if there's no word
160  *
161  * A word is a token or a quoted-string.
162  */
163 const char *mime_parse_word(const char *s, char **valuep,
164                             int (*special)(int)) {
165   struct dynstr value[1];
166   int c;
167
168   dynstr_init(value);
169   if(*s == '"') {
170     ++s;
171     while((c = *s++) != '"') {
172       switch(c) {
173       case '\\':
174         if(!(c = *s++))
175           return 0;
176       default:
177         dynstr_append(value, c);
178         break;
179       }
180     }
181     if(!c)
182       return 0;
183   } else {
184     if(!iswordchar((unsigned char)*s, special))
185       return NULL;
186     dynstr_init(value);
187     while(iswordchar((unsigned char)*s, special))
188       dynstr_append(value, *s++);
189   }
190   dynstr_terminate(value);
191   *valuep = value->vec;
192   return s;
193 }
194
195 /** @brief Parse an RFC1521/RFC2616 token
196  * @param s Pointer to start of token
197  * @param valuep Where to store value
198  * @param special mime_tspecial() (MIME/RFC2405) or mime_http_separator() (HTTP/RFC2616)
199  * @return Pointer just after end of token or NULL if there's no token
200  */
201 static const char *parsetoken(const char *s, char **valuep,
202                               int (*special)(int)) {
203   if(*s == '"')
204     return 0;
205   return mime_parse_word(s, valuep, special);
206 }
207
208 /** @brief Parse a MIME content-type field
209  * @param s Start of field
210  * @param typep Where to store type
211  * @param parametersp Where to store parameter list
212  * @return 0 on success, non-0 on error
213  *
214  * See <a href="http://tools.ietf.org/html/rfc2045#section-5">RFC 2045 s5</a>.
215  */
216 int mime_content_type(const char *s,
217                       char **typep,
218                       struct kvp **parametersp) {
219   struct dynstr type, parametername;
220   struct kvp *parameters = 0;
221   char *parametervalue;
222
223   dynstr_init(&type);
224   if(!(s = skipwhite(s, 1)))
225     return -1;
226   if(!*s)
227     return -1;
228   while(*s && !mime_tspecial(*s) && !whitespace(*s))
229     dynstr_append(&type, tolower((unsigned char)*s++));
230   if(!(s = skipwhite(s, 1)))
231     return -1;
232   if(*s++ != '/')
233     return -1;
234   dynstr_append(&type, '/');
235   if(!(s = skipwhite(s, 1)))
236     return -1;
237   while(*s && !mime_tspecial(*s) && !whitespace(*s))
238     dynstr_append(&type, tolower((unsigned char)*s++));
239   if(!(s = skipwhite(s, 1)))
240     return -1;
241
242   while(*s == ';') {
243     dynstr_init(&parametername);
244     ++s;
245     if(!(s = skipwhite(s, 1)))
246       return -1;
247     if(!*s)
248       return -1;
249     while(*s && !mime_tspecial(*s) && !whitespace(*s))
250       dynstr_append(&parametername, tolower((unsigned char)*s++));
251     if(!(s = skipwhite(s, 1)))
252       return -1;
253     if(*s++ != '=')
254       return -1;
255     if(!(s = skipwhite(s, 1)))
256       return -1;
257     if(!(s = mime_parse_word(s, &parametervalue, mime_tspecial)))
258       return -1;
259     if(!(s = skipwhite(s, 1)))
260       return -1;
261     dynstr_terminate(&parametername);
262     kvp_set(&parameters, parametername.vec, parametervalue);
263   }
264   dynstr_terminate(&type);
265   *typep = type.vec;
266   *parametersp = parameters;
267   return 0;
268 }
269
270 /** @brief Parse a MIME message
271  * @param s Start of message
272  * @param callback Called for each header field
273  * @param u Passed to callback
274  * @return Pointer to decoded body (might be in original string), or NULL on error
275  *
276  * This does an RFC 822-style parse and honors Content-Transfer-Encoding as
277  * described in <a href="http://tools.ietf.org/html/rfc2045#section-6">RFC 2045
278  * s6</a>.  @p callback is called for each header field encountered, in order,
279  * with ASCII characters in the header name forced to lower case.
280  */
281 const char *mime_parse(const char *s,
282                        int (*callback)(const char *name, const char *value,
283                                        void *u),
284                        void *u) {
285   struct dynstr name, value;
286   char *cte = 0, *p;
287   
288   while(*s && !iscrlf(s)) {
289     dynstr_init(&name);
290     dynstr_init(&value);
291     while(*s && !mime_tspecial(*s) && !whitespace(*s))
292       dynstr_append(&name, tolower((unsigned char)*s++));
293     if(!(s = skipwhite(s, 1)))
294       return 0;
295     if(*s != ':')
296       return 0;
297     ++s;
298     while(*s && !(*s == '\n' && !(s[1] == ' ' || s[1] == '\t'))) {
299       const int c = *s++;
300       /* Strip leading whitespace */
301       if(value.nvec || !(c == ' ' || c == '\t' || c == '\n' || c == '\r'))
302         dynstr_append(&value, c);
303     }
304     /* Strip trailing whitespace */
305     while(value.nvec > 0 && (value.vec[value.nvec - 1] == ' '
306                              || value.vec[value.nvec - 1] == '\t'
307                              || value.vec[value.nvec - 1] == '\n'
308                              || value.vec[value.nvec - 1] == '\r'))
309       --value.nvec;
310     if(*s)
311       ++s;
312     dynstr_terminate(&name);
313     dynstr_terminate(&value);
314     if(!strcmp(name.vec, "content-transfer-encoding")) {
315       cte = xstrdup(value.vec);
316       for(p = cte; *p; p++)
317         *p = tolower((unsigned char)*p);
318     }
319     if(callback(name.vec, value.vec, u))
320       return 0;
321   }
322   if(*s)
323     s += 2;
324   if(cte) {
325     if(!strcmp(cte, "base64"))
326       return mime_base64(s, 0);
327     if(!strcmp(cte, "quoted-printable"))
328       return mime_qp(s);
329     if(!strcmp(cte, "7bit") || !strcmp(cte, "8bit"))
330       return s;
331     disorder_error(0, "unknown content-transfer-encoding '%s'", cte);
332     return 0;
333   }
334   return s;
335 }
336
337 /** @brief Match the boundary string */
338 static int isboundary(const char *ptr, const char *boundary, size_t bl) {
339   return (ptr[0] == '-'
340           && ptr[1] == '-'
341           && !strncmp(ptr + 2, boundary, bl)
342           && (iscrlf(ptr + bl + 2)
343               || (ptr[bl + 2] == '-'
344                   && ptr[bl + 3] == '-'
345                   && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0))));
346 }
347
348 /** @brief Match the final boundary string */
349 static int isfinal(const char *ptr, const char *boundary, size_t bl) {
350   return (ptr[0] == '-'
351           && ptr[1] == '-'
352           && !strncmp(ptr + 2, boundary, bl)
353           && ptr[bl + 2] == '-'
354           && ptr[bl + 3] == '-'
355           && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0));
356 }
357
358 /** @brief Parse a multipart MIME body
359  * @param s Start of message
360  * @param callback Callback for each part
361  * @param boundary Boundary string
362  * @param u Passed to callback
363  * @return 0 on success, non-0 on error
364  * 
365  * See <a href="http://tools.ietf.org/html/rfc2046#section-5.1">RFC 2046
366  * s5.1</a>.  @p callback is called for each part (not yet decoded in any way)
367  * in succession; you should probably call mime_parse() for each part.
368  */
369 int mime_multipart(const char *s,
370                    int (*callback)(const char *s, void *u),
371                    const char *boundary,
372                    void *u) {
373   size_t bl = strlen(boundary);
374   const char *start, *e;
375   int ret;
376
377   /* We must start with a boundary string */
378   if(!isboundary(s, boundary, bl)) {
379     disorder_error(0, "mime_multipart: first line is not the boundary string");
380     return -1;
381   }
382   /* Keep going until we hit a final boundary */
383   while(!isfinal(s, boundary, bl)) {
384     s = strstr(s, "\r\n") + 2;
385     start = s;
386     while(!isboundary(s, boundary, bl)) {
387       if(!(e = strstr(s, "\r\n"))) {
388         disorder_error(0, "mime_multipart: line does not end CRLF");
389         return -1;
390       }
391       s = e + 2;
392     }
393     if((ret = callback(xstrndup(start,
394                                 s == start ? 0 : s - start - 2),
395                        u)))
396       return ret;
397   }
398   return 0;
399 }
400
401 /** @brief Parse an RFC2388-style content-disposition field
402  * @param s Start of field
403  * @param dispositionp Where to store disposition
404  * @param parameternamep Where to store parameter name
405  * @param parametervaluep Wher to store parameter value
406  * @return 0 on success, non-0 on error
407  *
408  * See <a href="http://tools.ietf.org/html/rfc2388#section-3">RFC 2388 s3</a>
409  * and <a href="http://tools.ietf.org/html/rfc2183">RFC 2183</a>.
410  */
411 int mime_rfc2388_content_disposition(const char *s,
412                                      char **dispositionp,
413                                      char **parameternamep,
414                                      char **parametervaluep) {
415   struct dynstr disposition, parametername;
416
417   dynstr_init(&disposition);
418   if(!(s = skipwhite(s, 1)))
419     return -1;
420   if(!*s)
421     return -1;
422   while(*s && !mime_tspecial(*s) && !whitespace(*s))
423     dynstr_append(&disposition, tolower((unsigned char)*s++));
424   if(!(s = skipwhite(s, 1)))
425     return -1;
426
427   if(*s == ';') {
428     dynstr_init(&parametername);
429     ++s;
430     if(!(s = skipwhite(s, 1)))
431       return -1;
432     if(!*s)
433       return -1;
434     while(*s && !mime_tspecial(*s) && !whitespace(*s))
435       dynstr_append(&parametername, tolower((unsigned char)*s++));
436     if(!(s = skipwhite(s, 1)))
437       return -1;
438     if(*s++ != '=')
439       return -1;
440     if(!(s = skipwhite(s, 1)))
441       return -1;
442     if(!(s = mime_parse_word(s, parametervaluep, mime_tspecial)))
443       return -1;
444     if(!(s = skipwhite(s, 1)))
445       return -1;
446     dynstr_terminate(&parametername);
447     *parameternamep = parametername.vec;
448   } else
449     *parametervaluep = *parameternamep = 0;
450   dynstr_terminate(&disposition);
451   *dispositionp = disposition.vec;
452   return 0;
453 }
454
455 /** @brief Convert MIME quoted-printable
456  * @param s Quoted-printable data
457  * @return Decoded data
458  *
459  * See <a href="http://tools.ietf.org/html/rfc2045#section-6.7">RFC 2045
460  * s6.7</a>.
461  */
462 char *mime_qp(const char *s) {
463   struct dynstr d;
464   int c, a, b;
465   const char *t;
466
467   dynstr_init(&d);
468   while((c = *s++)) {
469     switch(c) {
470     case '=':
471       if((a = unhexdigitq(s[0])) != -1
472          && (b = unhexdigitq(s[1])) != -1) {
473         dynstr_append(&d, a * 16 + b);
474         s += 2;
475       } else {
476         t = s;
477         while(*t == ' ' || *t == '\t') ++t;
478         if(iscrlf(t)) {
479           /* soft line break */
480           s = t + 2;
481         } else
482           return 0;
483       }
484       break;
485     case ' ':
486     case '\t':
487       t = s;
488       while(*t == ' ' || *t == '\t') ++t;
489       if(iscrlf(t))
490         /* trailing space is always eliminated */
491         s = t;
492       else
493         dynstr_append(&d, c);
494       break;
495     default:
496       dynstr_append(&d, c);
497       break;
498     }
499   }
500   dynstr_terminate(&d);
501   return d.vec;
502 }
503
504 /** @brief Match cookie separator characters
505  *
506  * This is a subset of the RFC2616 specials, and technically is in breach of
507  * the specification.  However rejecting (in particular) slashes is
508  * unreasonably strict and has broken at least one (admittedly somewhat
509  * obscure) browser, so we're more forgiving.
510  */
511 static int cookie_separator(int c) {
512   switch(c) {
513   case '(':
514   case ')':
515   case ',':
516   case ';':
517   case '=':
518   case ' ':
519   case '"':
520   case '\t':
521     return 1;
522
523   default:
524     return 0;
525   }
526 }
527
528 /** @brief Match cookie value separator characters
529  *
530  * Same as cookie_separator() but allows for @c = in cookie values.
531  */
532 static int cookie_value_separator(int c) {
533   switch(c) {
534   case '(':
535   case ')':
536   case ',':
537   case ';':
538   case ' ':
539   case '"':
540   case '\t':
541     return 1;
542
543   default:
544     return 0;
545   }
546 }
547
548 /** @brief Parse a RFC2109 Cookie: header
549  * @param s Header field value
550  * @param cd Where to store result
551  * @return 0 on success, non-0 on error
552  *
553  * See <a href="http://tools.ietf.org/html/rfc2109">RFC 2109</a>.
554  */
555 int parse_cookie(const char *s,
556                  struct cookiedata *cd) {
557   char *n = 0, *v = 0;
558
559   memset(cd, 0, sizeof *cd);
560   s = skipwhite(s, 0);
561   while(*s) {
562     /* Skip separators */
563     if(*s == ';' || *s == ',') {
564       ++s;
565       s = skipwhite(s, 0);
566       continue;
567     }
568     if(!(s = parsetoken(s, &n, cookie_separator))) {
569       disorder_error(0, "parse_cookie: cannot parse attribute name");
570       return -1;
571     }
572     s = skipwhite(s, 0);
573     if(*s++ != '=') {
574       disorder_error(0, "parse_cookie: did not find expected '='");
575       return -1;
576     }
577     s = skipwhite(s, 0);
578     if(!(s = mime_parse_word(s, &v, cookie_value_separator))) {
579       disorder_error(0, "parse_cookie: cannot parse value for '%s'", n);
580       return -1;
581     }
582     if(n[0] == '$') {
583       /* Some bit of meta-information */
584       if(!strcmp(n, "$Version"))
585         cd->version = v;
586       else if(!strcmp(n, "$Path")) {
587         if(cd->ncookies > 0 && cd->cookies[cd->ncookies-1].path == 0)
588           cd->cookies[cd->ncookies-1].path = v;
589         else {
590           disorder_error(0, "redundant $Path in Cookie: header");
591           return -1;
592         }
593       } else if(!strcmp(n, "$Domain")) {
594         if(cd->ncookies > 0 && cd->cookies[cd->ncookies-1].domain == 0)
595           cd->cookies[cd->ncookies-1].domain = v;
596         else {
597           disorder_error(0, "redundant $Domain in Cookie: header");
598           return -1;
599         }
600       }
601     } else {
602       /* It's a new cookie */
603       cd->cookies = xrealloc(cd->cookies,
604                              (cd->ncookies + 1) * sizeof (struct cookie));
605       cd->cookies[cd->ncookies].name = n;
606       cd->cookies[cd->ncookies].value = v;
607       cd->cookies[cd->ncookies].path = 0;
608       cd->cookies[cd->ncookies].domain = 0;
609       ++cd->ncookies;
610     }
611     s = skipwhite(s, 0);
612     if(*s && (*s != ',' && *s != ';')) {
613       disorder_error(0, "missing separator in Cookie: header");
614       return -1;
615     }
616   }
617   return 0;
618 }
619
620 /** @brief Find a named cookie
621  * @param cd Parse cookie data
622  * @param name Name of cookie
623  * @return Cookie structure or NULL if not found
624  */
625 const struct cookie *find_cookie(const struct cookiedata *cd,
626                                  const char *name) {
627   int n;
628
629   for(n = 0; n < cd->ncookies; ++n)
630     if(!strcmp(cd->cookies[n].name, name))
631       return &cd->cookies[n];
632   return 0;
633 }
634
635 /** @brief RFC822 quoting
636  * @param s String to quote
637  * @param force If non-0, always quote
638  * @return Possibly quoted string
639  */
640 char *quote822(const char *s, int force) {
641   const char *t;
642   struct dynstr d[1];
643   int c;
644
645   if(!force) {
646     /* See if we need to quote */
647     for(t = s; (c = (unsigned char)*t); ++t) {
648       if(mime_tspecial(c) || mime_http_separator(c) || whitespace(c))
649         break;
650     }
651     if(*t)
652       force = 1;
653   }
654
655   if(!force)
656     return xstrdup(s);
657
658   dynstr_init(d);
659   dynstr_append(d, '"');
660   for(t = s; (c = (unsigned char)*t); ++t) {
661     if(c == '"' || c == '\\')
662       dynstr_append(d, '\\');
663     dynstr_append(d, c);
664   }
665   dynstr_append(d, '"');
666   dynstr_terminate(d);
667   return d->vec;
668 }
669
670 /** @brief Return true if @p ptr points at trailing space */
671 static int is_trailing_space(const char *ptr) {
672   if(*ptr == ' ' || *ptr == '\t') {
673     while(*ptr == ' ' || *ptr == '\t')
674       ++ptr;
675     return *ptr == '\n' || *ptr == 0;
676   } else
677     return 0;
678 }
679
680 /** @brief Encoding text as quoted-printable
681  * @param text String to encode
682  * @return Encoded string
683  *
684  * See <a href="http://tools.ietf.org/html/rfc2045#section-6.7">RFC2045
685  * s6.7</a>.
686  */
687 char *mime_to_qp(const char *text) {
688   struct dynstr d[1];
689   int linelength = 0;                   /* length of current line */
690   char buffer[10];
691
692   dynstr_init(d);
693   /* The rules are:
694    * 1. Anything except newline can be replaced with =%02X
695    * 2. Newline, 33-60 and 62-126 stand for themselves (i.e. not '=')
696    * 3. Non-trailing space/tab stand for themselves.
697    * 4. Output lines are limited to 76 chars, with =<newline> being used
698    *    as a soft line break
699    * 5. Newlines aren't counted towards the 76 char limit.
700    */
701   while(*text) {
702     const int c = (unsigned char)*text;
703     if(c == '\n') {
704       /* Newline stands as itself */
705       dynstr_append(d, '\n');
706       linelength = 0;
707     } else if((c >= 33 && c <= 126 && c != '=')
708               || ((c == ' ' || c == '\t')
709                   && !is_trailing_space(text))) {
710       /* Things that can stand for themselves  */
711       dynstr_append(d, c);
712       ++linelength;
713     } else {
714       /* Anything else that needs encoding */
715       snprintf(buffer, sizeof buffer, "=%02X", c);
716       dynstr_append_string(d, buffer);
717       linelength += 3;
718     }
719     ++text;
720     if(linelength > 73 && *text && *text != '\n') {
721       /* Next character might overflow 76 character limit if encoded, so we
722        * insert a soft break */
723       dynstr_append_string(d, "=\n");
724       linelength = 0;
725     }
726   }
727   /* Ensure there is a final newline */
728   if(linelength)
729     dynstr_append(d, '\n');
730   /* That's all */
731   dynstr_terminate(d);
732   return d->vec;
733 }
734
735 /** @brief Encode text
736  * @param text Underlying UTF-8 text
737  * @param charsetp Where to store charset string
738  * @param encodingp Where to store encoding string
739  * @return Encoded text (might be @p text)
740  */
741 const char *mime_encode_text(const char *text,
742                              const char **charsetp,
743                              const char **encodingp) {
744   const char *ptr;
745
746   /* See if there are in fact any non-ASCII characters */
747   for(ptr = text; *ptr; ++ptr)
748     if((unsigned char)*ptr >= 128)
749       break;
750   if(!*ptr) {
751     /* Plain old ASCII, no encoding required */
752     *charsetp = "us-ascii";
753     *encodingp = "7bit";
754     return text;
755   }
756   *charsetp = "utf-8";
757   *encodingp = "quoted-printable";
758   return mime_to_qp(text);
759 }
760
761 /*
762 Local Variables:
763 c-basic-offset:2
764 comment-column:40
765 fill-column:79
766 End:
767 */