several fixes, mostly in the anti-abuse system
[mir.git] / source / mircoders / global / Abuse.java
1 /*
2  * Copyright (C) 2001, 2002 The Mir-coders group
3  *
4  * This file is part of Mir.
5  *
6  * Mir 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
9  * (at your option) any later version.
10  *
11  * Mir is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Mir; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  *
20  * In addition, as a special exception, The Mir-coders gives permission to link
21  * the code of this program with  any library licensed under the Apache Software License,
22  * The Sun (tm) Java Advanced Imaging library (JAI), The Sun JIMI library
23  * (or with modified versions of the above that use the same license as the above),
24  * and distribute linked combinations including the two.  You must obey the
25  * GNU General Public License in all respects for all of the code used other than
26  * the above mentioned libraries.  If you modify this file, you may extend this
27  * exception to your version of the file, but you are not obligated to do so.
28  * If you do not wish to do so, delete this exception statement from your version.
29  */
30
31 package mircoders.global;
32
33 import gnu.regexp.RE;
34
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.io.FileOutputStream;
38 import java.util.Arrays;
39 import java.util.Date;
40 import java.util.HashMap;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Random;
45 import java.util.Vector;
46
47 import javax.servlet.http.Cookie;
48 import javax.servlet.http.HttpServletRequest;
49 import javax.servlet.http.HttpServletResponse;
50
51 import mir.entity.Entity;
52 import mir.log.LoggerWrapper;
53 import mir.session.HTTPAdapters;
54 import mir.session.Request;
55 import mir.util.DateToMapAdapter;
56 import mir.util.InternetFunctions;
57 import mir.util.StringRoutines;
58 import mircoders.entity.EntityComment;
59 import mircoders.entity.EntityContent;
60 import mircoders.localizer.MirAdminInterfaceLocalizer;
61
62 import org.apache.commons.collections.ExtendedProperties;
63
64
65 public class Abuse {
66   private List filters;
67   private int maxIdentifier;
68   private LoggerWrapper logger;
69   private int logSize;
70   private boolean logEnabled;
71   private boolean openPostingDisabled;
72   private boolean openPostingPassword;
73   private boolean cookieOnBlock;
74   private String articleBlockAction;
75   private String commentBlockAction;
76   private List log;
77   private String configFile = MirGlobal.config().getStringWithHome("Abuse.Config");
78
79
80   private static final String IP_FILTER_TYPE="ip";
81   private static final String REGEXP_FILTER_TYPE="regexp";
82   private static String cookieName=MirGlobal.config().getString("Abuse.CookieName");
83   private static int cookieMaxAge = 60*60*MirGlobal.config().getInt("Abuse.CookieMaxAge");
84
85   public Abuse() {
86     logger = new LoggerWrapper("Global.Abuse");
87     filters = new Vector();
88     maxIdentifier = 0;
89     log = new Vector();
90
91     logSize = 100;
92     logEnabled = false;
93     articleBlockAction = "";
94     commentBlockAction = "";
95     openPostingPassword = false;
96     openPostingDisabled = false;
97     cookieOnBlock = false;
98
99     load();
100   }
101
102   public boolean checkIpFilter(String anIpAddress) {
103     synchronized (filters) {
104       Iterator i = filters.iterator();
105
106       while (i.hasNext()) {
107         Filter filter = (Filter) i.next();
108
109         try {
110           if ( (filter.getType().equals(IP_FILTER_TYPE)) &&
111               InternetFunctions.isIpAddressInNetwork(anIpAddress, filter.getExpression())) {
112             logger.debug("ip match on " + filter.getExpression());
113             return true;
114           }
115         }
116         catch (Throwable t) {
117           logger.warn("error while checking ip address " + anIpAddress + " over network " + filter.expression + ": " + t.getMessage());
118         }
119       }
120
121       return false;
122     }
123   }
124
125   private boolean checkRegExpFilter(Entity anEntity) {
126     synchronized (filters) {
127       Iterator i = filters.iterator();
128
129       while (i.hasNext()) {
130         Filter filter = (Filter) i.next();
131
132         if (filter.getType().equals(REGEXP_FILTER_TYPE)) {
133           try {
134             RE regularExpression = new RE(filter.getExpression());
135
136             Iterator j = anEntity.getFields().iterator();
137             while (j.hasNext()) {
138               String field = anEntity.getValue( (String) j.next());
139
140               if (field != null && regularExpression.isMatch(field.toLowerCase())) {
141                 logger.debug("regexp match on " + filter.getExpression());
142                 return true;
143               }
144             }
145           }
146           catch (Throwable t) {
147             logger.warn("error while checking entity with regexp " + filter.getExpression() + ": " + t.getMessage());
148           }
149         }
150       }
151
152       return false;
153     }
154   }
155
156   private void setCookie(HttpServletResponse aResponse) {
157     Random random = new Random();
158
159     Cookie cookie = new Cookie(cookieName, Integer.toString(random.nextInt(1000000000)));
160     cookie.setMaxAge(cookieMaxAge);
161     cookie.setPath("/");
162
163     if (aResponse!=null)
164       aResponse.addCookie(cookie);
165   }
166
167   private boolean checkCookie(List aCookies) {
168     if (getCookieOnBlock()) {
169       Iterator i = aCookies.iterator();
170
171       while (i.hasNext()) {
172         Cookie cookie = (Cookie) i.next();
173
174         if (cookie.getName().equals(cookieName)) {
175           logger.debug("cookie match");
176           return true;
177         }
178       }
179     }
180
181     return false;
182   }
183
184   public void checkComment(EntityComment aComment, Request aRequest, HttpServletResponse aResponse) {
185     try {
186       long time = System.currentTimeMillis();
187       String address = "0.0.0.0";
188       String browser = "unknown";
189       Cookie[] cookies = {};
190
191       HttpServletRequest request = null;
192
193       if (aRequest instanceof HTTPAdapters.HTTPParsedRequestAdapter) {
194         request = ((HTTPAdapters.HTTPParsedRequestAdapter) aRequest).getRequest();
195       }
196       else if (aRequest instanceof HTTPAdapters.HTTPRequestAdapter) {
197         request = ((HTTPAdapters.HTTPRequestAdapter) aRequest).getRequest();
198       }
199       if (request!=null) {
200         browser = (String) request.getHeader("User-Agent");
201         address = request.getRemoteAddr();
202         cookies = request.getCookies();
203       }
204
205       logComment(address, aComment.getId(), new Date(), browser);
206
207       MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation = MirGlobal.localizer().adminInterface().simpleCommentOperationForName(commentBlockAction);
208
209       if (checkCookie(Arrays.asList(cookies)) || checkIpFilter(address) || checkRegExpFilter(aComment)) {
210         logger.debug("performing operation " + operation.getName());
211         operation.perform(null, MirGlobal.localizer().dataModel().adapterModel().makeEntityAdapter("comment", aComment));
212         setCookie(aResponse);
213       }
214
215       logger.info("checkComment: " + (System.currentTimeMillis()-time) + "ms");
216     }
217     catch (Throwable t) {
218       logger.error("Abuse.checkComment: " + t.toString());
219     }
220   }
221
222   public void checkArticle(EntityContent anArticle, Request aRequest, HttpServletResponse aResponse) {
223     try {
224       long time = System.currentTimeMillis();
225       String address = "0.0.0.0";
226       String browser = "unknown";
227       Cookie[] cookies = {};
228
229       HttpServletRequest request = null;
230
231       if (aRequest instanceof HTTPAdapters.HTTPParsedRequestAdapter) {
232         request = ((HTTPAdapters.HTTPParsedRequestAdapter) aRequest).getRequest();
233       }
234       else if (aRequest instanceof HTTPAdapters.HTTPRequestAdapter) {
235         request = ((HTTPAdapters.HTTPRequestAdapter) aRequest).getRequest();
236       }
237       if (request!=null) {
238         browser = (String) request.getHeader("User-Agent");
239         address = request.getRemoteAddr();
240         cookies = request.getCookies();
241       }
242       else
243         logger.debug("no request available! " + aRequest.getClass().getName());
244
245       logArticle(address, anArticle.getId(), new Date(), browser);
246
247       MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation = MirGlobal.localizer().adminInterface().simpleArticleOperationForName(commentBlockAction);
248
249       if (checkCookie(Arrays.asList(cookies)) || checkIpFilter(address) || checkRegExpFilter(anArticle)) {
250         logger.debug("performing operation " + operation.getName());
251         operation.perform(null, MirGlobal.localizer().dataModel().adapterModel().makeEntityAdapter("content", anArticle));
252         setCookie(aResponse);
253       }
254
255       logger.info("checkArticle: " + (System.currentTimeMillis()-time) + "ms");
256     }
257     catch (Throwable t) {
258       logger.error("Abuse.checkArticle: " + t.toString());
259     }
260   }
261
262   public boolean getLogEnabled() {
263     return logEnabled;
264   }
265
266   public void setLogEnabled(boolean anEnabled) {
267     logEnabled = anEnabled;
268     truncateLog();
269   }
270
271   public int getLogSize() {
272     return logSize;
273   }
274
275   public void setLogSize(int aSize) {
276     logSize = aSize;
277     truncateLog();
278   }
279
280   public boolean getOpenPostingDisabled() {
281     return openPostingDisabled;
282   }
283
284   public void setOpenPostingDisabled(boolean anOpenPostingDisabled) {
285     openPostingDisabled = anOpenPostingDisabled;
286   }
287
288   public boolean getOpenPostingPassword() {
289     return openPostingPassword;
290   }
291
292   public void setOpenPostingPassword(boolean anOpenPostingPassword) {
293     openPostingPassword = anOpenPostingPassword;
294   }
295
296   public boolean getCookieOnBlock() {
297     return cookieOnBlock;
298   }
299
300   public void setCookieOnBlock(boolean aCookieOnBlock) {
301     cookieOnBlock = aCookieOnBlock;
302   }
303
304   public String getArticleBlockAction() {
305     return articleBlockAction;
306   }
307
308   public void setArticleBlockAction(String anAction) {
309     articleBlockAction = anAction;
310   }
311
312   public String getCommentBlockAction() {
313     return commentBlockAction;
314   }
315
316   public void setCommentBlockAction(String anAction) {
317     commentBlockAction = anAction;
318   }
319
320
321   public List getLog() {
322     synchronized(log) {
323       List result = new Vector();
324
325       Iterator i = log.iterator();
326       while (i.hasNext()) {
327         LogEntry logEntry = (LogEntry) i.next();
328         Map entry = new HashMap();
329
330         entry.put("ip", logEntry.getIpNumber());
331         entry.put("id", logEntry.getId());
332         entry.put("timestamp", new DateToMapAdapter(logEntry.getTimeStamp()));
333         if (logEntry.getIsArticle())
334           entry.put("type", "content");
335         else
336           entry.put("type", "comment");
337         entry.put("browser", logEntry.getBrowserString());
338
339         result.add(entry);
340       }
341
342       return result;
343     }
344   }
345
346   public void logComment(String anIp, String anId, Date aTimeStamp, String aBrowser) {
347     appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, false));
348   }
349
350   public void logArticle(String anIp, String anId, Date aTimeStamp, String aBrowser) {
351     appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, true));
352   }
353
354   public void load() {
355     try {
356       ExtendedProperties configuration = new ExtendedProperties();
357
358       try {
359         configuration = new ExtendedProperties(configFile);
360       }
361       catch (FileNotFoundException e) {
362       }
363
364       getFilterConfig(filters, "abuse.filter", configuration);
365
366       setOpenPostingDisabled(configuration.getString("abuse.openPostingDisabled", "0").equals("1"));
367       setOpenPostingPassword(configuration.getString("abuse.openPostingPassword", "0").equals("1"));
368       setCookieOnBlock(configuration.getString("abuse.cookieOnBlock", "0").equals("1"));
369       setLogEnabled(configuration.getString("abuse.logEnabled", "0").equals("1"));
370       setLogSize(configuration.getInt("abuse.logSize", 10));
371       setArticleBlockAction(configuration.getString("abuse.articleBlockAction", ""));
372       setCommentBlockAction(configuration.getString("abuse.commentBlockAction", ""));
373     }
374     catch (Throwable t) {
375       throw new RuntimeException(t.toString());
376     }
377   }
378   public void save() {
379     try {
380       ExtendedProperties configuration = new ExtendedProperties();
381
382       setFilterConfig(filters, "abuse.filter", configuration);
383
384       configuration.addProperty("abuse.openPostingDisabled", getOpenPostingDisabled()?"1":"0");
385       configuration.addProperty("abuse.openPostingPassword", getOpenPostingPassword()?"1":"0");
386       configuration.addProperty("abuse.cookieOnBlock", getCookieOnBlock()?"1":"0");
387       configuration.addProperty("abuse.logEnabled", getLogEnabled()?"1":"0");
388       configuration.addProperty("abuse.logSize", Integer.toString(getLogSize()));
389       configuration.addProperty("abuse.articleBlockAction", getArticleBlockAction());
390       configuration.addProperty("abuse.commentBlockAction", getCommentBlockAction());
391
392       configuration.save(new FileOutputStream(new File(configFile)), "Anti abuse configuration");
393     }
394     catch (Throwable t) {
395       throw new RuntimeException(t.toString());
396     }
397   }
398
399   public List getFilterTypes() {
400     List result = new Vector();
401
402     Map entry = new HashMap();
403     entry.put("resource", "ip");
404     entry.put("id", IP_FILTER_TYPE);
405     result.add(entry);
406
407     entry = new HashMap();
408     entry.put("resource", "regexp");
409     entry.put("id", REGEXP_FILTER_TYPE);
410     result.add(entry);
411
412     return result;
413   }
414
415   public List getArticleActions() {
416     try {
417       List result = new Vector();
418
419       Iterator i = MirGlobal.localizer().adminInterface().simpleArticleOperations().iterator();
420       while (i.hasNext()) {
421         MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
422             (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
423
424         Map action = new HashMap();
425         action.put("resource", operation.getName());
426         action.put("identifier", operation.getName());
427
428         result.add(action);
429       }
430
431       return result;
432     }
433     catch (Throwable t) {
434       throw new RuntimeException("can't get article actions");
435     }
436   }
437
438   public List getCommentActions() {
439     try {
440       List result = new Vector();
441
442       Iterator i = MirGlobal.localizer().adminInterface().simpleCommentOperations().iterator();
443       while (i.hasNext()) {
444         MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
445             (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
446
447         Map action = new HashMap();
448         action.put("resource", operation.getName());
449         action.put("identifier", operation.getName());
450
451         result.add(action);
452       }
453
454       return result;
455     }
456     catch (Throwable t) {
457       throw new RuntimeException("can't get comment actions");
458     }
459   }
460
461   public List getFilters() {
462     return getFiltersAsMaps(filters);
463   }
464
465   public void addFilter(String aType, String anExpression) {
466     addFilter(filters, aType, anExpression);
467   }
468
469   public void setFilter(String anIdentifier, String aType, String anExpression) {
470     setFilter(filters, anIdentifier, aType, anExpression);
471   }
472
473   public void deleteFilter(String anIdentifier) {
474     deleteFilter(filters, anIdentifier);
475   }
476
477   public void validateIpFilter(String anIdentifier, String anArticleAction, String aCommentAction) throws Exception {
478   }
479
480   private List getFiltersAsMaps(List aFilters) {
481     synchronized(aFilters) {
482       List result = new Vector();
483
484       Iterator i = aFilters.iterator();
485       while (i.hasNext()) {
486         Filter filter = (Filter) i.next();
487         Map map = new HashMap();
488
489         map.put("id", filter.getId());
490         map.put("expression", filter.getExpression());
491         map.put("type", filter.getType());
492
493         result.add(map);
494       }
495       return result;
496     }
497   }
498
499   private void addFilter(List aFilters, String aType, String anExpression) {
500     Filter filter = new Filter();
501
502     filter.setId(generateId());
503     filter.setExpression(anExpression);
504     filter.setType(aType);
505
506     synchronized (aFilters) {
507       aFilters.add(filter);
508     }
509   }
510
511   private void setFilter(List aFilters, String anIdentifier, String aType, String anExpression) {
512     synchronized (aFilters) {
513       Filter filter = findFilter(aFilters, anIdentifier);
514
515       if (filter!=null) {
516         filter.setExpression(anExpression);
517         filter.setType(aType);
518       }
519     }
520   }
521
522   private Filter findFilter(List aFilters, String anIdentifier) {
523     synchronized (aFilters) {
524       Iterator i = aFilters.iterator();
525       while (i.hasNext()) {
526         Filter filter = (Filter) i.next();
527
528         if (filter.getId().equals(anIdentifier)) {
529           return filter;
530         }
531       }
532     }
533
534     return null;
535   }
536
537   private void deleteFilter(List aFilters, String anIdentifier) {
538     synchronized (aFilters) {
539       Filter filter = findFilter(aFilters, anIdentifier);
540
541       if (filter!=null) {
542         aFilters.remove(filter);
543       }
544     }
545   }
546
547   private String generateId() {
548     synchronized(this) {
549       maxIdentifier = maxIdentifier+1;
550
551       return Integer.toString(maxIdentifier);
552     }
553   }
554
555   private static class Filter {
556     private String identifier;
557     private String expression;
558     private String type;
559
560     public Filter() {
561       expression="";
562       type="";
563       identifier="";
564     }
565
566     public String getId() {
567       return identifier;
568     }
569
570     public void setId(String anId) {
571       identifier = anId;
572     }
573
574     public String getExpression() {
575       return expression;
576     }
577
578     public void setExpression(String anExpression) {
579       expression = anExpression;
580     }
581
582     public String getType() {
583       return type;
584     }
585
586     public void setType(String aType) {
587       type = aType;
588     }
589   }
590
591   private void setFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
592     synchronized(aFilters) {
593       Iterator i = aFilters.iterator();
594
595       while (i.hasNext()) {
596         Filter filter = (Filter) i.next();
597
598         aConfiguration.addProperty(aConfigKey, filter.getType()+":"+filter.getExpression());
599       }
600     }
601   }
602
603   private void getFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
604     synchronized(aFilters) {
605       aFilters.clear();
606
607       Iterator i = Arrays.asList(aConfiguration.getStringArray(aConfigKey)).iterator();
608
609       while (i.hasNext()) {
610         String filter = (String) i.next();
611         List parts = StringRoutines.separateString(filter, ":");
612
613         if (parts.size()==2) {
614           addFilter( (String) parts.get(0), (String) parts.get(1));
615         }
616       }
617     }
618   }
619
620   private static class LogEntry {
621     private String ipNumber;
622     private String browserString;
623     private String id;
624     private Date timeStamp;
625     private boolean isArticle;
626
627     public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle) {
628       ipNumber = anIpNumber;
629       browserString = aBrowserString;
630       id = anId;
631       isArticle = anIsArticle;
632       timeStamp=aTimeStamp;
633     }
634
635     public String getIpNumber() {
636       return ipNumber;
637     }
638
639     public String getBrowserString() {
640       return browserString;
641     }
642
643     public String getId() {
644       return id;
645     }
646
647     public Date getTimeStamp() {
648       return timeStamp;
649     }
650
651     public boolean getIsArticle() {
652       return isArticle;
653     }
654   }
655
656   private void truncateLog() {
657     synchronized(log) {
658       if (!logEnabled)
659         log.clear();
660       else {
661         while (log.size()>0 && log.size()>logSize) {
662           log.remove(0);
663         }
664       }
665     }
666   };
667
668   private void appendLog(LogEntry anEntry) {
669     synchronized (log) {
670       if (logEnabled) {
671         log.add(anEntry);
672         truncateLog();
673       }
674     }
675   }
676
677 }