Changed to include many of the new actions.
[privoxy.git] / pcrs.c
1 const char pcrs_rcs[] = "$Id: pcrs.c,v 1.4 2001/05/25 14:12:40 oes Exp $";
2
3 /*********************************************************************
4  *
5  * File        :  $Source: /cvsroot/ijbswa/current/pcrs.c,v $
6  *
7  * Purpose     :  This is the alpha release of libpcrs. It is only published
8  *                at this early stage of development, because it is
9  *                needed for a new feature in JunkBuster.
10  *
11  *                While no inconsistencies, memory leaks or functional bugs
12  *                are known at this time, there *could* be plenty ;-). Also,
13  *                Many pcre-specific options are not yet supported, and
14  *                error handling needs improvement.
15  *
16  *                pcrs is a supplement to the brilliant pcre library by Philip
17  *                Hazel (ph10@cam.ac.uk) and adds Perl-style substitution. That
18  *                is, it mimics Perl's 's' operator.
19  *
20  *                Currently, there's no documentation besides comments and the
21  *                source itself ;-)
22  *
23  * Copyright   :  Written and Copyright (C) 2000 by Andreas Oesterhelt
24  *                <andreas@oesterhelt.org>
25  *
26  *                This program is free software; you can redistribute it 
27  *                and/or modify it under the terms of the GNU General
28  *                Public License as published by the Free Software
29  *                Foundation; either version 2 of the License, or (at
30  *                your option) any later version.
31  *
32  *                This program is distributed in the hope that it will
33  *                be useful, but WITHOUT ANY WARRANTY; without even the
34  *                implied warranty of MERCHANTABILITY or FITNESS FOR A
35  *                PARTICULAR PURPOSE.  See the GNU General Public
36  *                License for more details.
37  *
38  *                The GNU General Public License should be included with
39  *                this file.  If not, you can view it at
40  *                http://www.gnu.org/copyleft/gpl.html
41  *                or write to the Free Software Foundation, Inc., 59
42  *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
43  *
44  * Revisions   :
45  *    $Log: pcrs.c,v $
46  *    Revision 1.4  2001/05/25 14:12:40  oes
47  *    Fixed bug: Empty substitutes now detected
48  *
49  *    Revision 1.3  2001/05/25 11:03:55  oes
50  *    Added sanity check for NULL jobs to pcrs_exec_substitution
51  *
52  *    Revision 1.2  2001/05/22 18:46:04  oes
53  *
54  *    - Enabled filtering banners by size rather than URL
55  *      by adding patterns that replace all standard banner
56  *      sizes with the "Junkbuster" gif to the re_filterfile
57  *
58  *    - Enabled filtering WebBugs by providing a pattern
59  *      which kills all 1x1 images
60  *
61  *    - Added support for PCRE_UNGREEDY behaviour to pcrs,
62  *      which is selected by the (nonstandard and therefore
63  *      capital) letter 'U' in the option string.
64  *      It causes the quantifiers to be ungreedy by default.
65  *      Appending a ? turns back to greedy (!).
66  *
67  *    - Added a new interceptor ijb-send-banner, which
68  *      sends back the "Junkbuster" gif. Without imagelist or
69  *      MSIE detection support, or if tinygif = 1, or the
70  *      URL isn't recognized as an imageurl, a lame HTML
71  *      explanation is sent instead.
72  *
73  *    - Added new feature, which permits blocking remote
74  *      script redirects and firing back a local redirect
75  *      to the browser.
76  *      The feature is conditionally compiled, i.e. it
77  *      can be disabled with --disable-fast-redirects,
78  *      plus it must be activated by a "fast-redirects"
79  *      line in the config file, has its own log level
80  *      and of course wants to be displayed by show-proxy-args
81  *      Note: Boy, all the #ifdefs in 1001 locations and
82  *      all the fumbling with configure.in and acconfig.h
83  *      were *way* more work than the feature itself :-(
84  *
85  *    - Because a generic redirect template was needed for
86  *      this, tinygif = 3 now uses the same.
87  *
88  *    - Moved GIFs, and other static HTTP response templates
89  *      to project.h
90  *
91  *    - Some minor fixes
92  *
93  *    - Removed some >400 CRs again (Jon, you really worked
94  *      a lot! ;-)
95  *
96  *    Revision 1.1.1.1  2001/05/15 13:59:02  oes
97  *    Initial import of version 2.9.3 source tree
98  *
99  *
100  *********************************************************************/
101 \f
102
103 #include <pcre.h>
104 #include <string.h>
105 #include "pcrs.h"
106 const char pcrs_h_rcs[] = PCRS_H_VERSION;
107
108
109 /*********************************************************************
110  *
111  * Function    :  my_strsep
112  *
113  * Description :  Convenience function. It acts like strsep, except that
114  *                it respects quoting of the delimiter character with the
115  *                quote character. (And, of course, quoting the quote char
116  *                with itself.)  Called from `pcrs_make_job'.
117  *
118  * Parameters  :
119  *          1  :  token = current token
120  *          2  :  text = string to tokenize
121  *          3  :  delimiter = single character deliminter
122  *          4  :  quote_char = character to cause quoting
123  *
124  * Returns     :  -1 => failure, else the length of the token found.
125  *                In the latter case, *text is the token's start.
126  *
127  *********************************************************************/
128 int my_strsep(char *token, char **text, char delimiter, char quote_char)
129 {
130    int i, k=0, limit, quoted = FALSE;
131
132    limit = strlen(*text);
133    if ( 0 == limit )
134    {
135       return -1;
136    }
137
138    token[0] = '\0';
139
140    for (i=0; i < limit; i++)
141    {
142       if (text[0][i] == delimiter && !quoted)
143       {
144          *text += 1;
145          break;
146       }
147       else if (text[0][i] == quote_char && !quoted && i+1 < limit && text[0][i+1] == delimiter)
148       {
149          quoted = TRUE;
150          continue;
151       }
152       token[k++] = text[0][i];
153       quoted = FALSE;
154    }
155    token[k] = '\0';
156    *text += i;
157    return k;
158
159 }
160
161
162 /*********************************************************************
163  *
164  * Function    :  pcrs_compile_perl_options
165  *
166  * Description :  This function parses a string containing the options to
167  *                Perl's s/// operator. It returns an integer that is the
168  *                pcre equivalent of the symbolic optstring.
169  *                Since pcre doesn't know about Perl's 'g' (global) option,
170  *                but pcrs needs it, the globalflag integer is set if 'g'
171  *                is encountered.
172  *
173  * Parameters  :
174  *          1  :  optstring = string with options in perl syntax
175  *          2  :  globalflag = see description
176  *
177  * Returns     :  option integer suitable for pcre 
178  *
179  *********************************************************************/
180 int pcrs_compile_perl_options(char *optstring, int *globalflag)
181 {
182    size_t i;
183    int rc = 0;
184    *globalflag = 0;
185    for (i=0; i < strlen(optstring); i++)
186    {
187       switch(optstring[i])
188       {
189          case 'e': break;
190          case 'g': *globalflag = 1; break;
191          case 'i': rc |= PCRE_CASELESS; break;
192          case 'm': rc |= PCRE_MULTILINE; break;
193          case 'o': break;
194          case 's': rc |= PCRE_DOTALL; break;
195          case 'x': rc |= PCRE_EXTENDED; break;
196          case 'U': rc |= PCRE_UNGREEDY; break;
197          default:  break;
198       }
199    }
200    return rc;
201
202 }
203
204
205 /*********************************************************************
206  *
207  * Function    :  pcrs_compile_replacement
208  *
209  * Description :  This function takes a Perl-style replacement (2nd argument
210  *                to the s/// operator and returns a compiled pcrs_substitute,
211  *                or NULL if memory allocation for the substitute structure
212  *                fails.
213  *
214  * Parameters  :
215  *          1  :  replacement = replacement part of s/// operator
216  *                              in perl syntax
217  *          2  :  errptr = pointer to an integer in which error
218  *                         conditions can be returned.
219  *
220  * Returns     :  pcrs_substitute data structure, or NULL if an
221  *                error is encountered. In that case, *errptr has
222  *                the reason.
223  *
224  *********************************************************************/
225 pcrs_substitute *pcrs_compile_replacement(char *replacement, int *errptr)
226 {
227    int length, i, k = 0, l = 0, quoted = 0, idx;
228    char *text, *num_ptr, *numbers = "0123456789";
229    pcrs_substitute *r;
230
231    r = (pcrs_substitute *)malloc(sizeof(pcrs_substitute));
232    if (r == NULL) return NULL;
233    memset(r, '\0', sizeof(pcrs_substitute));
234
235    text = strdup(replacement);      /* must be free()d by caller */
236    if (text  == NULL)
237    {
238       *errptr = PCRS_ERR_NOMEM;
239       free(r);
240       return NULL;
241    }
242
243    length = strlen(replacement);
244
245    for (i=0; i < length; i++)
246    {
247       /* Backslash treatment */
248       if (replacement[i] == '\\')
249       {
250          if (quoted)
251          {
252             text[k++] = replacement[i];
253             quoted = 0;
254          }
255          else
256          {
257             quoted = 1;
258          }
259          continue;
260       }
261
262       /* Dollar treatment */
263       if (replacement[i] == '$' && !quoted && i < length - 1)
264       {
265          if (strchr("0123456789&", replacement[i + 1]) == NULL)
266          {
267             text[k++] = replacement[i];
268          }
269          else
270          {
271             r->block_length[l] = k - r->block_offset[l];
272             r->backref[l] = 0;
273             if (replacement[i + 1] != '&')
274             {
275                while ((num_ptr = strchr(numbers, replacement[++i])) != NULL && i < length)
276                {
277                   idx = num_ptr - numbers;
278                   r->backref[l] = r->backref[l] * 10 + idx;
279                }
280                i--;
281             }
282             else
283                i++;
284             if (r->backref[l] < PCRS_MAX_SUBMATCHES)
285                r->backref_count[r->backref[l]] += 1;
286             l++;
287             r->block_offset[l] = k;
288          }
289          continue;
290       }
291
292       /* Plain char treatment */
293       text[k++] = replacement[i];
294       quoted = 0;
295    }
296
297    text[k] = '\0';
298    r->text = text;
299    r->backrefs = l;
300    r->block_length[l] = k - r->block_offset[l];
301    return r;
302
303 }
304
305
306 /*********************************************************************
307  *
308  * Function    :  pcrs_free_job
309  *
310  * Description :  Frees the memory used by a pcrs_job struct and its
311  *                dependant structures. Returns a pointer to the next
312  *                job, if there was any, or NULL otherwise.
313  *
314  * Parameters  :
315  *          1  :  job = pointer to the pcrs_job structure to be freed
316  *
317  * Returns     : a pointer to the next job, if there was any, or
318  *               NULL otherwise. 
319  *
320  *********************************************************************/
321 pcrs_job *pcrs_free_job(pcrs_job *job)
322 {
323    pcrs_job *next;
324
325    if (job == NULL)
326    {
327       return NULL;
328    }
329    else
330    {
331       next = job->next;
332       if (job->pattern != NULL) free(job->pattern);
333       if (job->hints != NULL) free(job->hints);
334       if (job->substitute != NULL)
335       {
336          if (job->substitute->text != NULL) free(job->substitute->text);
337          free(job->substitute);
338       }
339       free(job);
340    }
341    return next;
342
343 }
344
345
346 /*********************************************************************
347  *
348  * Function    :  pcrs_make_job
349  *
350  * Description :  Main entry point. Takes a string with a Perl-style
351  *                s/// command and returns a corresponding pcrs_job,
352  *                or NULL if compiling the job fails at any stage.
353  *                Diagnostics could obviously be improved.
354  *
355  * Parameters  :
356  *          1  :  command = string with perl-style s/// command
357  *          2  :  errptr = pointer to an integer in which error
358  *                         conditions can be returned.
359  *
360  * Returns     :  a corresponding pcrs_job data structure, or NULL
361  *                if an error was encountered. In that case, *errptr
362  *                has the reason.
363  *
364  *********************************************************************/
365 pcrs_job *pcrs_make_job(char *command, int *errptr)
366 {
367    char *dummy, *token, delimiter;
368    const char *error;
369    int i = 0, globalflag;
370    pcrs_job *newjob;
371
372    /* Get and init memory */
373    if ((newjob = (pcrs_job *)malloc(sizeof(pcrs_job))) == NULL)
374    {
375       *errptr = PCRS_ERR_NOMEM;
376       return NULL;
377    }
378    memset(newjob, '\0', sizeof(pcrs_job));
379
380    /* Command too short? */
381    if (strlen(command) < 4)
382    {
383       *errptr = PCRS_ERR_CMDSYNTAX;
384       pcrs_free_job(newjob);
385       return NULL;
386    }
387
388    /* Split command into tokens and handle them */
389    delimiter = command[1];
390    token = (char *)malloc(strlen(command)); /* current token */
391    dummy = (char *)malloc(strlen(command)); /* must store pattern, since we can't */
392                                             /* use it until the options are known */
393    while (my_strsep(token, &command, delimiter, '\\') >= 0)
394    {
395       switch (i)
396       {
397          /* We don't care about the command and assume 's' */
398          case 0:
399             break;
400
401          /* The pattern */
402          case 1:
403             strcpy(dummy, token);
404             break;
405
406          /* The substitute */
407          case 2:
408             if ((newjob->substitute = pcrs_compile_replacement(token, errptr)) == NULL)
409             {
410                pcrs_free_job(newjob);
411                return NULL;
412             }
413             else
414             {
415                break;
416             }
417
418          /* The options */
419          case 3:
420             newjob->options = pcrs_compile_perl_options(token, &globalflag);
421             newjob->globalflag = globalflag;
422             break;
423
424          /* There shouldn't be anything else! */
425          default:
426             *errptr = PCRS_ERR_CMDSYNTAX;
427             pcrs_free_job(newjob);
428             return NULL;
429       }
430       i++;
431    }
432    free(token);
433
434    /* We have a valid substitute? */
435    if (newjob->substitute == NULL)
436    {
437       *errptr = PCRS_ERR_CMDSYNTAX;
438       pcrs_free_job(newjob);
439       return NULL;
440    }
441
442    /* Compile the pattern */
443    newjob->pattern = pcre_compile(dummy, newjob->options, &error, errptr, NULL);
444    if (newjob->pattern == NULL)
445    {
446       pcrs_free_job(newjob);
447       return NULL;
448    }
449    free(dummy);
450
451    /*
452     * Generate hints. This has little overhead, since the
453     * hints will be NULL for a boring pattern anyway.
454     */
455    newjob->hints = pcre_study(newjob->pattern, 0, &error);
456    if (error != NULL)
457    {
458       *errptr = PCRS_ERR_STUDY;
459       pcrs_free_job(newjob);
460       return NULL;
461    }
462
463    return newjob;
464
465 }
466
467
468 /*********************************************************************
469  *
470  * Function    :  create_pcrs_job
471  *
472  * Description :  Create a job from all its components, if you don't
473  *                have a Perl command to start from. Rather boring.
474  *
475  * Parameters  :
476  *          1  :  pattern = pointer to pcre pattern
477  *          2  :  hints = pointer to pcre hints
478  *          3  :  options = options in pcre format
479  *          4  :  globalflag = flag that indicates if global matching is desired
480  *          5  :  substitute = pointer to pcrs_substitute data structure
481  *          2  :  errptr = pointer to an integer in which error
482  *                         conditions can be returned.
483  *
484  * Returns     :  pcrs_job structure, or NULL if an error was encountered.
485  *                In that case, *errptr has the reason why.
486  *
487  *********************************************************************/
488 pcrs_job *create_pcrs_job(pcre *pattern, pcre_extra *hints, int options, int globalflag, pcrs_substitute *substitute, int *errptr)
489 {
490    pcrs_job *newjob;
491
492    if ((newjob = (pcrs_job *)malloc(sizeof(pcrs_job))) == NULL)
493    {
494       *errptr = PCRS_ERR_NOMEM;
495       return NULL;
496    }
497    memset(newjob, '\0', sizeof(pcrs_job));
498
499    newjob->pattern = pattern;
500    newjob->hints = hints;
501    newjob->options = options;
502    newjob->globalflag = globalflag;
503    newjob->substitute = substitute;
504
505    return(newjob);
506
507 }
508
509
510 /*********************************************************************
511  *
512  * Function    :  pcrs_exec_substitution
513  *
514  * Description :  Modify the subject by executing the regular substitution
515  *                defined by the job. Since the result may be longer than
516  *                the subject, its space requirements are precalculated in
517  *                the matching phase and new memory is allocated accordingly.
518  *                It is the caller's responsibility to free the result when
519  *                it's no longer needed.
520  *
521  * Parameters  :
522  *          1  :  job = the pcrs_job to be executed
523  *          2  :  subject = the subject (== original) string
524  *          3  :  subject_length = the subject's length
525  *          4  :  result = char** for returning  the result 
526  *          5  :  result_length = int* for returning the result's length
527  *
528  * Returns     :  the number of substitutions that were made. May be > 1
529  *                if job->globalflag was set
530  *
531  *********************************************************************/
532 int pcrs_exec_substitution(pcrs_job *job, char *subject, int subject_length, char **result, int *result_length)
533 {
534    int offsets[3 * PCRS_MAX_SUBMATCHES],
535       offset = 0, i=0, k, matches_found, newsize, submatches;
536    pcrs_match matches[PCRS_MAX_MATCHES];
537    char *result_offset;
538
539    /* Sanity first */
540    if (job == NULL || job->pattern == NULL || job->substitute == NULL)
541    {
542       *result = NULL;
543       return(PCRS_ERR_BADJOB);
544    }
545
546    newsize=subject_length;
547
548    /* Find.. */
549    while ((submatches = pcre_exec(job->pattern, job->hints, subject, subject_length, offset, 0, offsets, 99)) > 0)
550    {
551       matches[i].submatches = submatches;
552       for (k=0; k < submatches; k++)
553       {
554          matches[i].submatch_offset[k] = offsets[2 * k];
555          matches[i].submatch_length[k] = offsets[2 * k + 1] - offsets[2 * k]; /* Non-found optional submatches have length -1-(-1)==0 */
556          newsize += matches[i].submatch_length[k] * job->substitute->backref_count[k]; /* reserve mem for each submatch as often as it is ref'd */
557       }
558       newsize += strlen(job->substitute->text) - matches[i].submatch_length[0]; /* plus replacement text size minus match text size */
559
560       /* Non-global search or limit reached? */
561       if (++i >= PCRS_MAX_MATCHES || !job->globalflag ) break;
562
563       /* Don't loop on empty matches */
564       if (offsets[1] == offset)
565          if (offset < subject_length)
566             offset++;
567          else
568             break;
569       /* Go find the next one */
570       else
571          offset = offsets[1];
572    }
573    if (submatches < -1) return submatches;   /* Pass pcre error through */
574    matches_found = i;
575
576    /* ..get memory ..*/
577    if ((*result = (char *)malloc(newsize)) == NULL)   /* must be free()d by caller */
578    {
579       return PCRS_ERR_NOMEM;
580    }
581
582    /* ..and replace */
583    offset = 0;
584    result_offset = *result;
585
586    for (i=0; i < matches_found; i++)
587    {
588       memcpy(result_offset, subject + offset, matches[i].submatch_offset[0] - offset); /* copy the chunk preceding the match */
589       result_offset += matches[i].submatch_offset[0] - offset;
590
591       /* For every segment of the substitute.. */
592       for (k=0; k <= job->substitute->backrefs; k++)
593       {
594          /* ...copy its text.. */
595          memcpy(result_offset, job->substitute->text + job->substitute->block_offset[k], job->substitute->block_length[k]);
596          result_offset += job->substitute->block_length[k];
597
598          /* ..plus, if it's not the last chunk (i.e.: There IS a backref).. */
599          if (k != job->substitute->backrefs
600              /* ..and a nonempty match.. */
601              && matches[i].submatch_length[job->substitute->backref[k]] > 0
602              /* ..and in legal range, ... */
603              && job->substitute->backref[k] <= PCRS_MAX_SUBMATCHES)
604          {
605             /* copy the submatch that is ref'd. */
606             memcpy(
607                result_offset,
608                subject + matches[i].submatch_offset[job->substitute->backref[k]],
609                matches[i].submatch_length[job->substitute->backref[k]]
610             );
611             result_offset += matches[i].submatch_length[job->substitute->backref[k]];
612          }
613       }
614       offset =  matches[i].submatch_offset[0] + matches[i].submatch_length[0];
615    }
616
617    /* Copy the rest. */
618    memcpy(result_offset, subject + offset, subject_length - offset);
619
620    *result_length = newsize;
621    return matches_found;
622
623 }
624
625
626 /*
627   Local Variables:
628   tab-width: 3
629   end:
630 */