source: trunk/quest-plugin-php/src/main/java/de/ugoe/cs/quest/plugin/php/WeblogParser.java @ 639

Last change on this file since 639 was 639, checked in by sherbold, 12 years ago
  • all usages of the Console tracing API now define log levels
  • Property svn:mime-type set to text/plain
File size: 13.1 KB
RevLine 
[434]1package de.ugoe.cs.quest.plugin.php;
[54]2
3import java.io.FileNotFoundException;
4import java.io.IOException;
[111]5import java.net.URI;
6import java.net.URISyntaxException;
[54]7import java.text.ParseException;
8import java.text.SimpleDateFormat;
9import java.util.ArrayList;
[203]10import java.util.Collection;
[54]11import java.util.HashMap;
[232]12import java.util.HashSet;
[54]13import java.util.LinkedList;
14import java.util.List;
15import java.util.Map;
[232]16import java.util.Set;
[639]17import java.util.logging.Level;
[54]18
[554]19import de.ugoe.cs.quest.eventcore.Event;
20import de.ugoe.cs.quest.eventcore.IEventTarget;
21import de.ugoe.cs.quest.eventcore.IEventType;
22import de.ugoe.cs.quest.plugin.php.eventcore.PHPEventTarget;
23import de.ugoe.cs.quest.plugin.php.eventcore.PHPEventType;
24import de.ugoe.cs.quest.plugin.php.eventcore.WebRequest;
[74]25import de.ugoe.cs.util.FileTools;
[68]26import de.ugoe.cs.util.console.Console;
[54]27
[171]28/**
29 * <p>
30 * Provides functionality to parse log files with web request.
31 * </p>
32 *
33 * @author Steffen Herbold
34 * @version 1.0
35 */
[54]36public class WeblogParser {
[171]37
38        /**
39         * <p>
40         * Timeout between two sessions in milliseconds.
41         * </p>
42         */
[54]43        private long timeout;
[171]44
45        /**
46         * <p>
[225]47         * Minimal length of a session. All shorter sessions will be pruned.<br>
[171]48         * Default: 2
49         * </p>
50         */
[68]51        private int minLength = 2;
[171]52
53        /**
54         * <p>
[225]55         * Maximal length of a session. All longer sessions will be pruned.<br>
56         * Default: 100
[224]57         * </p>
58         */
59        private int maxLength = 100;
60
61        /**
62         * <p>
[225]63         * URL of the server that generated the log that is currently parser; null
64         * of URL is not available.<br>
65         * Default: null
66         * </p>
67         */
68        private String url = null;
69
70        /**
71         * <p>
[171]72         * Collection of generated sequences.
73         * </p>
74         */
[554]75        private List<List<Event>> sequences;
[171]76
77        /**
78         * <p>
[232]79         * List that stores the users (identified through their cookie id) to each
80         * sequence.
81         * </p>
82         */
83        private List<String> users;
84
85        /**
86         * <p>
87         * List that stores the frequent users (identified through their cookie id)
88         * to each sequence.
89         * </p>
90         */
91        private List<String> frequentUsers;
92
93        /**
94         * <p>
95         * Sequences for all frequent users.
96         * </p>
97         */
[554]98        private List<Collection<List<Event>>> sequencesFrequentUsers;
[232]99
100        /**
101         * <p>
102         * Threshold that defines how many sessions of a user are require to deem
103         * the user frequent. Note, that only sessions whose lengths is in range if
104         * {@link #minLength} and {@link #maxLength} are counted.
105         * </p>
106         */
107        private int frequentUsersThreshold = -1;
108
109        /**
110         * <p>
[171]111         * Name and path of the robot filter.
112         * </p>
113         */
[72]114        private static final String ROBOTFILTERFILE = "misc/robotfilter.txt";
[171]115
116        /**
117         * <p>
118         * Field that contains a regular expression that matches all robots
119         * contained in {@link #ROBOTFILTERFILE}.
120         * </p>
121         */
122        private String robotRegex = null;
123
124        /**
125         * <p>
126         * Constructor. Creates a new WeblogParser with a default timeout of
127         * 3,600,000 milliseconds (1 hour).
128         * </p>
129         */
[54]130        public WeblogParser() {
[171]131                this(3600000);
[54]132        }
[171]133
134        /**
135         * <p>
136         * Constructor. Creates a new WeblogParser.
137         * </p>
138         *
139         * @param timeout
140         *            session timeout
141         */
[54]142        public WeblogParser(long timeout) {
143                this.timeout = timeout;
144        }
[171]145
146        /**
147         * <p>
148         * Returns the generated event sequences.
149         * </p>
150         *
151         * @return generated event sequences
152         */
[554]153        public Collection<List<Event>> getSequences() {
[171]154                return sequences;
[54]155        }
[171]156
157        /**
158         * <p>
159         * Sets the session timeout.
160         * </p>
161         *
162         * @param timeout
163         *            new session timeout
164         */
[68]165        public void setTimeout(long timeout) {
166                this.timeout = timeout;
167        }
[171]168
169        /**
170         * <p>
171         * Sets the minimal length of a session. All sessions that contain less
172         * events will be pruned.
173         * </p>
174         *
175         * @param minLength
176         *            new minimal length
177         */
[68]178        public void setMinLength(int minLength) {
179                this.minLength = minLength;
180        }
[171]181
182        /**
183         * <p>
[224]184         * Sets the maximal length of a session. All sessions that contain more
185         * events will be pruned.
186         * </p>
187         *
188         * @param maxLength
189         *            new maximal length
190         */
191        public void setMaxLength(int maxLength) {
192                this.maxLength = maxLength;
193        }
194
195        /**
196         * <p>
[225]197         * Sets the URL of the server from which this log was generated. Often
198         * required for replay generation
199         * </p>
200         *
201         * @param url
202         *            URL of the server
203         */
204        public void setUrl(String url) {
205                this.url = url;
206        }
207
208        /**
209         * <p>
[232]210         * Sets the threshold for frequent users.
211         * </p>
212         *
213         * @param threshold
214         *            threshold value; if the value is &lt;1, the sessions of the
215         *            frequent users will not be determined
216         */
217        public void setFrequentUserThreshold(int threshold) {
218                this.frequentUsersThreshold = threshold;
219        }
220
221        /**
222         * <p>
223         * Returns the IDs of all frequent users.
224         * </p>
225         *
226         * @return IDs of the frequent users
227         */
228        public List<String> getFrequentUsers() {
229                return frequentUsers;
230        }
231
232        /**
233         * <p>
234         * Returns the sequences of all frequent users.
235         * </p>
236         * </p>
237         *
238         * @return list of the sequences of all frequent users
239         */
[554]240        public List<Collection<List<Event>>> getFrequentUserSequences() {
[232]241                return sequencesFrequentUsers;
242        }
243
244        /**
245         * <p>
[171]246         * Parses a web log file.
247         * </p>
248         *
249         * @param filename
250         *            name and path of the log file
251         * @throws IOException
252         *             thrown if there is a problem with reading the log file
253         * @throws FileNotFoundException
254         *             thrown if the log file is not found
255         * @throws ParseException
256         *             thrown the date format is invalid
257         */
258        public void parseFile(String filename) throws IOException,
[177]259                        FileNotFoundException, ParseException {
[74]260                String[] lines = FileTools.getLinesFromFile(filename);
[171]261
[54]262                Map<String, List<Integer>> cookieSessionMap = new HashMap<String, List<Integer>>();
[554]263                Map<String, Long> cookieTimestampMap = new HashMap<String, Long>();
264               
[54]265                int lastId = -1;
[171]266
267                SimpleDateFormat dateFormat = new SimpleDateFormat(
268                                "yyyy-MM-dd HH:mm:ss");
[72]269                loadRobotRegex();
[171]270
[554]271                sequences = new ArrayList<List<Event>>();
[232]272                users = new ArrayList<String>();
[171]273
[177]274                int lineCounter = 0;
[171]275                for (String line : lines) {
[177]276                        lineCounter++;
[171]277                        String[] values = line.substring(1, line.length() - 1).split(
278                                        "\" \"");
279
[54]280                        // use cookie as session identifier
281                        int cookieStart = values[0].lastIndexOf('.');
[171]282                        String cookie = values[0].substring(cookieStart + 1);
[72]283                        String dateString = values[1];
[54]284                        long timestamp = dateFormat.parse(dateString).getTime();
[111]285                        String uriString = values[2];
[72]286                        // String ref = values[3]; // referer is not yet used!
[78]287                        String agent;
[171]288                        if (values.length > 4) {
[78]289                                agent = values[4];
290                        } else {
291                                agent = "noagent";
292                        }
[171]293
[54]294                        List<String> postedVars = new ArrayList<String>();
[171]295                        if (values.length == 6) { // post vars found
296                                for (String postVar : values[5].trim().split(" ")) {
[363]297                                        if (!isBrokenVariable(postVar)) {
[232]298                                                postedVars.add(postVar);
299                                        }
[72]300                                }
[54]301                        }
[171]302                        if (!isRobot(agent)) {
[177]303                                try {
304                                        URI uri = new URI(uriString);
305                                        String path = uri.getPath();
306                                        List<String> getVars = extractGetVarsFromUri(uri);
[410]307                                       
[554]308                                        IEventType type = new PHPEventType(path, postedVars, getVars);
309                                        IEventTarget target = new PHPEventTarget(path);
310                                        Event event = new Event(type, target);
311                                        event.addReplayable(new WebRequest(url, path, postedVars, getVars));
[224]312
[177]313                                        // find session and add event
314                                        List<Integer> sessionIds = cookieSessionMap.get(cookie);
315                                        if (sessionIds == null) {
316                                                sessionIds = new ArrayList<Integer>();
317                                                // start new session
318                                                sessionIds.add(++lastId);
319                                                cookieSessionMap.put(cookie, sessionIds);
[554]320                                                sequences.add(new LinkedList<Event>());
[232]321                                                users.add(cookie);
[177]322                                        }
323                                        Integer lastSessionIndex = sessionIds
324                                                        .get(sessionIds.size() - 1);
[554]325                                        List<Event> lastSession = sequences
[224]326                                                        .get(lastSessionIndex);
[177]327                                        long lastEventTime = timestamp;
328                                        if (!lastSession.isEmpty()) {
[554]329                                                lastEventTime = cookieTimestampMap.get(cookie);
[177]330                                        }
331                                        if (timestamp - lastEventTime > timeout) {
332                                                sessionIds.add(++lastId);
[554]333                                                List<Event> newSession = new LinkedList<Event>();
[177]334                                                newSession.add(event);
335                                                sequences.add(newSession);
[232]336                                                users.add(cookie);
[177]337                                        } else {
338                                                lastSession.add(event);
339                                        }
[554]340                                        cookieTimestampMap.put(cookie, timestamp);
[177]341                                } catch (URISyntaxException e) {
[639]342                                        Console.traceln(Level.FINE, "Ignored line " + lineCounter + ": "
[224]343                                                        + e.getMessage());
[171]344                                }
[54]345                        }
346                }
[639]347                Console.traceln(Level.INFO, "" + sequences.size() + " user sequences found");
[224]348                pruneSequences();
[639]349                Console.traceln(Level.INFO, "" + sequences.size()
[232]350                                + " remaining after pruning of sequences shorter than "
351                                + minLength);
352                Set<String> uniqueUsers = new HashSet<String>(users);
[639]353                Console.traceln(Level.INFO, "" + uniqueUsers.size() + " unique users");
[232]354                if (frequentUsersThreshold > 0) {
355                        generateFrequentUserSequences(uniqueUsers);
356                }
[74]357        }
358
[171]359        /**
360         * <p>
[232]361         * Generates the frequent user sequences, according to the threshold
362         * {@link #frequentUsersThreshold}.
[171]363         * </p>
[232]364         *
365         * @param uniqueUsers
366         *            set with all user IDs
[171]367         */
[232]368        private void generateFrequentUserSequences(Set<String> uniqueUsers) {
369                frequentUsers = new ArrayList<String>();
[554]370                sequencesFrequentUsers = new ArrayList<Collection<List<Event>>>();
[232]371                for (String user : uniqueUsers) {
372                        List<String> tmp = new ArrayList<String>();
373                        tmp.add(user);
374                        List<String> usersCopy = new LinkedList<String>(users);
375                        usersCopy.retainAll(tmp);
376                        int size = usersCopy.size();
377                        if (size >= frequentUsersThreshold) {
378                                frequentUsers.add(user);
[554]379                                Collection<List<Event>> sequencesUser = new ArrayList<List<Event>>();
[232]380                                for (int i = 0; i < sequences.size(); i++) {
381                                        if (users.get(i).equals(user)) {
382                                                sequencesUser.add(sequences.get(i));
383                                        }
384                                }
385                                sequencesFrequentUsers.add(sequencesUser);
386
387                        }
388                }
[639]389                Console.traceln(Level.INFO, "" + frequentUsers.size() + " users with more than "
[232]390                                + frequentUsersThreshold + " sequences");
391        }
392
393        /**
394         * <p>
395         * Prunes sequences shorter than {@link #minLength} and longer than
396         * {@link #maxLength}.
397         * </p>
398         */
[224]399        private void pruneSequences() {
[171]400                int i = 0;
401                while (i < sequences.size()) {
[224]402                        if ((sequences.get(i).size() < minLength)
403                                        || sequences.get(i).size() > maxLength) {
[68]404                                sequences.remove(i);
[232]405                                users.remove(i);
[72]406                        } else {
407                                i++;
[68]408                        }
409                }
[232]410
[54]411        }
[171]412
413        /**
414         * <p>
415         * Reads {@link #ROBOTFILTERFILE} and creates a regular expression that
416         * matches all the robots defined in the file. The regular expression is
417         * stored in the field {@link #robotRegex}.
418         * </p>
419         *
420         * @throws IOException
421         *             thrown if there is a problem reading the robot filter
422         * @throws FileNotFoundException
423         *             thrown if the robot filter is not found
424         */
[72]425        private void loadRobotRegex() throws IOException, FileNotFoundException {
[74]426                String[] lines = FileTools.getLinesFromFile(ROBOTFILTERFILE);
[72]427                StringBuilder regex = new StringBuilder();
[171]428                for (int i = 0; i < lines.length; i++) {
429                        regex.append("(.*" + lines[i] + ".*)");
430                        if (i != lines.length - 1) {
[176]431                                regex.append('|');
[72]432                        }
433                }
434                robotRegex = regex.toString();
435        }
[171]436
437        /**
438         * <p>
439         * Checks whether an agent is a robot.
440         * </p>
441         *
442         * @param agent
443         *            agent that is checked
444         * @return true, if the agent is a robot; false otherwise
445         */
[72]446        private boolean isRobot(String agent) {
447                return agent.matches(robotRegex);
448        }
[171]449
450        /**
451         * <p>
452         * Parses the URI and extracts the GET variables that have been passed.
453         * </p>
454         *
455         * @param uri
456         *            URI that is parsed
457         * @return a list with all GET variables
[410]458         * @throws URISyntaxException
459         *             thrown if one of the variables seems to indicate that the
460         *             request is a malicious attack on the web application
[171]461         */
[410]462        private List<String> extractGetVarsFromUri(URI uri)
463                        throws URISyntaxException {
[111]464                List<String> getVars = new ArrayList<String>();
465                String query = uri.getQuery();
[171]466                if (query != null) {
[111]467                        String[] paramPairs = query.split("&");
[171]468                        for (String paramPair : paramPairs) {
[111]469                                String[] paramSplit = paramPair.split("=");
[363]470                                if (!isBrokenVariable(paramSplit[0])) {
[410]471                                        for (int i = 1; i < paramSplit.length; i++) {
472                                                checkForAttack(paramSplit[i]);
473                                        }
[232]474                                        getVars.add(paramSplit[0]);
475                                }
[111]476                        }
477                }
478                return getVars;
479        }
[363]480
481        /**
482         * <p>
483         * Checks if a variable is broken.Currently, the check rather imprecise and
484         * checks only if the term &quot;and&quot; is part of the variable name.
485         * </p>
486         *
487         * @param var
488         *            variable that is checked
489         * @return true if the variable is broken, false otherwise
490         */
491        private boolean isBrokenVariable(String var) {
492                return var.contains("and");
493        }
[410]494
495        /**
496         * <p>
497         * Checks if the variable name send with a request seems like an attack on the server.
498         * </p>
499         * @param value
500         * @throws URISyntaxException
501         */
502        private void checkForAttack(String value) throws URISyntaxException {
503                if (value.contains("UNION+") || value.contains("SELECT+")) {
504                        throw new URISyntaxException(value, "possible injection attack");
505                }
506        }
[54]507}
Note: See TracBrowser for help on using the repository browser.