Advertise IPv6 support on the show-status page.
[privoxy.git] / urlmatch.c
1 const char urlmatch_rcs[] = "$Id: urlmatch.c,v 1.49 2009/04/17 11:34:35 fabiankeil Exp $";
2 /*********************************************************************
3  *
4  * File        :  $Source: /cvsroot/ijbswa/current/urlmatch.c,v $
5  *
6  * Purpose     :  Declares functions to match URLs against URL
7  *                patterns.
8  *
9  * Copyright   :  Written by and Copyright (C) 2001-2009
10  *                the Privoxy team. http://www.privoxy.org/
11  *
12  *                Based on the Internet Junkbuster originally written
13  *                by and Copyright (C) 1997 Anonymous Coders and
14  *                Junkbusters Corporation.  http://www.junkbusters.com
15  *
16  *                This program is free software; you can redistribute it
17  *                and/or modify it under the terms of the GNU General
18  *                Public License as published by the Free Software
19  *                Foundation; either version 2 of the License, or (at
20  *                your option) any later version.
21  *
22  *                This program is distributed in the hope that it will
23  *                be useful, but WITHOUT ANY WARRANTY; without even the
24  *                implied warranty of MERCHANTABILITY or FITNESS FOR A
25  *                PARTICULAR PURPOSE.  See the GNU General Public
26  *                License for more details.
27  *
28  *                The GNU General Public License should be included with
29  *                this file.  If not, you can view it at
30  *                http://www.gnu.org/copyleft/gpl.html
31  *                or write to the Free Software Foundation, Inc., 59
32  *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
33  *
34  * Revisions   :
35  *    $Log: urlmatch.c,v $
36  *    Revision 1.49  2009/04/17 11:34:35  fabiankeil
37  *    Style cosmetics for the IPv6 code.
38  *
39  *    Revision 1.48  2009/04/17 11:27:49  fabiankeil
40  *    Petr Pisar's privoxy-3.0.12-ipv6-3.diff.
41  *
42  *    Revision 1.47  2009/03/02 19:18:10  fabiankeil
43  *    Streamline parse_http_request()'s prototype. As
44  *    cparser pointed out it doesn't actually use csp.
45  *
46  *    Revision 1.46  2009/02/11 19:31:32  fabiankeil
47  *    Reject request lines that end with neither HTTP/1.0 nor HTTP/1.1.
48  *
49  *    Revision 1.45  2008/06/21 21:19:18  fabiankeil
50  *    Silence bogus compiler warning.
51  *
52  *    Revision 1.44  2008/05/04 16:18:32  fabiankeil
53  *    Provide parse_http_url() with a third parameter to specify
54  *    whether or not URLs without protocol are acceptable.
55  *
56  *    Revision 1.43  2008/05/04 13:30:55  fabiankeil
57  *    Streamline parse_http_url()'s prototype.
58  *
59  *    Revision 1.42  2008/05/04 13:24:16  fabiankeil
60  *    If the method isn't CONNECT, reject URLs without protocol.
61  *
62  *    Revision 1.41  2008/05/02 09:51:34  fabiankeil
63  *    In parse_http_url(), don't muck around with values
64  *    that are none of its business: require an initialized
65  *    http structure and never unset http->ssl.
66  *
67  *    Revision 1.40  2008/04/23 16:12:28  fabiankeil
68  *    Free with freez().
69  *
70  *    Revision 1.39  2008/04/22 16:27:42  fabiankeil
71  *    In parse_http_request(), remove a pointless
72  *    temporary variable and free the buffer earlier.
73  *
74  *    Revision 1.38  2008/04/18 05:17:18  fabiankeil
75  *    Mark simplematch()'s parameters as immutable.
76  *
77  *    Revision 1.37  2008/04/17 14:53:29  fabiankeil
78  *    Move simplematch() into urlmatch.c as it's only
79  *    used to match (old-school) domain patterns.
80  *
81  *    Revision 1.36  2008/04/14 18:19:48  fabiankeil
82  *    Remove now-pointless cast in create_url_spec().
83  *
84  *    Revision 1.35  2008/04/14 18:11:21  fabiankeil
85  *    The compiler might not notice it, but the buffer passed to
86  *    create_url_spec() is modified later on and thus shouldn't
87  *    be declared immutable.
88  *
89  *    Revision 1.34  2008/04/13 13:32:07  fabiankeil
90  *    Factor URL pattern compilation out of create_url_spec().
91  *
92  *    Revision 1.33  2008/04/12 14:03:13  fabiankeil
93  *    Remove an obvious comment and improve another one.
94  *
95  *    Revision 1.32  2008/04/12 12:38:06  fabiankeil
96  *    Factor out duplicated code to compile host, path and tag patterns.
97  *
98  *    Revision 1.31  2008/04/10 14:41:04  fabiankeil
99  *    Ditch url_spec's path member now that it's no longer used.
100  *
101  *    Revision 1.30  2008/04/10 04:24:24  fabiankeil
102  *    Stop duplicating the plain text representation of the path regex
103  *    (and keeping the copy around). Once the regex is compiled it's no
104  *    longer useful.
105  *
106  *    Revision 1.29  2008/04/10 04:17:56  fabiankeil
107  *    In url_match(), check the right member for NULL when determining
108  *    whether there's a path regex to execute. Looking for a plain-text
109  *    representation works as well, but it looks "interesting" and that
110  *    member will be removed soonish anyway.
111  *
112  *    Revision 1.28  2008/04/08 16:07:39  fabiankeil
113  *    Make it harder to mistake url_match()'s
114  *    second parameter for an url_spec.
115  *
116  *    Revision 1.27  2008/04/08 15:44:33  fabiankeil
117  *    Save a bit of memory (and a few cpu cycles) by not bothering to
118  *    compile slash-only path regexes that don't affect the result.
119  *
120  *    Revision 1.26  2008/04/07 16:57:18  fabiankeil
121  *    - Use free_url_spec() more consistently.
122  *    - Let it reset url->dcount just in case.
123  *
124  *    Revision 1.25  2008/04/06 15:18:38  fabiankeil
125  *    Oh well, rename the --enable-pcre-host-patterns option to
126  *    --enable-extended-host-patterns as it's not really PCRE syntax.
127  *
128  *    Revision 1.24  2008/04/06 14:54:26  fabiankeil
129  *    Use PCRE syntax in host patterns when configured
130  *    with --enable-pcre-host-patterns.
131  *
132  *    Revision 1.23  2008/04/05 12:19:20  fabiankeil
133  *    Factor compile_host_pattern() out of create_url_spec().
134  *
135  *    Revision 1.22  2008/03/30 15:02:32  fabiankeil
136  *    SZitify unknown_method().
137  *
138  *    Revision 1.21  2007/12/24 16:34:23  fabiankeil
139  *    Band-aid (and micro-optimization) that makes it less likely to run out of
140  *    stack space with overly-complex path patterns. Probably masks the problem
141  *    reported by Lee in #1856679. Hohoho.
142  *
143  *    Revision 1.20  2007/09/02 15:31:20  fabiankeil
144  *    Move match_portlist() from filter.c to urlmatch.c.
145  *    It's used for url matching, not for filtering.
146  *
147  *    Revision 1.19  2007/09/02 13:42:11  fabiankeil
148  *    - Allow port lists in url patterns.
149  *    - Ditch unused url_spec member pathlen.
150  *
151  *    Revision 1.18  2007/07/30 16:42:21  fabiankeil
152  *    Move the method check into unknown_method()
153  *    and loop through the known methods instead
154  *    of using a screen-long OR chain.
155  *
156  *    Revision 1.17  2007/04/15 16:39:21  fabiankeil
157  *    Introduce tags as alternative way to specify which
158  *    actions apply to a request. At the moment tags can be
159  *    created based on client and server headers.
160  *
161  *    Revision 1.16  2007/02/13 13:59:24  fabiankeil
162  *    Remove redundant log message.
163  *
164  *    Revision 1.15  2007/01/28 16:11:23  fabiankeil
165  *    Accept WebDAV methods for subversion
166  *    in parse_http_request(). Closes FR 1581425.
167  *
168  *    Revision 1.14  2007/01/06 14:23:56  fabiankeil
169  *    Fix gcc43 warnings. Mark *csp as immutable
170  *    for parse_http_url() and url_match().
171  *    Replace a sprintf call with snprintf.
172  *
173  *    Revision 1.13  2006/12/06 19:50:54  fabiankeil
174  *    parse_http_url() now handles intercepted
175  *    HTTP request lines as well. Moved parts
176  *    of parse_http_url()'s code into
177  *    init_domain_components() so that it can
178  *    be reused in chat().
179  *
180  *    Revision 1.12  2006/07/18 14:48:47  david__schmidt
181  *    Reorganizing the repository: swapping out what was HEAD (the old 3.1 branch)
182  *    with what was really the latest development (the v_3_0_branch branch)
183  *
184  *    Revision 1.10.2.7  2003/05/17 15:57:24  oes
185  *     - parse_http_url now checks memory allocation failure for
186  *       duplication of "*" URL and rejects "*something" URLs
187  *       Closes bug #736344
188  *     - Added a comment to what might look like a bug in
189  *       create_url_spec (see !bug #736931)
190  *     - Comment cosmetics
191  *
192  *    Revision 1.10.2.6  2003/05/07 12:39:48  oes
193  *    Fix typo: Default port for https URLs is 443, not 143.
194  *    Thanks to Scott Tregear for spotting this one.
195  *
196  *    Revision 1.10.2.5  2003/02/28 13:09:29  oes
197  *    Fixed a rare double free condition as per Bug #694713
198  *
199  *    Revision 1.10.2.4  2003/02/28 12:57:44  oes
200  *    Moved freeing of http request structure to its owner
201  *    as per Dan Price's observations in Bug #694713
202  *
203  *    Revision 1.10.2.3  2002/11/12 16:50:40  oes
204  *    Fixed memory leak in parse_http_request() reported by Oliver Stoeneberg. Fixes bug #637073
205  *
206  *    Revision 1.10.2.2  2002/09/25 14:53:15  oes
207  *    Added basic support for OPTIONS and TRACE HTTP methods:
208  *    parse_http_url now recognizes the "*" URI as well as
209  *    the OPTIONS and TRACE method keywords.
210  *
211  *    Revision 1.10.2.1  2002/06/06 19:06:44  jongfoster
212  *    Adding support for proprietary Microsoft WebDAV extensions
213  *
214  *    Revision 1.10  2002/05/12 21:40:37  jongfoster
215  *    - Removing some unused code
216  *
217  *    Revision 1.9  2002/04/04 00:36:36  gliptak
218  *    always use pcre for matching
219  *
220  *    Revision 1.8  2002/04/03 23:32:47  jongfoster
221  *    Fixing memory leak on error
222  *
223  *    Revision 1.7  2002/03/26 22:29:55  swa
224  *    we have a new homepage!
225  *
226  *    Revision 1.6  2002/03/24 13:25:43  swa
227  *    name change related issues
228  *
229  *    Revision 1.5  2002/03/13 00:27:05  jongfoster
230  *    Killing warnings
231  *
232  *    Revision 1.4  2002/03/07 03:46:17  oes
233  *    Fixed compiler warnings
234  *
235  *    Revision 1.3  2002/03/03 14:51:11  oes
236  *    Fixed CLF logging: Added ocmd member for client's request to struct http_request
237  *
238  *    Revision 1.2  2002/01/21 00:14:09  jongfoster
239  *    Correcting comment style
240  *    Fixing an uninitialized memory bug in create_url_spec()
241  *
242  *    Revision 1.1  2002/01/17 20:53:46  jongfoster
243  *    Moving all our URL and URL pattern parsing code to the same file - it
244  *    was scattered around in filters.c, loaders.c and parsers.c.
245  *
246  *    Providing a single, simple url_match(pattern,url) function - rather than
247  *    the 3-line match routine which was repeated all over the place.
248  *
249  *    Renaming free_url to free_url_spec, since it frees a struct url_spec.
250  *
251  *    Providing parse_http_url() so that URLs can be parsed without faking a
252  *    HTTP request line for parse_http_request() or repeating the parsing
253  *    code (both of which were techniques that were actually in use).
254  *
255  *    Standardizing that struct http_request is used to represent a URL, and
256  *    struct url_spec is used to represent a URL pattern.  (Before, URLs were
257  *    represented as seperate variables and a partially-filled-in url_spec).
258  *
259  *
260  *********************************************************************/
261 \f
262
263 #include "config.h"
264
265 #ifndef _WIN32
266 #include <stdio.h>
267 #include <sys/types.h>
268 #endif
269
270 #include <stdlib.h>
271 #include <ctype.h>
272 #include <assert.h>
273 #include <string.h>
274
275 #if !defined(_WIN32) && !defined(__OS2__)
276 #include <unistd.h>
277 #endif
278
279 #include "project.h"
280 #include "urlmatch.h"
281 #include "ssplit.h"
282 #include "miscutil.h"
283 #include "errlog.h"
284
285 const char urlmatch_h_rcs[] = URLMATCH_H_VERSION;
286
287 enum regex_anchoring {NO_ANCHORING, LEFT_ANCHORED, RIGHT_ANCHORED};
288 static jb_err compile_host_pattern(struct url_spec *url, const char *host_pattern);
289
290 /*********************************************************************
291  *
292  * Function    :  free_http_request
293  *
294  * Description :  Freez a http_request structure
295  *
296  * Parameters  :
297  *          1  :  http = points to a http_request structure to free
298  *
299  * Returns     :  N/A
300  *
301  *********************************************************************/
302 void free_http_request(struct http_request *http)
303 {
304    assert(http);
305
306    freez(http->cmd);
307    freez(http->ocmd);
308    freez(http->gpc);
309    freez(http->host);
310    freez(http->url);
311    freez(http->hostport);
312    freez(http->path);
313    freez(http->ver);
314    freez(http->host_ip_addr_str);
315    freez(http->dbuffer);
316    freez(http->dvec);
317    http->dcount = 0;
318 }
319
320
321 /*********************************************************************
322  *
323  * Function    :  init_domain_components
324  *
325  * Description :  Splits the domain name so we can compare it
326  *                against wildcards. It used to be part of
327  *                parse_http_url, but was separated because the
328  *                same code is required in chat in case of
329  *                intercepted requests.
330  *
331  * Parameters  :
332  *          1  :  http = pointer to the http structure to hold elements.
333  *
334  * Returns     :  JB_ERR_OK on success
335  *                JB_ERR_MEMORY on out of memory
336  *                JB_ERR_PARSE on malformed command/URL
337  *                             or >100 domains deep.
338  *
339  *********************************************************************/
340 jb_err init_domain_components(struct http_request *http)
341 {
342    char *vec[BUFFER_SIZE];
343    size_t size;
344    char *p;
345
346    http->dbuffer = strdup(http->host);
347    if (NULL == http->dbuffer)
348    {
349       return JB_ERR_MEMORY;
350    }
351
352    /* map to lower case */
353    for (p = http->dbuffer; *p ; p++)
354    {
355       *p = (char)tolower((int)(unsigned char)*p);
356    }
357
358    /* split the domain name into components */
359    http->dcount = ssplit(http->dbuffer, ".", vec, SZ(vec), 1, 1);
360
361    if (http->dcount <= 0)
362    {
363       /*
364        * Error: More than SZ(vec) components in domain
365        *    or: no components in domain
366        */
367       log_error(LOG_LEVEL_ERROR, "More than SZ(vec) components in domain or none at all.");
368       return JB_ERR_PARSE;
369    }
370
371    /* save a copy of the pointers in dvec */
372    size = (size_t)http->dcount * sizeof(*http->dvec);
373
374    http->dvec = (char **)malloc(size);
375    if (NULL == http->dvec)
376    {
377       return JB_ERR_MEMORY;
378    }
379
380    memcpy(http->dvec, vec, size);
381
382    return JB_ERR_OK;
383 }
384
385
386 /*********************************************************************
387  *
388  * Function    :  parse_http_url
389  *
390  * Description :  Parse out the host and port from the URL.  Find the
391  *                hostname & path, port (if ':'), and/or password (if '@')
392  *
393  * Parameters  :
394  *          1  :  url = URL (or is it URI?) to break down
395  *          2  :  http = pointer to the http structure to hold elements.
396  *                       Must be initialized with valid values (like NULLs).
397  *          3  :  require_protocol = Whether or not URLs without
398  *                                   protocol are acceptable.
399  *
400  * Returns     :  JB_ERR_OK on success
401  *                JB_ERR_MEMORY on out of memory
402  *                JB_ERR_PARSE on malformed command/URL
403  *                             or >100 domains deep.
404  *
405  *********************************************************************/
406 jb_err parse_http_url(const char *url, struct http_request *http, int require_protocol)
407 {
408    int host_available = 1; /* A proxy can dream. */
409
410    /*
411     * Save our initial URL
412     */
413    http->url = strdup(url);
414    if (http->url == NULL)
415    {
416       return JB_ERR_MEMORY;
417    }
418
419
420    /*
421     * Check for * URI. If found, we're done.
422     */  
423    if (*http->url == '*')
424    {
425       if  ( NULL == (http->path = strdup("*"))
426          || NULL == (http->hostport = strdup("")) ) 
427       {
428          return JB_ERR_MEMORY;
429       }
430       if (http->url[1] != '\0')
431       {
432          return JB_ERR_PARSE;
433       }
434       return JB_ERR_OK;
435    }
436
437
438    /*
439     * Split URL into protocol,hostport,path.
440     */
441    {
442       char *buf;
443       char *url_noproto;
444       char *url_path;
445
446       buf = strdup(url);
447       if (buf == NULL)
448       {
449          return JB_ERR_MEMORY;
450       }
451
452       /* Find the start of the URL in our scratch space */
453       url_noproto = buf;
454       if (strncmpic(url_noproto, "http://",  7) == 0)
455       {
456          url_noproto += 7;
457       }
458       else if (strncmpic(url_noproto, "https://", 8) == 0)
459       {
460          /*
461           * Should only happen when called from cgi_show_url_info().
462           */
463          url_noproto += 8;
464          http->ssl = 1;
465       }
466       else if (*url_noproto == '/')
467       {
468         /*
469          * Short request line without protocol and host.
470          * Most likely because the client's request
471          * was intercepted and redirected into Privoxy.
472          */
473          http->host = NULL;
474          host_available = 0;
475       }
476       else if (require_protocol)
477       {
478          freez(buf);
479          return JB_ERR_PARSE;
480       }
481
482       url_path = strchr(url_noproto, '/');
483       if (url_path != NULL)
484       {
485          /*
486           * Got a path.
487           *
488           * NOTE: The following line ignores the path for HTTPS URLS.
489           * This means that you get consistent behaviour if you type a
490           * https URL in and it's parsed by the function.  (When the
491           * URL is actually retrieved, SSL hides the path part).
492           */
493          http->path = strdup(http->ssl ? "/" : url_path);
494          *url_path = '\0';
495          http->hostport = strdup(url_noproto);
496       }
497       else
498       {
499          /*
500           * Repair broken HTTP requests that don't contain a path,
501           * or CONNECT requests
502           */
503          http->path = strdup("/");
504          http->hostport = strdup(url_noproto);
505       }
506
507       freez(buf);
508
509       if ( (http->path == NULL)
510         || (http->hostport == NULL))
511       {
512          return JB_ERR_MEMORY;
513       }
514    }
515
516    if (!host_available)
517    {
518       /* Without host, there is nothing left to do here */
519       return JB_ERR_OK;
520    }
521
522    /*
523     * Split hostport into user/password (ignored), host, port.
524     */
525    {
526       char *buf;
527       char *host;
528       char *port;
529
530       buf = strdup(http->hostport);
531       if (buf == NULL)
532       {
533          return JB_ERR_MEMORY;
534       }
535
536       /* check if url contains username and/or password */
537       host = strchr(buf, '@');
538       if (host != NULL)
539       {
540          /* Contains username/password, skip it and the @ sign. */
541          host++;
542       }
543       else
544       {
545          /* No username or password. */
546          host = buf;
547       }
548
549       /* Move after hostname before port number */
550       if (*host == '[')
551       {
552          /* Numeric IPv6 address delimited by brackets */
553          host++;
554          port = strchr(host, ']');
555
556          if (port == NULL)
557          {
558             /* Missing closing bracket */
559             freez(buf);
560             return JB_ERR_PARSE;
561          }
562
563          *port++ = '\0';
564
565          if (*port == '\0')
566          {
567             port = NULL;
568          }
569          else if (*port != ':')
570          {
571             /* Garbage after closing bracket */
572             freez(buf);
573             return JB_ERR_PARSE;
574          }
575       }
576       else
577       {
578          /* Plain non-escaped hostname */
579          port = strchr(host, ':');
580       }
581
582       /* check if url contains port */
583       if (port != NULL)
584       {
585          /* Contains port */
586          /* Terminate hostname and point to start of port string */
587          *port++ = '\0';
588          http->port = atoi(port);
589       }
590       else
591       {
592          /* No port specified. */
593          http->port = (http->ssl ? 443 : 80);
594       }
595
596       http->host = strdup(host);
597
598       freez(buf);
599
600       if (http->host == NULL)
601       {
602          return JB_ERR_MEMORY;
603       }
604    }
605
606    /*
607     * Split domain name so we can compare it against wildcards
608     */
609    return init_domain_components(http);
610
611 }
612
613
614 /*********************************************************************
615  *
616  * Function    :  unknown_method
617  *
618  * Description :  Checks whether a method is unknown.
619  *
620  * Parameters  :
621  *          1  :  method = points to a http method
622  *
623  * Returns     :  TRUE if it's unknown, FALSE otherwise.
624  *
625  *********************************************************************/
626 static int unknown_method(const char *method)
627 {
628    static const char *known_http_methods[] = {
629       /* Basic HTTP request type */
630       "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE", "CONNECT",
631       /* webDAV extensions (RFC2518) */
632       "PROPFIND", "PROPPATCH", "MOVE", "COPY", "MKCOL", "LOCK", "UNLOCK",
633       /*
634        * Microsoft webDAV extension for Exchange 2000.  See:
635        * http://lists.w3.org/Archives/Public/w3c-dist-auth/2002JanMar/0001.html
636        * http://msdn.microsoft.com/library/en-us/wss/wss/_webdav_methods.asp
637        */ 
638       "BCOPY", "BMOVE", "BDELETE", "BPROPFIND", "BPROPPATCH",
639       /*
640        * Another Microsoft webDAV extension for Exchange 2000.  See:
641        * http://systems.cs.colorado.edu/grunwald/MobileComputing/Papers/draft-cohen-gena-p-base-00.txt
642        * http://lists.w3.org/Archives/Public/w3c-dist-auth/2002JanMar/0001.html
643        * http://msdn.microsoft.com/library/en-us/wss/wss/_webdav_methods.asp
644        */ 
645       "SUBSCRIBE", "UNSUBSCRIBE", "NOTIFY", "POLL",
646       /*
647        * Yet another WebDAV extension, this time for
648        * Web Distributed Authoring and Versioning (RFC3253)
649        */
650       "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
651       "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY",
652    };
653    int i;
654
655    for (i = 0; i < SZ(known_http_methods); i++)
656    {
657       if (0 == strcmpic(method, known_http_methods[i]))
658       {
659          return FALSE;
660       }
661    }
662
663    return TRUE;
664
665 }
666
667
668 /*********************************************************************
669  *
670  * Function    :  parse_http_request
671  *
672  * Description :  Parse out the host and port from the URL.  Find the
673  *                hostname & path, port (if ':'), and/or password (if '@')
674  *
675  * Parameters  :
676  *          1  :  req = HTTP request line to break down
677  *          2  :  http = pointer to the http structure to hold elements
678  *
679  * Returns     :  JB_ERR_OK on success
680  *                JB_ERR_MEMORY on out of memory
681  *                JB_ERR_CGI_PARAMS on malformed command/URL
682  *                                  or >100 domains deep.
683  *
684  *********************************************************************/
685 jb_err parse_http_request(const char *req, struct http_request *http)
686 {
687    char *buf;
688    char *v[10]; /* XXX: Why 10? We should only need three. */
689    int n;
690    jb_err err;
691
692    memset(http, '\0', sizeof(*http));
693
694    buf = strdup(req);
695    if (buf == NULL)
696    {
697       return JB_ERR_MEMORY;
698    }
699
700    n = ssplit(buf, " \r\n", v, SZ(v), 1, 1);
701    if (n != 3)
702    {
703       freez(buf);
704       return JB_ERR_PARSE;
705    }
706
707    /*
708     * Fail in case of unknown methods
709     * which we might not handle correctly.
710     *
711     * XXX: There should be a config option
712     * to forward requests with unknown methods
713     * anyway. Most of them don't need special
714     * steps.
715     */
716    if (unknown_method(v[0]))
717    {
718       log_error(LOG_LEVEL_ERROR, "Unknown HTTP method detected: %s", v[0]);
719       freez(buf);
720       return JB_ERR_PARSE;
721    }
722
723    if (strcmpic(v[2], "HTTP/1.1") && strcmpic(v[2], "HTTP/1.0"))
724    {
725       log_error(LOG_LEVEL_ERROR, "The only supported HTTP "
726          "versions are 1.0 and 1.1. This rules out: %s", v[2]);
727       freez(buf);
728       return JB_ERR_PARSE;
729    }
730
731    http->ssl = !strcmpic(v[0], "CONNECT");
732
733    err = parse_http_url(v[1], http, !http->ssl);
734    if (err)
735    {
736       freez(buf);
737       return err;
738    }
739
740    /*
741     * Copy the details into the structure
742     */
743    http->cmd = strdup(req);
744    http->gpc = strdup(v[0]);
745    http->ver = strdup(v[2]);
746
747    freez(buf);
748
749    if ( (http->cmd == NULL)
750      || (http->gpc == NULL)
751      || (http->ver == NULL) )
752    {
753       return JB_ERR_MEMORY;
754    }
755
756    return JB_ERR_OK;
757
758 }
759
760
761 /*********************************************************************
762  *
763  * Function    :  compile_pattern
764  *
765  * Description :  Compiles a host, domain or TAG pattern.
766  *
767  * Parameters  :
768  *          1  :  pattern = The pattern to compile.
769  *          2  :  anchoring = How the regex should be anchored.
770  *                            Can be either one of NO_ANCHORING,
771  *                            LEFT_ANCHORED or RIGHT_ANCHORED.
772  *          3  :  url     = In case of failures, the spec member is
773  *                          logged and the structure freed.
774  *          4  :  regex   = Where the compiled regex should be stored.
775  *
776  * Returns     :  JB_ERR_OK - Success
777  *                JB_ERR_MEMORY - Out of memory
778  *                JB_ERR_PARSE - Cannot parse regex
779  *
780  *********************************************************************/
781 static jb_err compile_pattern(const char *pattern, enum regex_anchoring anchoring,
782                               struct url_spec *url, regex_t **regex)
783 {
784    int errcode;
785    char rebuf[BUFFER_SIZE];
786    const char *fmt = NULL;
787
788    assert(pattern);
789    assert(strlen(pattern) < sizeof(rebuf) - 2);
790
791    if (pattern[0] == '\0')
792    {
793       *regex = NULL;
794       return JB_ERR_OK;
795    }
796
797    switch (anchoring)
798    {
799       case NO_ANCHORING:
800          fmt = "%s";
801          break;
802       case RIGHT_ANCHORED:
803          fmt = "%s$";
804          break;
805       case LEFT_ANCHORED:
806          fmt = "^%s";
807          break;
808       default:
809          log_error(LOG_LEVEL_FATAL,
810             "Invalid anchoring in compile_pattern %d", anchoring);
811    }
812
813    *regex = zalloc(sizeof(**regex));
814    if (NULL == *regex)
815    {
816       free_url_spec(url);
817       return JB_ERR_MEMORY;
818    }
819
820    snprintf(rebuf, sizeof(rebuf), fmt, pattern);
821
822    errcode = regcomp(*regex, rebuf, (REG_EXTENDED|REG_NOSUB|REG_ICASE));
823
824    if (errcode)
825    {
826       size_t errlen = regerror(errcode, *regex, rebuf, sizeof(rebuf));
827       if (errlen > (sizeof(rebuf) - (size_t)1))
828       {
829          errlen = sizeof(rebuf) - (size_t)1;
830       }
831       rebuf[errlen] = '\0';
832       log_error(LOG_LEVEL_ERROR, "error compiling %s from %s: %s",
833          pattern, url->spec, rebuf);
834       free_url_spec(url);
835
836       return JB_ERR_PARSE;
837    }
838
839    return JB_ERR_OK;
840
841 }
842
843
844 /*********************************************************************
845  *
846  * Function    :  compile_url_pattern
847  *
848  * Description :  Compiles the three parts of an URL pattern.
849  *
850  * Parameters  :
851  *          1  :  url = Target url_spec to be filled in.
852  *          2  :  buf = The url pattern to compile. Will be messed up.
853  *
854  * Returns     :  JB_ERR_OK - Success
855  *                JB_ERR_MEMORY - Out of memory
856  *                JB_ERR_PARSE - Cannot parse regex
857  *
858  *********************************************************************/
859 static jb_err compile_url_pattern(struct url_spec *url, char *buf)
860 {
861    char *p;
862
863    p = strchr(buf, '/');
864    if (NULL != p)
865    {
866       /*
867        * Only compile the regex if it consists of more than
868        * a single slash, otherwise it wouldn't affect the result.
869        */
870       if (p[1] != '\0')
871       {
872          /*
873           * XXX: does it make sense to compile the slash at the beginning?
874           */
875          jb_err err = compile_pattern(p, LEFT_ANCHORED, url, &url->preg);
876
877          if (JB_ERR_OK != err)
878          {
879             return err;
880          }
881       }
882       *p = '\0';
883    }
884
885    /*
886     * IPv6 numeric hostnames can contain colons, thus we need
887     * to delimit the hostname before the real port separator.
888     * As brackets are already used in the hostname pattern,
889     * we use angle brackets ('<', '>') instead.
890     */
891    if ((buf[0] == '<') && (NULL != (p = strchr(buf + 1, '>'))))
892    {
893       *p++ = '\0';
894       buf++;
895
896       if (*p == '\0')
897       {
898          /* IPv6 address without port number */
899          p = NULL;
900       }
901       else if (*p != ':')
902       {
903          /* Garbage after address delimiter */
904          return JB_ERR_PARSE;
905       }
906    }
907    else
908    {
909       p = strchr(buf, ':');
910    }
911
912    if (NULL != p)
913    {
914       *p++ = '\0';
915       url->port_list = strdup(p);
916       if (NULL == url->port_list)
917       {
918          return JB_ERR_MEMORY;
919       }
920    }
921    else
922    {
923       url->port_list = NULL;
924    }
925
926    if (buf[0] != '\0')
927    {
928       return compile_host_pattern(url, buf);
929    }
930
931    return JB_ERR_OK;
932
933 }
934
935
936 #ifdef FEATURE_EXTENDED_HOST_PATTERNS
937 /*********************************************************************
938  *
939  * Function    :  compile_host_pattern
940  *
941  * Description :  Parses and compiles a host pattern..
942  *
943  * Parameters  :
944  *          1  :  url = Target url_spec to be filled in.
945  *          2  :  host_pattern = Host pattern to compile.
946  *
947  * Returns     :  JB_ERR_OK - Success
948  *                JB_ERR_MEMORY - Out of memory
949  *                JB_ERR_PARSE - Cannot parse regex
950  *
951  *********************************************************************/
952 static jb_err compile_host_pattern(struct url_spec *url, const char *host_pattern)
953 {
954    return compile_pattern(host_pattern, RIGHT_ANCHORED, url, &url->host_regex);
955 }
956
957 #else
958
959 /*********************************************************************
960  *
961  * Function    :  compile_host_pattern
962  *
963  * Description :  Parses and "compiles" an old-school host pattern.
964  *
965  * Parameters  :
966  *          1  :  url = Target url_spec to be filled in.
967  *          2  :  host_pattern = Host pattern to parse.
968  *
969  * Returns     :  JB_ERR_OK - Success
970  *                JB_ERR_MEMORY - Out of memory
971  *                JB_ERR_PARSE - Cannot parse regex
972  *
973  *********************************************************************/
974 static jb_err compile_host_pattern(struct url_spec *url, const char *host_pattern)
975 {
976    char *v[150];
977    size_t size;
978    char *p;
979
980    /*
981     * Parse domain part
982     */
983    if (host_pattern[strlen(host_pattern) - 1] == '.')
984    {
985       url->unanchored |= ANCHOR_RIGHT;
986    }
987    if (host_pattern[0] == '.')
988    {
989       url->unanchored |= ANCHOR_LEFT;
990    }
991
992    /* 
993     * Split domain into components
994     */
995    url->dbuffer = strdup(host_pattern);
996    if (NULL == url->dbuffer)
997    {
998       free_url_spec(url);
999       return JB_ERR_MEMORY;
1000    }
1001
1002    /* 
1003     * Map to lower case
1004     */
1005    for (p = url->dbuffer; *p ; p++)
1006    {
1007       *p = (char)tolower((int)(unsigned char)*p);
1008    }
1009
1010    /* 
1011     * Split the domain name into components
1012     */
1013    url->dcount = ssplit(url->dbuffer, ".", v, SZ(v), 1, 1);
1014
1015    if (url->dcount < 0)
1016    {
1017       free_url_spec(url);
1018       return JB_ERR_MEMORY;
1019    }
1020    else if (url->dcount != 0)
1021    {
1022       /* 
1023        * Save a copy of the pointers in dvec
1024        */
1025       size = (size_t)url->dcount * sizeof(*url->dvec);
1026       
1027       url->dvec = (char **)malloc(size);
1028       if (NULL == url->dvec)
1029       {
1030          free_url_spec(url);
1031          return JB_ERR_MEMORY;
1032       }
1033
1034       memcpy(url->dvec, v, size);
1035    }
1036    /*
1037     * else dcount == 0 in which case we needn't do anything,
1038     * since dvec will never be accessed and the pattern will
1039     * match all domains.
1040     */
1041    return JB_ERR_OK;
1042 }
1043
1044
1045 /*********************************************************************
1046  *
1047  * Function    :  simplematch
1048  *
1049  * Description :  String matching, with a (greedy) '*' wildcard that
1050  *                stands for zero or more arbitrary characters and
1051  *                character classes in [], which take both enumerations
1052  *                and ranges.
1053  *
1054  * Parameters  :
1055  *          1  :  pattern = pattern for matching
1056  *          2  :  text    = text to be matched
1057  *
1058  * Returns     :  0 if match, else nonzero
1059  *
1060  *********************************************************************/
1061 static int simplematch(const char *pattern, const char *text)
1062 {
1063    const unsigned char *pat = (const unsigned char *)pattern;
1064    const unsigned char *txt = (const unsigned char *)text;
1065    const unsigned char *fallback = pat; 
1066    int wildcard = 0;
1067   
1068    unsigned char lastchar = 'a';
1069    unsigned i;
1070    unsigned char charmap[32];
1071   
1072    while (*txt)
1073    {
1074
1075       /* EOF pattern but !EOF text? */
1076       if (*pat == '\0')
1077       {
1078          if (wildcard)
1079          {
1080             pat = fallback;
1081          }
1082          else
1083          {
1084             return 1;
1085          }
1086       }
1087
1088       /* '*' in the pattern?  */
1089       if (*pat == '*') 
1090       {
1091      
1092          /* The pattern ends afterwards? Speed up the return. */
1093          if (*++pat == '\0')
1094          {
1095             return 0;
1096          }
1097      
1098          /* Else, set wildcard mode and remember position after '*' */
1099          wildcard = 1;
1100          fallback = pat;
1101       }
1102
1103       /* Character range specification? */
1104       if (*pat == '[')
1105       {
1106          memset(charmap, '\0', sizeof(charmap));
1107
1108          while (*++pat != ']')
1109          {
1110             if (!*pat)
1111             { 
1112                return 1;
1113             }
1114             else if (*pat == '-')
1115             {
1116                if ((*++pat == ']') || *pat == '\0')
1117                {
1118                   return(1);
1119                }
1120                for (i = lastchar; i <= *pat; i++)
1121                {
1122                   charmap[i / 8] |= (unsigned char)(1 << (i % 8));
1123                } 
1124             }
1125             else
1126             {
1127                charmap[*pat / 8] |= (unsigned char)(1 << (*pat % 8));
1128                lastchar = *pat;
1129             }
1130          }
1131       } /* -END- if Character range specification */
1132
1133
1134       /* 
1135        * Char match, or char range match? 
1136        */
1137       if ( (*pat == *txt)
1138       ||   (*pat == '?')
1139       ||   ((*pat == ']') && (charmap[*txt / 8] & (1 << (*txt % 8)))) )
1140       {
1141          /* 
1142           * Sucess: Go ahead
1143           */
1144          pat++;
1145       }
1146       else if (!wildcard)
1147       {
1148          /* 
1149           * No match && no wildcard: No luck
1150           */
1151          return 1;
1152       }
1153       else if (pat != fallback)
1154       {
1155          /*
1156           * Increment text pointer if in char range matching
1157           */
1158          if (*pat == ']')
1159          {
1160             txt++;
1161          }
1162          /*
1163           * Wildcard mode && nonmatch beyond fallback: Rewind pattern
1164           */
1165          pat = fallback;
1166          /*
1167           * Restart matching from current text pointer
1168           */
1169          continue;
1170       }
1171       txt++;
1172    }
1173
1174    /* Cut off extra '*'s */
1175    if(*pat == '*')  pat++;
1176
1177    /* If this is the pattern's end, fine! */
1178    return(*pat);
1179
1180 }
1181
1182
1183 /*********************************************************************
1184  *
1185  * Function    :  simple_domaincmp
1186  *
1187  * Description :  Domain-wise Compare fqdn's.  The comparison is
1188  *                both left- and right-anchored.  The individual
1189  *                domain names are compared with simplematch().
1190  *                This is only used by domain_match.
1191  *
1192  * Parameters  :
1193  *          1  :  pv = array of patterns to compare
1194  *          2  :  fv = array of domain components to compare
1195  *          3  :  len = length of the arrays (both arrays are the
1196  *                      same length - if they weren't, it couldn't
1197  *                      possibly be a match).
1198  *
1199  * Returns     :  0 => domains are equivalent, else no match.
1200  *
1201  *********************************************************************/
1202 static int simple_domaincmp(char **pv, char **fv, int len)
1203 {
1204    int n;
1205
1206    for (n = 0; n < len; n++)
1207    {
1208       if (simplematch(pv[n], fv[n]))
1209       {
1210          return 1;
1211       }
1212    }
1213
1214    return 0;
1215
1216 }
1217
1218
1219 /*********************************************************************
1220  *
1221  * Function    :  domain_match
1222  *
1223  * Description :  Domain-wise Compare fqdn's. Governed by the bimap in
1224  *                pattern->unachored, the comparison is un-, left-,
1225  *                right-anchored, or both.
1226  *                The individual domain names are compared with
1227  *                simplematch().
1228  *
1229  * Parameters  :
1230  *          1  :  pattern = a domain that may contain a '*' as a wildcard.
1231  *          2  :  fqdn = domain name against which the patterns are compared.
1232  *
1233  * Returns     :  0 => domains are equivalent, else no match.
1234  *
1235  *********************************************************************/
1236 static int domain_match(const struct url_spec *pattern, const struct http_request *fqdn)
1237 {
1238    char **pv, **fv;  /* vectors  */
1239    int    plen, flen;
1240    int unanchored = pattern->unanchored & (ANCHOR_RIGHT | ANCHOR_LEFT);
1241
1242    plen = pattern->dcount;
1243    flen = fqdn->dcount;
1244
1245    if (flen < plen)
1246    {
1247       /* fqdn is too short to match this pattern */
1248       return 1;
1249    }
1250
1251    pv   = pattern->dvec;
1252    fv   = fqdn->dvec;
1253
1254    if (unanchored == ANCHOR_LEFT)
1255    {
1256       /*
1257        * Right anchored.
1258        *
1259        * Convert this into a fully anchored pattern with
1260        * the fqdn and pattern the same length
1261        */
1262       fv += (flen - plen); /* flen - plen >= 0 due to check above */
1263       return simple_domaincmp(pv, fv, plen);
1264    }
1265    else if (unanchored == 0)
1266    {
1267       /* Fully anchored, check length */
1268       if (flen != plen)
1269       {
1270          return 1;
1271       }
1272       return simple_domaincmp(pv, fv, plen);
1273    }
1274    else if (unanchored == ANCHOR_RIGHT)
1275    {
1276       /* Left anchored, ignore all extra in fqdn */
1277       return simple_domaincmp(pv, fv, plen);
1278    }
1279    else
1280    {
1281       /* Unanchored */
1282       int n;
1283       int maxn = flen - plen;
1284       for (n = 0; n <= maxn; n++)
1285       {
1286          if (!simple_domaincmp(pv, fv, plen))
1287          {
1288             return 0;
1289          }
1290          /*
1291           * Doesn't match from start of fqdn
1292           * Try skipping first part of fqdn
1293           */
1294          fv++;
1295       }
1296       return 1;
1297    }
1298
1299 }
1300 #endif /* def FEATURE_EXTENDED_HOST_PATTERNS */
1301
1302
1303 /*********************************************************************
1304  *
1305  * Function    :  create_url_spec
1306  *
1307  * Description :  Creates a "url_spec" structure from a string.
1308  *                When finished, free with free_url_spec().
1309  *
1310  * Parameters  :
1311  *          1  :  url = Target url_spec to be filled in.  Will be
1312  *                      zeroed before use.
1313  *          2  :  buf = Source pattern, null terminated.  NOTE: The
1314  *                      contents of this buffer are destroyed by this
1315  *                      function.  If this function succeeds, the
1316  *                      buffer is copied to url->spec.  If this
1317  *                      function fails, the contents of the buffer
1318  *                      are lost forever.
1319  *
1320  * Returns     :  JB_ERR_OK - Success
1321  *                JB_ERR_MEMORY - Out of memory
1322  *                JB_ERR_PARSE - Cannot parse regex (Detailed message
1323  *                               written to system log)
1324  *
1325  *********************************************************************/
1326 jb_err create_url_spec(struct url_spec *url, char *buf)
1327 {
1328    assert(url);
1329    assert(buf);
1330
1331    memset(url, '\0', sizeof(*url));
1332
1333    /* Remember the original specification for the CGI pages. */
1334    url->spec = strdup(buf);
1335    if (NULL == url->spec)
1336    {
1337       return JB_ERR_MEMORY;
1338    }
1339
1340    /* Is it tag pattern? */
1341    if (0 == strncmpic("TAG:", url->spec, 4))
1342    {
1343       /* The pattern starts with the first character after "TAG:" */
1344       const char *tag_pattern = buf + 4;
1345       return compile_pattern(tag_pattern, NO_ANCHORING, url, &url->tag_regex);
1346    }
1347
1348    /* If it isn't a tag pattern it must be a URL pattern. */
1349    return compile_url_pattern(url, buf);
1350 }
1351
1352
1353 /*********************************************************************
1354  *
1355  * Function    :  free_url_spec
1356  *
1357  * Description :  Called from the "unloaders".  Freez the url
1358  *                structure elements.
1359  *
1360  * Parameters  :
1361  *          1  :  url = pointer to a url_spec structure.
1362  *
1363  * Returns     :  N/A
1364  *
1365  *********************************************************************/
1366 void free_url_spec(struct url_spec *url)
1367 {
1368    if (url == NULL) return;
1369
1370    freez(url->spec);
1371 #ifdef FEATURE_EXTENDED_HOST_PATTERNS
1372    if (url->host_regex)
1373    {
1374       regfree(url->host_regex);
1375       freez(url->host_regex);
1376    }
1377 #else
1378    freez(url->dbuffer);
1379    freez(url->dvec);
1380    url->dcount = 0;
1381 #endif /* ndef FEATURE_EXTENDED_HOST_PATTERNS */
1382    freez(url->port_list);
1383    if (url->preg)
1384    {
1385       regfree(url->preg);
1386       freez(url->preg);
1387    }
1388    if (url->tag_regex)
1389    {
1390       regfree(url->tag_regex);
1391       freez(url->tag_regex);
1392    }
1393 }
1394
1395
1396 /*********************************************************************
1397  *
1398  * Function    :  url_match
1399  *
1400  * Description :  Compare a URL against a URL pattern.
1401  *
1402  * Parameters  :
1403  *          1  :  pattern = a URL pattern
1404  *          2  :  url = URL to match
1405  *
1406  * Returns     :  Nonzero if the URL matches the pattern, else 0.
1407  *
1408  *********************************************************************/
1409 int url_match(const struct url_spec *pattern,
1410               const struct http_request *http)
1411 {
1412    /* XXX: these should probably be functions. */
1413 #define PORT_MATCHES ((NULL == pattern->port_list) || match_portlist(pattern->port_list, http->port))
1414 #ifdef FEATURE_EXTENDED_HOST_PATTERNS
1415 #define DOMAIN_MATCHES ((NULL == pattern->host_regex) || (0 == regexec(pattern->host_regex, http->host, 0, NULL, 0)))
1416 #else
1417 #define DOMAIN_MATCHES ((NULL == pattern->dbuffer) || (0 == domain_match(pattern, http)))
1418 #endif
1419 #define PATH_MATCHES ((NULL == pattern->preg) || (0 == regexec(pattern->preg, http->path, 0, NULL, 0)))
1420
1421    if (pattern->tag_regex != NULL)
1422    {
1423       /* It's a tag pattern and shouldn't be matched against URLs */
1424       return 0;
1425    } 
1426
1427    return (PORT_MATCHES && DOMAIN_MATCHES && PATH_MATCHES);
1428
1429 }
1430
1431
1432 /*********************************************************************
1433  *
1434  * Function    :  match_portlist
1435  *
1436  * Description :  Check if a given number is covered by a comma
1437  *                separated list of numbers and ranges (a,b-c,d,..)
1438  *
1439  * Parameters  :
1440  *          1  :  portlist = String with list
1441  *          2  :  port = port to check
1442  *
1443  * Returns     :  0 => no match
1444  *                1 => match
1445  *
1446  *********************************************************************/
1447 int match_portlist(const char *portlist, int port)
1448 {
1449    char *min, *max, *next, *portlist_copy;
1450
1451    min = next = portlist_copy = strdup(portlist);
1452
1453    /*
1454     * Zero-terminate first item and remember offset for next
1455     */
1456    if (NULL != (next = strchr(portlist_copy, (int) ',')))
1457    {
1458       *next++ = '\0';
1459    }
1460
1461    /*
1462     * Loop through all items, checking for match
1463     */
1464    while(min)
1465    {
1466       if (NULL == (max = strchr(min, (int) '-')))
1467       {
1468          /*
1469           * No dash, check for equality
1470           */
1471          if (port == atoi(min))
1472          {
1473             freez(portlist_copy);
1474             return(1);
1475          }
1476       }
1477       else
1478       {
1479          /*
1480           * This is a range, so check if between min and max,
1481           * or, if max was omitted, between min and 65K
1482           */
1483          *max++ = '\0';
1484          if(port >= atoi(min) && port <= (atoi(max) ? atoi(max) : 65535))
1485          {
1486             freez(portlist_copy);
1487             return(1);
1488          }
1489
1490       }
1491
1492       /*
1493        * Jump to next item
1494        */
1495       min = next;
1496
1497       /*
1498        * Zero-terminate next item and remember offset for n+1
1499        */
1500       if ((NULL != next) && (NULL != (next = strchr(next, (int) ','))))
1501       {
1502          *next++ = '\0';
1503       }
1504    }
1505
1506    freez(portlist_copy);
1507    return 0;
1508
1509 }
1510
1511
1512 /*********************************************************************
1513  *
1514  * Function    :  parse_forwarder_address
1515  *
1516  * Description :  Parse out the host and port from a forwarder address.
1517  *
1518  * Parameters  :
1519  *          1  :  address = The forwarder address to parse.
1520  *          2  :  hostname = Used to return the hostname. NULL on error.
1521  *          3  :  port = Used to return the port. Untouched if no port
1522  *                       is specified.
1523  *
1524  * Returns     :  JB_ERR_OK on success
1525  *                JB_ERR_MEMORY on out of memory
1526  *                JB_ERR_PARSE on malformed address.
1527  *
1528  *********************************************************************/
1529 jb_err parse_forwarder_address(char *address, char **hostname, int *port)
1530 {
1531    char *p = address;
1532
1533    if ((*address == '[') && (NULL == strchr(address, ']')))
1534    {
1535       /* XXX: Should do some more validity checks here. */
1536       return JB_ERR_PARSE;
1537    }
1538
1539    *hostname = strdup(address);
1540    if (NULL == *hostname)
1541    {
1542       return JB_ERR_MEMORY;
1543    }
1544
1545    if ((**hostname == '[') && (NULL != (p = strchr(*hostname, ']'))))
1546    {
1547       *p++ = '\0';
1548       memmove(*hostname, (*hostname + 1), (size_t)(p - *hostname));
1549       if (*p == ':')
1550       {
1551          *port = (int)strtol(++p, NULL, 0);
1552       }
1553    }
1554    else if (NULL != (p = strchr(*hostname, ':')))
1555    {
1556       *p++ = '\0';
1557       *port = (int)strtol(p, NULL, 0);
1558    }
1559
1560    return JB_ERR_OK;
1561
1562 }
1563
1564
1565 /*
1566   Local Variables:
1567   tab-width: 3
1568   end:
1569 */