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