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