OSDN Git Service

URI uniqueness (and removing non-uniques)
[pukiwiki/pukiwiki_sandbox.git] / spam / spam.php
1 <?php
2 // $Id: spam.php,v 1.34 2006/11/25 11:40:00 henoheno Exp $
3 // Copyright (C) 2006 PukiWiki Developers Team
4 // License: GPL v2 or (at your option) any later version
5
6 // Functions for Concept-work of spam-uri metrics
7
8 // Return an array of URIs in the $string
9 // [OK] http://nasty.example.org#nasty_string
10 // [OK] http://nasty.example.org:80/foo/xxx#nasty_string/bar
11 // [OK] ftp://nasty.example.org:80/dfsdfs
12 // [OK] ftp://cnn.example.com&story=breaking_news@10.0.0.1/top_story.htm (from RFC3986)
13 function uri_pickup($string = '', $normalize = TRUE,
14         $preserve_rawuri = FALSE, $preserve_chunk = TRUE)
15 {
16         // Not available for: IDN(ignored)
17         $array = array();
18         preg_match_all(
19                 // scheme://userinfo@host:port/path/or/pathinfo/maybefile.and?query=string#fragment
20                 // Refer RFC3986 (Regex below is not strict)
21                 '#(\b[a-z][a-z0-9.+-]{1,8})://' .       // 1: Scheme
22                 '(?:' .
23                         '([^\s<>"\'\[\]/\#?@]*)' .              // 2: Userinfo (Username)
24                 '@)?' .
25                 '(' .
26                         // 3: Host
27                         '\[[0-9a-f:.]+\]' . '|' .                               // IPv6([colon-hex and dot]): RFC2732
28                         '(?:[0-9]{1-3}\.){3}[0-9]{1-3}' . '|' . // IPv4(dot-decimal): 001.22.3.44
29                         '[^\s<>"\'\[\]:/\#?]+' .                                // FQDN: foo.example.org
30                 ')' .
31                 '(?::([0-9]*))?' .                                      // 4: Port
32                 '((?:/+[^\s<>"\'\[\]/\#]+)*/+)?' .      // 5: Directory path or path-info
33                 '([^\s<>"\'\[\]\#]+)?' .                        // 6: File and query string
34                 '(?:\#([a-z0-9._~%!$&\'()*+,;=:@-]*))?' .       // 7: Fragment
35                 '#i',
36                  $string, $array, PREG_SET_ORDER | PREG_OFFSET_CAPTURE
37         );
38         //var_dump(recursive_map('htmlspecialchars', $array));
39
40         // Shrink $array
41         static $parts = array(
42                 1 => 'scheme', 2 => 'userinfo', 3 => 'host', 4 => 'port',
43                 5 => 'path', 6 => 'file', 7 => 'fragment'
44         );
45         $default = array('');
46         foreach(array_keys($array) as $uri) {
47                 array_rename_keys($array[$uri], $parts, TRUE, $default);
48                 $offset = $array[$uri]['scheme'][1]; // Scheme's offset
49
50                 foreach(array_keys($array[$uri]) as $part) {
51                         // Remove offsets for each part
52                         $array[$uri][$part] = & $array[$uri][$part][0];
53                 }
54
55                 if ($normalize) {
56                         $array[$uri]['scheme'] = scheme_normalize($array[$uri]['scheme']);
57                         //if ($array[$uri]['scheme'] === '') {
58                         //      // Ignore
59                         //      unset ($array[$uri]);
60                         //      continue;
61                         //}
62                         
63                         $array[$uri]['host']   = strtolower($array[$uri]['host']);
64                         $array[$uri]['port']   = port_normalize($array[$uri]['port'], $array[$uri]['scheme'], FALSE);
65                         $array[$uri]['path']   = path_normalize($array[$uri]['path']);
66                         $array[$uri]['uri']    = uri_array_implode($array[$uri]);
67                         if ($preserve_rawuri) $array[$uri]['rawuri'] = & $array[$uri][0];
68                 } else {
69                         $array[$uri]['uri'] = & $array[$uri][0]; // Raw
70                 }
71                 unset($array[$uri][0]); // Matched string itself
72                 if (! $preserve_chunk) {
73                         unset(
74                                 $array[$uri]['scheme'],
75                                 $array[$uri]['userinfo'],
76                                 $array[$uri]['host'],
77                                 $array[$uri]['port'],
78                                 $array[$uri]['path'],
79                                 $array[$uri]['file'],
80                                 $array[$uri]['fragment']
81                         );
82                 }
83
84                 $array[$uri]['offset'] = $offset;
85                 $array[$uri]['area']   = 0;
86         }
87
88         return $array;
89 }
90
91 // Domain exposure callback (See spam_uri_pickup_preprocess())
92 // http://victim.example.org/?foo+site:nasty.example.com+bar
93 // => http://nasty.example.com/?refer=victim.example.org
94 // NOTE: 'refer=' is not so good for (at this time).
95 // Consider about using IP address of the victim, try to avoid that.
96 function _preg_replace_callback_domain_exposure($matches = array())
97 {
98         $result = '';
99
100         // Preserve the victim URI as a complicity or ...
101         if (isset($matches[5])) {
102                 $result =
103                         $matches[1] . '://' .   // scheme
104                         $matches[2] . '/' .             // victim.example.org
105                         $matches[3];                    // The rest of all (before victim)
106         }
107
108         // Flipped URI
109         $result = 
110                 $matches[1] . '://' .   // scheme
111                 $matches[4] .                   // nasty.example.com
112                 '/?refer=' . strtolower($matches[2]) .  // victim.example.org
113                 ' ' . $result;
114
115         return $result;
116 }
117
118 // Preprocess: rawurldecode() and adding space(s) to detect/count some URIs _if possible_
119 // NOTE: It's maybe danger to var_dump(result). [e.g. 'javascript:']
120 // [OK] http://victim.example.org/go?http%3A%2F%2Fnasty.example.org
121 // [OK] http://victim.example.org/http://nasty.example.org
122 function spam_uri_pickup_preprocess($string = '')
123 {
124         if (! is_string($string)) return '';
125
126         $string = rawurldecode($string);
127
128         // Domain exposure (See _preg_replace_callback_domain_exposure())
129         $string = preg_replace_callback(
130                 array(
131                         // Something Google: http://www.google.com/supported_domains
132                         '#(http)://([a-z0-9.]+\.google\.[a-z]{2,3}(?:\.[a-z]{2})?)/' .
133                         '([a-z0-9?=&.%_+-]+)' .         // ?query=foo+
134                         '\bsite:([a-z0-9.%_-]+)' .      // site:nasty.example.com
135                         '()' .  // Preserve?
136                         '#i',
137                 ),
138                 '_preg_replace_callback_domain_exposure',
139                 $string
140         );
141
142         // URI exposure (uriuri => uri uri)
143         $string = preg_replace(
144                 array(
145                         '#(?<! )(?:https?|ftp):/#',
146                 //      '#[a-z][a-z0-9.+-]{1,8}://#i',
147                 //      '#[a-z][a-z0-9.+-]{1,8}://#i'
148                 ),
149                 ' $0',
150                 $string
151         );
152
153         return $string;
154 }
155
156 // TODO: Area selection (Check BBCode only, check anchor only, check ...)
157 // Main function of spam-uri pickup
158 function spam_uri_pickup($string = '')
159 {
160         $string = spam_uri_pickup_preprocess($string);
161
162         $array  = uri_pickup($string);
163
164         // Area elevation for '(especially external)link' intension
165         if (! empty($array)) {
166                 // Anchor tags by preg_match_all()
167                 // [OK] <a href="http://nasty.example.com">visit http://nasty.example.com/</a>
168                 // [OK] <a href=\'http://nasty.example.com/\' >discount foobar</a> 
169                 // [NG] <a href="http://ng.example.com">visit http://ng.example.com _not_ended_
170                 // [NG] <a href=  >Good site!</a> <a href= "#" >test</a>
171                 $areas = array();
172                 preg_match_all('#<a\b[^>]*href[^>]*>.*?</a\b[^>]*(>)#i',
173                          $string, $areas, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
174                 //var_dump(recursive_map('htmlspecialchars', $areas));
175                 foreach(array_keys($areas) as $area) {
176                         $areas[$area] =  array(
177                                 $areas[$area][0][1], // Area start (<a href>)
178                                 $areas[$area][1][1], // Area end   (</a>)
179                         );
180                 }
181                 area_measure($areas, $array);
182
183                 // phpBB's "BBCode" by preg_match_all()
184                 // [url]http://nasty.example.com/[/url]
185                 // [link]http://nasty.example.com/[/link]
186                 // [url=http://nasty.example.com]visit http://nasty.example.com/[/url]
187                 // [link http://nasty.example.com/]buy something[/link]
188                 // ?? [url=][/url]
189                 $areas = array();
190                 preg_match_all('#\[(url|link)\b[^\]]*\].*?\[/\1\b[^\]]*(\])#i',
191                          $string, $areas, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
192                 //var_dump(recursive_map('htmlspecialchars', $areas));
193                 foreach(array_keys($areas) as $area) {
194                         $areas[$area] = array(
195                                 $areas[$area][0][1], // Area start ([url])
196                                 $areas[$area][2][1], // Area end   ([/url])
197                         );
198                 }
199                 area_measure($areas, $array);
200
201                 // Various Wiki syntax
202                 // [text_or_uri>text_or_uri]
203                 // [text_or_uri:text_or_uri]
204                 // [text_or_uri|text_or_uri]
205                 // [text_or_uri->text_or_uri]
206                 // [text_or_uri text_or_uri] // MediaWiki
207                 // MediaWiki: [http://nasty.example.com/ visit http://nasty.example.com/]
208
209                 // Remove 'offset's for area_measure()
210                 //foreach(array_keys($array) as $key)
211                 //      unset($array[$key]['offset']);
212         }
213
214         return $array;
215 }
216
217 // $array['something'] => $array['wanted']
218 function array_rename_keys(& $array, $keys = array('from' => 'to'), $force = FALSE, $default = '')
219 {
220         if (! is_array($array) || ! is_array($keys))
221                 return FALSE;
222
223         // Nondestructive test
224         if (! $force)
225                 foreach(array_keys($keys) as $from)
226                         if (! isset($array[$from]))
227                                 return FALSE;
228
229         foreach($keys as $from => $to) {
230                 if ($from === $to) continue;
231                 if (! $force || isset($array[$from])) {
232                         $array[$to] = & $array[$from];
233                         unset($array[$from]);
234                 } else  {
235                         $array[$to] = $default;
236                 }
237         }
238
239         return TRUE;
240 }
241
242 // If in doubt, it's a little doubtful
243 function area_measure($areas, & $array, $belief = -1, $a_key = 'area', $o_key = 'offset')
244 {
245         if (! is_array($areas) || ! is_array($array)) return;
246
247         $areas_keys = array_keys($areas);
248         foreach(array_keys($array) as $u_index) {
249                 $offset = isset($array[$u_index][$o_key]) ?
250                         intval($array[$u_index][$o_key]) : 0;
251                 foreach($areas_keys as $a_index) {
252                         if (isset($array[$u_index][$a_key])) {
253                                 $offset_s = intval($areas[$a_index][0]);
254                                 $offset_e = intval($areas[$a_index][1]);
255                                 // [Area => inside <= Area]
256                                 if ($offset_s < $offset && $offset < $offset_e) {
257                                         $array[$u_index][$a_key] += $belief;
258                                 }
259                         }
260                 }
261         }
262 }
263
264
265 // ---------------------
266 // Part Two
267
268 // Scheme normalization: Renaming the schemes
269 // snntp://example.org =>  nntps://example.org
270 // NOTE: Keep the static lists simple. See also port_normalize().
271 function scheme_normalize($scheme = '', $considerd_harmfull = TRUE)
272 {
273         // Abbreviations considerable they don't have link intension
274         static $abbrevs = array(
275                 'ttp'   => 'http',
276                 'ttps'  => 'https',
277         );
278
279         // Alias => normalized
280         static $aliases = array(
281                 'pop'   => 'pop3',
282                 'news'  => 'nntp',
283                 'imap4' => 'imap',
284                 'snntp' => 'nntps',
285                 'snews' => 'nntps',
286                 'spop3' => 'pop3s',
287                 'pops'  => 'pop3s',
288         );
289
290         $scheme = strtolower(trim($scheme));
291         if (isset($abbrevs[$scheme])) {
292                 if ($considerd_harmfull) {
293                         $scheme = $abbrevs[$scheme];
294                 } else {
295                         $scheme = '';
296                 }
297         }
298         if (isset($aliases[$scheme])) $scheme = $aliases[$scheme];
299
300         return $scheme;
301 }
302
303 // Port normalization: Suppress the (redundant) default port
304 // HTTP://example.org:80/ => http://example.org/
305 // HTTP://example.org:8080/ => http://example.org:8080/
306 // HTTPS://example.org:443/ => https://example.org/
307 function port_normalize($port, $scheme, $scheme_normalize = TRUE)
308 {
309         // Schemes that users _maybe_ want to add protocol-handlers
310         // to their web browsers. (and attackers _maybe_ want to use ...)
311         // Reference: http://www.iana.org/assignments/port-numbers
312         static $array = array(
313                 // scheme => default port
314                 'ftp'     =>    21,
315                 'ssh'     =>    22,
316                 'telnet'  =>    23,
317                 'smtp'    =>    25,
318                 'tftp'    =>    69,
319                 'gopher'  =>    70,
320                 'finger'  =>    79,
321                 'http'    =>    80,
322                 'pop3'    =>   110,
323                 'sftp'    =>   115,
324                 'nntp'    =>   119,
325                 'imap'    =>   143,
326                 'irc'     =>   194,
327                 'wais'    =>   210,
328                 'https'   =>   443,
329                 'nntps'   =>   563,
330                 'rsync'   =>   873,
331                 'ftps'    =>   990,
332                 'telnets' =>   992,
333                 'imaps'   =>   993,
334                 'ircs'    =>   994,
335                 'pop3s'   =>   995,
336                 'mysql'   =>  3306,
337         );
338
339         $port = trim($port);
340         if ($port === '') return $port;
341
342         if ($scheme_normalize) $scheme = scheme_normalize($scheme);
343         if (isset($array[$scheme]) && $port == $array[$scheme])
344                 $port = ''; // Ignore the defaults
345
346         return $port;
347 }
348
349 // Path normalization
350 // http://example.org => http://example.org/
351 // http://example.org#hoge => http://example.org/#hoge
352 // http://example.org/path/a/b/./c////./d => http://example.org/path/a/b/c/d
353 // http://example.org/path/../../a/../back => http://example.org/back
354 function path_normalize($path = '', $divider = '/', $addroot = TRUE)
355 {
356         if (! is_string($path) || $path == '') {
357                 $path = $addroot ? $divider : '';
358         } else {
359                 $path = trim($path);
360                 $last = ($path[strlen($path) - 1] == $divider) ? $divider : '';
361                 $array = explode($divider, $path);
362
363                 // Remove paddings
364                 foreach(array_keys($array) as $key) {
365                         if ($array[$key] == '' || $array[$key] == '.')
366                                  unset($array[$key]);
367                 }
368                 // Back-track
369                 $tmp = array();
370                 foreach($array as $value) {
371                         if ($value == '..') {
372                                 array_pop($tmp);
373                         } else {
374                                 array_push($tmp, $value);
375                         }
376                 }
377                 $array = & $tmp;
378
379                 $path = $addroot ? $divider : '';
380                 if (! empty($array)) $path .= implode($divider, $array) . $last;
381         }
382
383         return $path;
384 }
385
386 // An URI array => An URI (See uri_pickup())
387 function uri_array_implode($uri = array())
388 {
389         if (empty($uri) || ! is_array($uri)) return NULL;
390         
391         $tmp = array();
392         if (isset($uri['scheme']) && $uri['scheme'] !== '') {
393                 $tmp[] = & $uri['scheme'];
394                 $tmp[] = '://';
395         }
396         if (isset($uri['userinfo']) && $uri['userinfo'] !== '') {
397                 $tmp[] = & $uri['userinfo'];
398                 $tmp[] = '@';
399         }
400         if (isset($uri['host']) && $uri['host'] !== '') {
401                 $tmp[] = & $uri['host'];
402         }
403         if (isset($uri['port']) && $uri['port'] !== '') {
404                 $tmp[] = ':';
405                 $tmp[] = & $uri['port'];
406         }
407         if (isset($uri['path']) && $uri['path'] !== '') {
408                 $tmp[] = & $uri['path'];
409         }
410         if (isset($uri['file']) && $uri['file'] !== '') {
411                 $tmp[] = & $uri['file'];
412         }
413         if (isset($uri['fragment']) && $uri['fragment'] !== '') {
414                 $tmp[] = '#';
415                 $tmp[] = & $uri['fragment'];
416         }
417
418         return implode('', $tmp);
419 }
420
421 // ---------------------
422 // Part One : Checker
423
424 function generate_glob_regex($string = '', $divider = '/')
425 {
426         static $from = array(
427                         0 => '*',
428                         1 => '?',
429                         2 => '\[',
430                         3 => '\]',
431                         4 => '[',
432                         5 => ']',
433                 );
434         static $mid = array(
435                         0 => '_AST_',
436                         1 => '_QUE_',
437                         2 => '_eRBR_',
438                         3 => '_eLBR_',
439                         4 => '_RBR_',
440                         5 => '_LBR_',
441                 );
442         static $to = array(
443                         0 => '.*',
444                         1 => '.',
445                         2 => '\[',
446                         3 => '\]',
447                         4 => '[',
448                         5 => ']',
449                 );
450
451         $string = str_replace($from, $mid, $string); // Hide
452         $string = preg_quote($string, $divider);
453         $string = str_replace($mid, $to, $string);   // Unhide
454
455         return $string;
456 }
457
458 // TODO: Ignore list
459 // TODO: require_or_include_once(another file)
460 function is_badhost($hosts = '')
461 {
462         static $blocklist_regex;
463
464         if (! isset($blocklist_regex)) {
465                 $blocklist_regex = array();
466                 $blocklist = array(
467                         // Deny all uri
468                         //'*',
469
470                         // IP address or ...
471                         //'10.20.*.*',  // 10.20.example.com also matches
472                         //'\[1\]',
473                         
474                         // Too much malicious sub-domains
475                         '*.blogspot.com',
476
477                         // 2006-11 dev
478                         'wwwtahoo.com',
479
480                         // 2006-11 dev
481                         '*.infogami.com',
482
483                         // 2006/11/19 17:50 dev
484                         '*.google0site.org',
485                         '*.bigpricesearch.org',
486                         '*.osfind.org',
487                         '*.bablomira.biz',
488                 );
489                 foreach ($blocklist as $part) {
490                         $blocklist_regex[] = '#^' . generate_glob_regex($part, '#') . '$#i';
491                 }
492         }
493
494         if (! is_array($hosts)) $hosts = array($hosts);
495         foreach($hosts as $host) {
496                 if (is_string($host)) $host = '';
497                 foreach ($blocklist_regex as $regex) {
498                         if (preg_match($regex, $host)) {
499                                 return TRUE;
500                         }
501                 }
502         }
503
504         return FALSE;
505 }
506
507 // TODO return TRUE or FALSE!
508 // Simple/fast spam check
509 function is_uri_spam($target = '')
510 {
511         $is_spam   = FALSE;
512         $urinum    = 0;
513         $non_uniq  = 0;
514
515         if (is_array($target)) {
516                 foreach($target as $str) {
517                         // Recurse
518                         list($is_spam, $_urinum) = is_uri_spam($str);
519                         $urinum += $_urinum;
520                         if ($is_spam) break;
521                 }
522         } else {
523                 $pickups = spam_uri_pickup($target);
524                 $urinum += count($pickups);
525
526                 if (! empty($pickups)) {
527                 
528                         // URI quantity
529                         if (! $is_spam && $urinum > 8) {
530                                 $is_spam = TRUE;
531                         }
532
533                         // Using invalid sytax
534                         if (! $is_spam) {
535                                 foreach($pickups as $pickup) {
536                                         if ($pickup['area'] < 0) {
537                                                 $is_spam = TRUE;
538                                                 break;
539                                         }
540                                 }
541                         }
542
543                         // URI uniqueness (and removing non-uniques)
544                         if (! $is_spam) {
545                                 $uris = array();
546                                 foreach ($pickups as $key => $pickup) {
547                                         $uris[$key] = & $pickup['uri'];
548                                 }
549                                 $count = count($uris);
550                                 $uris = array_unique($uris);
551                                 $non_uniq += $count - count($uris);
552                                 if ($non_uniq > 3) {  // Allow N times dupe
553                                         $is_spam = TRUE;
554                                 } else {
555                                         foreach (array_diff(array_keys($pickups),
556                                                 array_keys($uris)) as $remove) {
557                                                 unset($pickups[$remove]);
558                                         }
559                                 }
560                                 //var_dump($is_spam, $uris, $pickups, "----");
561                         }
562
563                         // Bad host
564                         if (! $is_spam) {
565                                 $hosts = array();
566                                 foreach ($pickups as $pickup) {
567                                         $hosts[] = & $pickup['host'];
568                                 }
569                                 $is_spam = is_badhost(array_unique($hosts));
570                         }
571                 }
572         }
573
574         return array($is_spam, $urinum);
575 }
576
577 // ---------------------
578
579 // Check User-Agent (not testing yet)
580 function is_invalid_useragent($ua_name = '' /*, $ua_vars = ''*/ )
581 {
582         return $ua_name === '';
583 }
584
585 // ---------------------
586
587 // TODO: Separate check-part(s) and mail part
588 // TODO: Multi-metrics (uri, host, user-agent, ...)
589 // TODO: Mail to administrator with more measurement data?
590 // Simple/fast spam filter ($target: 'a string' or an array())
591 function pkwk_spamfilter($action, $page, $target = array('title' => ''))
592 {
593         $is_spam = FALSE;
594
595         //$is_spam =  is_invalid_useragent('NOTYET');
596         if ($is_spam) {
597                 $action .= ' (Invalid User-Agent)';
598         } else {
599                 list($is_spam) = is_uri_spam($target);
600         }
601
602         if ($is_spam) {
603                 // Mail to administrator(s)
604                 global $notify, $notify_subject;
605                 if ($notify) {
606                         $footer['ACTION'] = $action;
607                         $footer['PAGE']   = '[blocked] ' . $page;
608                         $footer['URI']    = get_script_uri() . '?' . rawurlencode($page);
609                         $footer['USER_AGENT']  = TRUE;
610                         $footer['REMOTE_ADDR'] = TRUE;
611                         pkwk_mail_notify($notify_subject,  var_export($target, TRUE), $footer);
612                         unset($footer);
613                 }
614         }
615
616         if ($is_spam) spam_exit();
617 }
618
619 // ---------------------
620
621 // Common bahavior for blocking
622 // NOTE: Call this function from various blocking feature, to disgueise the reason 'why blocked'
623 function spam_exit()
624 {
625         die("\n");
626 }
627
628 ?>