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