]> sjero.net Git - wget/blob - src/res.c
[svn] Minor fixes prompted by `lint'.
[wget] / src / res.c
1 /* Support for Robot Exclusion Standard (RES).
2    Copyright (C) 2001 Free Software Foundation, Inc.
3
4 This file is part of Wget.
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or (at
9 your option) any later version.
10
11 This program is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
19
20 /* This file implements the Robot Exclusion Standard (RES).
21
22    RES is a simple protocol that enables site admins to signalize to
23    the web crawlers that certain parts of the site should not be
24    accessed.  All the admin needs to do is create a "robots.txt" file
25    in the web server root, and use simple commands to allow or
26    disallow access to certain parts of the site.
27
28    The first specification was written by Martijn Koster in 1994, and
29    is still available at <http://www.robotstxt.org/wc/norobots.html>.
30    In 1996, Martijn wrote an Internet Draft specifying an improved RES
31    specification; however, that work was apparently abandoned since
32    the draft has expired in 1997 and hasn't been replaced since.  The
33    draft is available at
34    <http://www.robotstxt.org/wc/norobots-rfc.html>.
35
36    This file implements RES as specified by the draft.  Note that this
37    only handles the "robots.txt" support.  The META tag that controls
38    whether the links should be followed is handled in `html-url.c'.
39
40    Known deviations:
41
42    * The end-of-line comment recognition is more in the spirit of the
43      Bourne Shell (as specified by RES-1994).  That means that
44      "foo#bar" is taken literally, whereas "foo #bar" is interpreted
45      as "foo".  The Draft apparently specifies that both should be
46      interpreted as "foo".
47
48    * We don't recognize sole CR as the line ending.
49
50    * We don't implement expiry mechanism for /robots.txt specs.  I
51      consider it non-necessary for a relatively short-lived
52      application such as Wget.  Besides, it is highly questionable
53      whether anyone deploys the recommended expiry scheme for
54      robots.txt.
55
56    Entry points are functions res_parse, res_parse_from_file,
57    res_match_path, res_register_specs, res_get_specs, and
58    res_retrieve_file.  */
59
60 #ifdef HAVE_CONFIG_H
61 # include <config.h>
62 #endif
63
64 #include <stdio.h>
65 #include <stdlib.h>
66 #ifdef HAVE_STRING_H
67 # include <string.h>
68 #else
69 # include <strings.h>
70 #endif /* HAVE_STRING_H */
71 #include <errno.h>
72 #include <assert.h>
73
74 #include "wget.h"
75 #include "utils.h"
76 #include "hash.h"
77 #include "url.h"
78 #include "retr.h"
79 #include "res.h"
80
81 struct path_info {
82   char *path;
83   int allowedp;
84   int user_agent_exact_p;
85 };
86
87 struct robot_specs {
88   int count;
89   int size;
90   struct path_info *paths;
91 };
92 \f
93 /* Parsing the robot spec. */
94
95 /* Check whether AGENT (a string of length LENGTH) equals "wget" or
96    "*".  If it is either of them, *matches is set to one.  If it is
97    "wget", *exact_match is set to one.  */
98
99 static void
100 match_user_agent (const char *agent, int length,
101                   int *matches, int *exact_match)
102 {
103   if (length == 1 && *agent == '*')
104     {
105       *matches = 1;
106       *exact_match = 0;
107     }
108   else if (BOUNDED_EQUAL_NO_CASE (agent, agent + length, "wget"))
109     {
110       *matches = 1;
111       *exact_match = 1;
112     }
113   else
114     {
115       *matches = 0;
116       *exact_match = 0;
117     }
118 }
119
120 /* Add a path specification between PATH_B and PATH_E as one of the
121    paths in SPECS.  */
122
123 static void
124 add_path (struct robot_specs *specs, const char *path_b, const char *path_e,
125           int allowedp, int exactp)
126 {
127   struct path_info pp;
128   if (path_b < path_e && *path_b == '/')
129     /* Our path representation doesn't use a leading slash, so remove
130        one from theirs. */
131     ++path_b;
132   pp.path     = strdupdelim (path_b, path_e);
133   pp.allowedp = allowedp;
134   pp.user_agent_exact_p = exactp;
135   ++specs->count;
136   if (specs->count > specs->size)
137     {
138       if (specs->size == 0)
139         specs->size = 1;
140       else
141         specs->size <<= 1;
142       specs->paths = xrealloc (specs->paths,
143                                specs->size * sizeof (struct path_info));
144     }
145   specs->paths[specs->count - 1] = pp;
146 }
147
148 /* Recreate SPECS->paths with only those paths that have non-zero
149    user_agent_exact_p.  */
150
151 static void
152 prune_non_exact (struct robot_specs *specs)
153 {
154   struct path_info *newpaths;
155   int i, j, cnt;
156   cnt = 0;
157   for (i = 0; i < specs->count; i++)
158     if (specs->paths[i].user_agent_exact_p)
159       ++cnt;
160   newpaths = xmalloc (cnt * sizeof (struct path_info));
161   for (i = 0, j = 0; i < specs->count; i++)
162     if (specs->paths[i].user_agent_exact_p)
163       newpaths[j++] = specs->paths[i];
164   assert (j == cnt);
165   xfree (specs->paths);
166   specs->paths = newpaths;
167   specs->count = cnt;
168   specs->size  = cnt;
169 }
170
171 #define EOL(p) ((p) >= lineend)
172
173 #define SKIP_SPACE(p) do {              \
174   while (!EOL (p) && ISSPACE (*p))      \
175     ++p;                                \
176 } while (0)
177
178 #define FIELD_IS(string_literal)        \
179   BOUNDED_EQUAL_NO_CASE (field_b, field_e, string_literal)
180
181 /* Parse textual RES specs beginning with SOURCE of length LENGTH.
182    Return a specs objects ready to be fed to res_match_path.
183
184    The parsing itself is trivial, but creating a correct SPECS object
185    is trickier than it seems, because RES is surprisingly byzantine if
186    you attempt to implement it correctly.
187
188    A "record" is a block of one or more `User-Agent' lines followed by
189    one or more `Allow' or `Disallow' lines.  Record is accepted by
190    Wget if one of the `User-Agent' lines was "wget", or if the user
191    agent line was "*".
192
193    After all the lines have been read, we examine whether an exact
194    ("wget") user-agent field was specified.  If so, we delete all the
195    lines read under "User-Agent: *" blocks because we have our own
196    Wget-specific blocks.  This enables the admin to say:
197
198        User-Agent: *
199        Disallow: /
200
201        User-Agent: google
202        User-Agent: wget
203        Disallow: /cgi-bin
204
205    This means that to Wget and to Google, /cgi-bin is disallowed,
206    whereas for all other crawlers, everything is disallowed.
207    res_parse is implemented so that the order of records doesn't
208    matter.  In the case above, the "User-Agent: *" could have come
209    after the other one.  */
210
211 struct robot_specs *
212 res_parse (const char *source, int length)
213 {
214   int line_count = 1;
215
216   const char *p   = source;
217   const char *end = source + length;
218
219   /* non-zero if last applicable user-agent field matches Wget. */
220   int user_agent_applies = 0;
221
222   /* non-zero if last applicable user-agent field *exactly* matches
223      Wget.  */
224   int user_agent_exact = 0;
225
226   /* whether we ever encountered exact user agent. */
227   int found_exact = 0;
228
229   /* count of allow/disallow lines in the current "record", i.e. after
230      the last `user-agent' instructions.  */
231   int record_count = 0;
232
233   struct robot_specs *specs = xmalloc (sizeof (struct robot_specs));
234   memset (specs, '\0', sizeof (struct robot_specs));
235
236   while (1)
237     {
238       const char *lineend, *lineend_real;
239       const char *field_b, *field_e;
240       const char *value_b, *value_e;
241
242       if (p == end)
243         break;
244       lineend_real = memchr (p, '\n', end - p);
245       if (lineend_real)
246         ++lineend_real;
247       else
248         lineend_real = end;
249       lineend = lineend_real;
250
251       /* Before doing anything else, check whether the line is empty
252          or comment-only. */
253       SKIP_SPACE (p);
254       if (EOL (p) || *p == '#')
255         goto next;
256
257       /* Make sure the end-of-line comments are respected by setting
258          lineend to a location preceding the first comment.  Real line
259          ending remains in lineend_real.  */
260       for (lineend = p; lineend < lineend_real; lineend++)
261         if ((lineend == p || ISSPACE (*(lineend - 1)))
262             && *lineend == '#')
263           break;
264
265       /* Ignore trailing whitespace in the same way. */
266       while (lineend > p && ISSPACE (*(lineend - 1)))
267         --lineend;
268
269       assert (!EOL (p));
270
271       field_b = p;
272       while (!EOL (p) && (ISALNUM (*p) || *p == '-'))
273         ++p;
274       field_e = p;
275
276       SKIP_SPACE (p);
277       if (field_b == field_e || EOL (p) || *p != ':')
278         {
279           DEBUGP (("Ignoring malformed line %d", line_count));
280           goto next;
281         }
282       ++p;                      /* skip ':' */
283       SKIP_SPACE (p);
284
285       value_b = p;
286       while (!EOL (p))
287         ++p;
288       value_e = p;
289
290       /* Finally, we have a syntactically valid line. */
291       if (FIELD_IS ("user-agent"))
292         {
293           /* We have to support several cases:
294
295              --previous records--
296
297              User-Agent: foo
298              User-Agent: Wget
299              User-Agent: bar
300              ... matching record ...
301
302              User-Agent: baz
303              User-Agent: qux
304              ... non-matching record ...
305
306              User-Agent: *
307              ... matching record, but will be pruned later ...
308
309              We have to respect `User-Agent' at the beginning of each
310              new record simply because we don't know if we're going to
311              encounter "Wget" among the agents or not.  Hence,
312              match_user_agent is called when record_count != 0.
313
314              But if record_count is 0, we have to keep calling it
315              until it matches, and if that happens, we must not call
316              it any more, until the next record.  Hence the other part
317              of the condition.  */
318           if (record_count != 0 || user_agent_applies == 0)
319             match_user_agent (value_b, value_e - value_b,
320                               &user_agent_applies, &user_agent_exact);
321           if (user_agent_exact)
322             found_exact = 1;
323           record_count = 0;
324         }
325       else if (FIELD_IS ("allow"))
326         {
327           if (user_agent_applies)
328             {
329               add_path (specs, value_b, value_e, 1, user_agent_exact);
330             }
331           ++record_count;
332         }
333       else if (FIELD_IS ("disallow"))
334         {
335           if (user_agent_applies)
336             {
337               int allowed = 0;
338               if (value_b == value_e)
339                 /* Empty "disallow" line means everything is
340                    *allowed*!  */
341                 allowed = 1;
342               add_path (specs, value_b, value_e, allowed, user_agent_exact);
343             }
344           ++record_count;
345         }
346       else
347         {
348           DEBUGP (("Ignoring unknown field at line %d", line_count));
349           goto next;
350         }
351
352     next:
353       p = lineend_real;
354       ++line_count;
355     }
356
357   if (found_exact)
358     {
359       /* We've encountered an exactly matching user-agent.  Throw out
360          all the stuff with user-agent: *.  */
361       prune_non_exact (specs);
362     }
363   else if (specs->size > specs->count)
364     {
365       /* add_path normally over-allocates specs->paths.  Reallocate it
366          to the correct size in order to conserve some memory.  */
367       specs->paths = xrealloc (specs->paths,
368                                specs->count * sizeof (struct path_info));
369       specs->size = specs->count;
370     }
371
372   return specs;
373 }
374
375 /* The same like res_parse, but first map the FILENAME into memory,
376    and then parse it.  */
377
378 struct robot_specs *
379 res_parse_from_file (const char *filename)
380 {
381   struct robot_specs *specs;
382   struct file_memory *fm = read_file (filename);
383   if (!fm)
384     {
385       logprintf (LOG_NOTQUIET, "Cannot open %s: %s",
386                  filename, strerror (errno));
387       return NULL;
388     }
389   specs = res_parse (fm->content, fm->length);
390   read_file_free (fm);
391   return specs;
392 }
393
394 static void
395 free_specs (struct robot_specs *specs)
396 {
397   int i;
398   for (i = 0; i < specs->count; i++)
399     xfree (specs->paths[i].path);
400   FREE_MAYBE (specs->paths);
401   xfree (specs);
402 }
403 \f
404 /* Matching of a path according to the specs. */
405
406 /* If C is '%' and (ptr[1], ptr[2]) form a hexadecimal number, and if
407    that number is not a numerical representation of '/', decode C and
408    advance the pointer.  */
409
410 #define DECODE_MAYBE(c, ptr) do {                                       \
411   if (c == '%' && ISXDIGIT (ptr[1]) && ISXDIGIT (ptr[2]))               \
412     {                                                                   \
413       char decoded                                                      \
414         = (XCHAR_TO_XDIGIT (ptr[1]) << 4) + XCHAR_TO_XDIGIT (ptr[2]);   \
415       if (decoded != '/')                                               \
416         {                                                               \
417           c = decoded;                                                  \
418           ptr += 2;                                                     \
419         }                                                               \
420     }                                                                   \
421 } while (0)
422
423 /* The inner matching engine: return non-zero if RECORD_PATH matches
424    URL_PATH.  The rules for matching are described at
425    <http://info.webcrawler.com/mak/projects/robots/norobots-rfc.html>,
426    section 3.2.2.  */
427
428 static int
429 matches (const char *record_path, const char *url_path)
430 {
431   const char *rp = record_path;
432   const char *up = url_path;
433
434   for (; ; ++rp, ++up)
435     {
436       char rc = *rp;
437       char uc = *up;
438       if (!rc)
439         return 1;
440       if (!uc)
441         return 0;
442       DECODE_MAYBE(rc, rp);
443       DECODE_MAYBE(uc, up);
444       if (rc != uc)
445         return 0;
446     }
447 }
448
449 /* Iterate through all paths in SPECS.  For the first one that
450    matches, return its allow/reject status.  If none matches,
451    retrieval is by default allowed.  */
452
453 int
454 res_match_path (const struct robot_specs *specs, const char *path)
455 {
456   int i;
457   if (!specs)
458     return 1;
459   for (i = 0; i < specs->count; i++)
460     if (matches (specs->paths[i].path, path))
461       {
462         int allowedp = specs->paths[i].allowedp;
463         DEBUGP (("%s path %s because of rule `%s'.\n",
464                  allowedp ? "Allowing" : "Rejecting",
465                  path, specs->paths[i].path));
466         return allowedp;
467       }
468   return 1;
469 }
470 \f
471 /* Registering the specs. */
472
473 static struct hash_table *registered_specs;
474
475 /* Stolen from cookies.c. */
476 #define SET_HOSTPORT(host, port, result) do {           \
477   int HP_len = strlen (host);                           \
478   result = alloca (HP_len + 1 + numdigit (port) + 1);   \
479   memcpy (result, host, HP_len);                        \
480   result[HP_len] = ':';                                 \
481   long_to_string (result + HP_len + 1, port);           \
482 } while (0)
483
484 /* Register RES specs that below to server on HOST:PORT.  They will
485    later be retrievable using res_get_specs.  */
486
487 void
488 res_register_specs (const char *host, int port, struct robot_specs *specs)
489 {
490   struct robot_specs *old;
491   char *hp, *hp_old;
492   SET_HOSTPORT (host, port, hp);
493
494   if (!registered_specs)
495     registered_specs = make_nocase_string_hash_table (0);
496
497   /* Required to shut up the compiler. */
498   old    = NULL;
499   hp_old = NULL;
500
501   if (hash_table_get_pair (registered_specs, hp, hp_old, old))
502     {
503       if (old)
504         free_specs (old);
505       hash_table_put (registered_specs, hp_old, specs);
506     }
507   else
508     {
509       hash_table_put (registered_specs, xstrdup (hp), specs);
510     }
511 }
512
513 /* Get the specs that belong to HOST:PORT. */
514
515 struct robot_specs *
516 res_get_specs (const char *host, int port)
517 {
518   char *hp;
519   SET_HOSTPORT (host, port, hp);
520   if (!registered_specs)
521     return NULL;
522   return hash_table_get (registered_specs, hp);
523 }
524 \f
525 /* Loading the robots file.  */
526
527 #define RES_SPECS_LOCATION "/robots.txt"
528
529 /* Retrieve the robots.txt from the server root of the server that
530    serves URL.  The file will be named according to the currently
531    active rules, and the file name will be returned in *file.
532
533    Return non-zero if robots were retrieved OK, zero otherwise.  */
534
535 int
536 res_retrieve_file (const char *url, char **file)
537 {
538   uerr_t err;
539   char *robots_url = uri_merge (url, RES_SPECS_LOCATION);
540
541   logputs (LOG_VERBOSE, _("Loading robots.txt; please ignore errors.\n"));
542   *file = NULL;
543   err = retrieve_url (robots_url, file, NULL, NULL, NULL);
544   xfree (robots_url);
545
546   if (err != RETROK && *file != NULL)
547     {
548       /* If the file is not retrieved correctly, but retrieve_url
549          allocated the file name, deallocate is here so that the
550          caller doesn't have to worry about it.  */
551       xfree (*file);
552       *file = NULL;
553     }
554   return err == RETROK;
555 }
556 \f
557 static int
558 cleanup_hash_table_mapper (void *key, void *value, void *arg_ignored)
559 {
560   xfree (key);
561   free_specs (value);
562   return 0;
563 }
564
565 void
566 res_cleanup (void)
567 {
568   if (registered_specs)
569     {
570       hash_table_map (registered_specs, cleanup_hash_table_mapper, NULL);
571       hash_table_destroy (registered_specs);
572       registered_specs = NULL;
573     }
574 }