chiark / gitweb /
extra make clean in tests/
[disorder] / lib / mime.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2005, 2007 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 2 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, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * 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, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20 /** @file lib/mime.c
21  * @brief Support for MIME and allied protocols
22  */
23
24 #include <config.h>
25 #include "types.h"
26
27 #include <string.h>
28 #include <ctype.h>
29
30 #include <stdio.h>
31
32 #include "mem.h"
33 #include "mime.h"
34 #include "vector.h"
35 #include "hex.h"
36 #include "log.h"
37 #include "base64.h"
38
39 /** @brief Match whitespace characters */
40 static int whitespace(int c) {
41   switch(c) {
42   case ' ':
43   case '\t':
44   case '\r':
45   case '\n':
46     return 1;
47   default:
48     return 0;
49   }
50 }
51
52 /** @brief Match RFC2045 tspecial characters */
53 static int tspecial(int c) {
54   switch(c) {
55   case '(':
56   case ')':
57   case '<':
58   case '>':
59   case '@':
60   case ',':
61   case ';':
62   case ':':
63   case '\\':
64   case '"':
65   case '/':
66   case '[':
67   case ']':
68   case '?':
69   case '=':
70     return 1;
71   default:
72     return 0;
73   }
74 }
75
76 /** @brief Match RFC2616 seprator characters */
77 static int http_separator(int c) {
78   switch(c) {
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 '=':
94   case '{':
95   case '}':
96   case ' ':
97   case '\t':
98     return 1;
99   default:
100     return 0;
101   }
102 }
103
104 /** @brief Match CRLF */
105 static int iscrlf(const char *ptr) {
106   return ptr[0] == '\r' && ptr[1] == '\n';
107 }
108
109 /** @brief Skip whitespace
110  * @param s Pointer into string
111  * @param rfc822_comments If true, skip RFC822 nested comments
112  * @return Pointer into string after whitespace
113  */
114 static const char *skipwhite(const char *s, int rfc822_comments) {
115   int c, depth;
116   
117   for(;;) {
118     switch(c = *s) {
119     case ' ':
120     case '\t':
121     case '\r':
122     case '\n':
123       ++s;
124       break;
125     case '(':
126       if(!rfc822_comments)
127         return s;
128       ++s;
129       depth = 1;
130       while(*s && depth) {
131         c = *s++;
132         switch(c) {
133         case '(': ++depth; break;
134         case ')': --depth; break;
135         case '\\':
136           if(!*s) return 0;
137           ++s;
138           break;
139         }
140       }
141       if(depth) return 0;
142       break;
143     default:
144       return s;
145     }
146   }
147 }
148
149 /** @brief Test for a word character
150  * @param c Character to test
151  * @param special tspecial() (MIME/RFC2405) or http_separator() (HTTP/RFC2616)
152  * @return 1 if @p c is a word character, else 0
153  */
154 static int iswordchar(int c, int (*special)(int)) {
155   return !(c <= ' ' || c > '~' || special(c));
156 }
157
158 /** @brief Parse an RFC1521/RFC2616 word
159  * @param s Pointer to start of word
160  * @param valuep Where to store value
161  * @param special tspecial() (MIME/RFC2405) or http_separator() (HTTP/RFC2616)
162  * @return Pointer just after end of word or NULL if there's no word
163  *
164  * A word is a token or a quoted-string.
165  */
166 static const char *parseword(const char *s, char **valuep,
167                              int (*special)(int)) {
168   struct dynstr value[1];
169   int c;
170
171   dynstr_init(value);
172   if(*s == '"') {
173     ++s;
174     while((c = *s++) != '"') {
175       switch(c) {
176       case '\\':
177         if(!(c = *s++)) return 0;
178       default:
179         dynstr_append(value, c);
180         break;
181       }
182     }
183     if(!c) 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 tspecial() (MIME/RFC2405) or 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 == '"') return 0;
205   return parseword(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 parameternamep Where to store parameter name
212  * @param parametervaluep Wher to store parameter value
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                       char **parameternamep,
220                       char **parametervaluep) {
221   struct dynstr type, parametername;
222
223   dynstr_init(&type);
224   if(!(s = skipwhite(s, 1))) return -1;
225   if(!*s) return -1;
226   while(*s && !tspecial(*s) && !whitespace(*s))
227     dynstr_append(&type, tolower((unsigned char)*s++));
228   if(!(s = skipwhite(s, 1))) return -1;
229   if(*s++ != '/') return -1;
230   dynstr_append(&type, '/');
231   if(!(s = skipwhite(s, 1))) return -1;
232   while(*s && !tspecial(*s) && !whitespace(*s))
233     dynstr_append(&type, tolower((unsigned char)*s++));
234   if(!(s = skipwhite(s, 1))) return -1;
235
236   if(*s == ';') {
237     dynstr_init(&parametername);
238     ++s;
239     if(!(s = skipwhite(s, 1))) return -1;
240     if(!*s) return -1;
241     while(*s && !tspecial(*s) && !whitespace(*s))
242       dynstr_append(&parametername, tolower((unsigned char)*s++));
243     if(!(s = skipwhite(s, 1))) return -1;
244     if(*s++ != '=') return -1;
245     if(!(s = skipwhite(s, 1))) return -1;
246     if(!(s = parseword(s, parametervaluep, tspecial))) return -1;
247     if(!(s = skipwhite(s, 1))) return -1;
248     dynstr_terminate(&parametername);
249     *parameternamep = parametername.vec;
250   } else
251     *parametervaluep = *parameternamep = 0;
252   dynstr_terminate(&type);
253   *typep = type.vec;
254   return 0;
255 }
256
257 /** @brief Parse a MIME message
258  * @param s Start of message
259  * @param callback Called for each header field
260  * @param u Passed to callback
261  * @return Pointer to decoded body (might be in original string), or NULL on error
262  *
263  * This does an RFC 822-style parse and honors Content-Transfer-Encoding as
264  * described in <a href="http://tools.ietf.org/html/rfc2045#section-6">RFC 2045
265  * s6</a>.  @p callback is called for each header field encountered, in order,
266  * with ASCII characters in the header name forced to lower case.
267  */
268 const char *mime_parse(const char *s,
269                        int (*callback)(const char *name, const char *value,
270                                        void *u),
271                        void *u) {
272   struct dynstr name, value;
273   char *cte = 0, *p;
274   
275   while(*s && !iscrlf(s)) {
276     dynstr_init(&name);
277     dynstr_init(&value);
278     while(*s && !tspecial(*s) && !whitespace(*s))
279       dynstr_append(&name, tolower((unsigned char)*s++));
280     if(!(s = skipwhite(s, 1))) return 0;
281     if(*s != ':') return 0;
282     ++s;
283     while(*s && !(*s == '\n' && !(s[1] == ' ' || s[1] == '\t')))
284       dynstr_append(&value, *s++);
285     if(*s) ++s;
286     dynstr_terminate(&name);
287     dynstr_terminate(&value);
288     if(!strcmp(name.vec, "content-transfer-encoding")) {
289       cte = xstrdup(value.vec);
290       for(p = cte; *p; p++)
291         *p = tolower((unsigned char)*p);
292     }
293     if(callback(name.vec, value.vec, u)) return 0;
294   }
295   if(*s) s += 2;
296   if(cte) {
297     if(!strcmp(cte, "base64")) return mime_base64(s, 0);
298     if(!strcmp(cte, "quoted-printable")) return mime_qp(s);
299   }
300   return s;
301 }
302
303 /** @brief Match the boundary string */
304 static int isboundary(const char *ptr, const char *boundary, size_t bl) {
305   return (ptr[0] == '-'
306           && ptr[1] == '-'
307           && !strncmp(ptr + 2, boundary, bl)
308           && (iscrlf(ptr + bl + 2)
309               || (ptr[bl + 2] == '-'
310                   && ptr[bl + 3] == '-'
311                   && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0))));
312 }
313
314 /** @brief Match the final boundary string */
315 static int isfinal(const char *ptr, const char *boundary, size_t bl) {
316   return (ptr[0] == '-'
317           && ptr[1] == '-'
318           && !strncmp(ptr + 2, boundary, bl)
319           && ptr[bl + 2] == '-'
320           && ptr[bl + 3] == '-'
321           && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0));
322 }
323
324 /** @brief Parse a multipart MIME body
325  * @param s Start of message
326  * @param callback Callback for each part
327  * @param boundary Boundary string
328  * @param u Passed to callback
329  * @return 0 on success, non-0 on error
330  * 
331  * See <a href="http://tools.ietf.org/html/rfc2046#section-5.1">RFC 2046
332  * s5.1</a>.  @p callback is called for each part (not yet decoded in any way)
333  * in succession; you should probably call mime_parse() for each part.
334  */
335 int mime_multipart(const char *s,
336                    int (*callback)(const char *s, void *u),
337                    const char *boundary,
338                    void *u) {
339   size_t bl = strlen(boundary);
340   const char *start, *e;
341   int ret;
342
343   /* We must start with a boundary string */
344   if(!isboundary(s, boundary, bl))
345     return -1;
346   /* Keep going until we hit a final boundary */
347   while(!isfinal(s, boundary, bl)) {
348     s = strstr(s, "\r\n") + 2;
349     start = s;
350     while(!isboundary(s, boundary, bl)) {
351       if(!(e = strstr(s, "\r\n")))
352         return -1;
353       s = e + 2;
354     }
355     if((ret = callback(xstrndup(start,
356                                 s == start ? 0 : s - start - 2),
357                        u)))
358       return ret;
359   }
360   return 0;
361 }
362
363 /** @brief Parse an RFC2388-style content-disposition field
364  * @param s Start of field
365  * @param dispositionp Where to store disposition
366  * @param parameternamep Where to store parameter name
367  * @param parametervaluep Wher to store parameter value
368  * @return 0 on success, non-0 on error
369  *
370  * See <a href="http://tools.ietf.org/html/rfc2388#section-3">RFC 2388 s3</a>
371  * and <a href="http://tools.ietf.org/html/rfc2183">RFC 2183</a>.
372  */
373 int mime_rfc2388_content_disposition(const char *s,
374                                      char **dispositionp,
375                                      char **parameternamep,
376                                      char **parametervaluep) {
377   struct dynstr disposition, parametername;
378
379   dynstr_init(&disposition);
380   if(!(s = skipwhite(s, 1))) return -1;
381   if(!*s) return -1;
382   while(*s && !tspecial(*s) && !whitespace(*s))
383     dynstr_append(&disposition, tolower((unsigned char)*s++));
384   if(!(s = skipwhite(s, 1))) return -1;
385
386   if(*s == ';') {
387     dynstr_init(&parametername);
388     ++s;
389     if(!(s = skipwhite(s, 1))) return -1;
390     if(!*s) return -1;
391     while(*s && !tspecial(*s) && !whitespace(*s))
392       dynstr_append(&parametername, tolower((unsigned char)*s++));
393     if(!(s = skipwhite(s, 1))) return -1;
394     if(*s++ != '=') return -1;
395     if(!(s = skipwhite(s, 1))) return -1;
396     if(!(s = parseword(s, parametervaluep, tspecial))) return -1;
397     if(!(s = skipwhite(s, 1))) return -1;
398     dynstr_terminate(&parametername);
399     *parameternamep = parametername.vec;
400   } else
401     *parametervaluep = *parameternamep = 0;
402   dynstr_terminate(&disposition);
403   *dispositionp = disposition.vec;
404   return 0;
405 }
406
407 /** @brief Convert MIME quoted-printable
408  * @param s Quoted-printable data
409  * @return Decoded data
410  *
411  * See <a href="http://tools.ietf.org/html/rfc2045#section-6.7">RFC 2045
412  * s6.7</a>.
413  */
414 char *mime_qp(const char *s) {
415   struct dynstr d;
416   int c, a, b;
417   const char *t;
418
419   dynstr_init(&d);
420   while((c = *s++)) {
421     switch(c) {
422     case '=':
423       if((a = unhexdigitq(s[0])) != -1
424          && (b = unhexdigitq(s[1])) != -1) {
425         dynstr_append(&d, a * 16 + b);
426         s += 2;
427       } else {
428         t = s;
429         while(*t == ' ' || *t == '\t') ++t;
430         if(iscrlf(t)) {
431           /* soft line break */
432           s = t + 2;
433         } else
434           return 0;
435       }
436       break;
437     case ' ':
438     case '\t':
439       t = s;
440       while(*t == ' ' || *t == '\t') ++t;
441       if(iscrlf(t))
442         /* trailing space is always eliminated */
443         s = t;
444       else
445         dynstr_append(&d, c);
446       break;
447     default:
448       dynstr_append(&d, c);
449       break;
450     }
451   }
452   dynstr_terminate(&d);
453   return d.vec;
454 }
455
456 /** @brief Parse a RFC2109 Cookie: header
457  * @param s Header field value
458  * @param cd Where to store result
459  * @return 0 on success, non-0 on error
460  */
461 int parse_cookie(const char *s,
462                  struct cookiedata *cd) {
463   char *n = 0, *v = 0;
464
465   memset(cd, 0, sizeof *cd);
466   s = skipwhite(s, 0);
467   while(*s) {
468     /* Skip separators */
469     if(*s == ';' || *s == ',') {
470       ++s;
471       s = skipwhite(s, 0);
472       continue;
473     }
474     if(!(s = parsetoken(s, &n, http_separator))) return -1;
475     s = skipwhite(s, 0);
476     if(*s++ != '=') return -1;
477     s = skipwhite(s, 0);
478     if(!(s = parseword(s, &v, http_separator))) return -1;
479     if(n[0] == '$') {
480       /* Some bit of meta-information */
481       if(!strcmp(n, "$Version"))
482         cd->version = v;
483       else if(!strcmp(n, "$Path")) {
484         if(cd->ncookies > 0 && cd->cookies[cd->ncookies-1].path == 0)
485           cd->cookies[cd->ncookies-1].path = v;
486         else {
487           error(0, "redundant $Path in Cookie: header");
488           return -1;
489         }
490       } else if(!strcmp(n, "$Domain")) {
491         if(cd->ncookies > 0 && cd->cookies[cd->ncookies-1].domain == 0)
492           cd->cookies[cd->ncookies-1].domain = v;
493         else {
494           error(0, "redundant $Domain in Cookie: header");
495           return -1;
496         }
497       }
498     } else {
499       /* It's a new cookie */
500       cd->cookies = xrealloc(cd->cookies,
501                              (cd->ncookies + 1) * sizeof (struct cookie));
502       cd->cookies[cd->ncookies].name = n;
503       cd->cookies[cd->ncookies].value = v;
504       cd->cookies[cd->ncookies].path = 0;
505       cd->cookies[cd->ncookies].domain = 0;
506       ++cd->ncookies;
507     }
508     s = skipwhite(s, 0);
509     if(*s && (*s != ',' && *s != ';')) {
510       error(0, "missing separator in Cookie: header");
511       return -1;
512     }
513   }
514   return 0;
515 }
516
517 /** @brief Find a named cookie
518  * @param cd Parse cookie data
519  * @param name Name of cookie
520  * @return Cookie structure or NULL if not found
521  */
522 const struct cookie *find_cookie(const struct cookiedata *cd,
523                                  const char *name) {
524   int n;
525
526   for(n = 0; n < cd->ncookies; ++n)
527     if(!strcmp(cd->cookies[n].name, name))
528       return &cd->cookies[n];
529   return 0;
530 }
531
532 /*
533 Local Variables:
534 c-basic-offset:2
535 comment-column:40
536 fill-column:79
537 End:
538 */