1    /*
2     * Copyright 2009 :torweg free software group
3     *
4     * This program is free software: you can redistribute it and/or modify
5     * it under the terms of the GNU General Public License as published by
6     * the Free Software Foundation, either version 3 of the License, or
7     * (at your option) any later version.
8     * 
9     * This program is distributed in the hope that it will be useful,
10    * but WITHOUT ANY WARRANTY; without even the implied warranty of
11    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    * GNU General Public License for more details.
13    * 
14    * You should have received a copy of the GNU General Public License
15    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16    *
17    */
18   package org.torweg.pulse.util.streamscanner;
19   
20   import java.io.FilterOutputStream;
21   import java.io.IOException;
22   import java.io.OutputStream;
23   import java.util.HashSet;
24   import java.util.LinkedHashSet;
25   import java.util.Set;
26   
27   import org.slf4j.Logger;
28   import org.slf4j.LoggerFactory;
29   import org.torweg.pulse.accesscontrol.Role;
30   import org.torweg.pulse.accesscontrol.User;
31   import org.torweg.pulse.configuration.Configurable;
32   import org.torweg.pulse.configuration.Configuration;
33   import org.torweg.pulse.configuration.ConfigurationException;
34   import org.torweg.pulse.configuration.PoorMansCache;
35   import org.torweg.pulse.service.PulseException;
36   
37   /**
38    * is a filter chain for user supplied files.
39    * <p>
40    * The {@code OutputStreamScannerChain} is applied for both form and WebDAV
41    * based file uploads and can of course be used in your own code.
42    * </p>
43    * <p>
44    * <strong>Note:</strong> the {@code OutputStreamScannerChain} is
45    * <em>not thread-safe</em>.
46    * </p>
47    * <p>
48    * The filter chain is configured in the main config directory (
49    * <tt>WEB-INF/conf</tt>) by
50    * <tt>org.torweg.pulse.util.streamscanner.OutputStreamScannerChain.xml</tt>.
51    * </p>
52    * 
53    * @author Thomas Weber
54    * @version $Revision: 1914 $
55    */
56   public final class OutputStreamScannerChain extends FilterOutputStream {
57   
58       /**
59        * the logger.
60        */
61       private static final Logger LOGGER = LoggerFactory
62               .getLogger(OutputStreamScannerChain.class);
63   
64       /**
65        * the filter chain.
66        */
67       private final Set<IStreamScanner> filterChain;
68   
69       /**
70        * creates a new {@code OutputStreamScannerChain} for the given {@code
71        * OutputStream}.
72        * 
73        * @param out
74        *            the {@code OutputStream} to be scanned
75        * @param ct
76        *            the content-type of the scanned stream
77        * @param u
78        *            the user
79        */
80       public OutputStreamScannerChain(final OutputStream out, final String ct,
81               final User u) {
82           super(out);
83           this.filterChain = new LinkedHashSet<IStreamScanner>();
84           StreamScannerChainConfig configuration = (StreamScannerChainConfig) PoorMansCache
85                   .getConfiguration(this.getClass());
86           if (configuration == null) {
87               throw new ConfigurationException("The "
88                       + this.getClass().getSimpleName() + " is not configured.");
89           }
90   
91           /* prepare the role name set of the current user */
92           Set<String> userRoleNames = new HashSet<String>();
93           for (Role r : u.getRoles()) {
94               userRoleNames.add(r.getName());
95           }
96   
97           /* set up the chain */
98           for (StreamScannerConfig filterConfig : configuration
99                   .getScannerConfigurations()) {
100              try {
101                  /* check, whether the filter shall be skipped */
102                  if (skip(userRoleNames, filterConfig)) {
103                      continue;
104                  }
105                  /* actually add the filter to the chain */
106                  IStreamScanner filter = (IStreamScanner) Class.forName(
107                          filterConfig.getScannerClass()).newInstance();
108                  if (filter instanceof Configurable) {
109                      @SuppressWarnings("unchecked")
110                      Configurable<Configuration> configurable = (Configurable<Configuration>) filter;
111                      configurable.initialize(filterConfig);
112                  }
113                  filter.setContentType(ct);
114                  filter.setUser(u);
115                  this.filterChain.add(filter);
116              } catch (InstantiationException e) {
117                  throw new ConfigurationException(e.getLocalizedMessage(), e);
118              } catch (IllegalAccessException e) {
119                  throw new ConfigurationException(e.getLocalizedMessage(), e);
120              } catch (ClassNotFoundException e) {
121                  throw new ConfigurationException(e.getLocalizedMessage(), e);
122              }
123          }
124      }
125  
126      /**
127       * checks whether any of the user's roles matches any of the exclude roles
128       * for the current scanner.
129       * 
130       * @param userRoleNames
131       *            the role names of the user's roles
132       * @param filterConfig
133       *            the current filter configuration
134       * @return {@code true}, if and only if, the scanner is to be skipped.
135       *         Otherwise {@code false}.
136       */
137      private boolean skip(final Set<String> userRoleNames,
138              final StreamScannerConfig filterConfig) {
139          for (String roleName : filterConfig.getExludeRoleNames()) {
140              if (userRoleNames.contains(roleName)) {
141                  LOGGER.debug("Skipping filter '{}' due to user role '{}'.",
142                          filterConfig.getScannerClass(), roleName + "'.");
143                  return true;
144              }
145          }
146          return false;
147      }
148  
149      /**
150       * filters and then writes the given byte array to the underlying {@code
151       * OutputStream}.
152       * 
153       * @param b
154       *            the byte array to be scanned and written
155       * @throws StreamException
156       *             on errors writing to the underlying stream
157       * @throws InacceptableStreamException
158       *             if the scanned stream is not acceptable (e.g. it contains a
159       *             virus)
160       */
161      @Override
162      public void write(final byte[] b) throws StreamException,
163              InacceptableStreamException {
164          for (IStreamScanner filter : this.filterChain) {
165              try {
166                  filter.scan(b);
167              } catch (StreamException e) {
168                  shutdown();
169                  throw e;
170              } catch (InacceptableStreamException e) {
171                  shutdown();
172                  throw e;
173              } catch (Exception e) {
174                  shutdown();
175                  throw new PulseException("Exception in ScannerChain: "
176                          + e.getLocalizedMessage(), e);
177              }
178          }
179          try {
180              super.write(b);
181          } catch (IOException e) {
182              throw new StreamException(e);
183          }
184      }
185  
186      /**
187       * scans and then writes <tt>len</tt> bytes from the specified byte array
188       * starting at offset <tt>off</tt> to the underlying {@code OutputStream}.
189       * 
190       * @param b
191       *            the byte array
192       * @param off
193       *            the offset
194       * @param len
195       *            the length
196       * 
197       * @throws StreamException
198       *             on errors writing to the underlying stream
199       * @throws InacceptableStreamException
200       *             if the filtered stream is not acceptable (e.g. it contains a
201       *             virus)
202       */
203      @Override
204      public void write(final byte[] b, final int off, final int len)
205              throws StreamException, InacceptableStreamException {
206          byte[] writeBytes = new byte[len];
207          System.arraycopy(b, off, writeBytes, 0, len);
208          for (IStreamScanner filter : this.filterChain) {
209              try {
210                  filter.scan(writeBytes);
211              } catch (StreamException e) {
212                  shutdown();
213                  throw e;
214              } catch (InacceptableStreamException e) {
215                  shutdown();
216                  throw e;
217              } catch (Exception e) {
218                  shutdown();
219                  throw new PulseException("Exception in ScannerChain: "
220                          + e.getLocalizedMessage(), e);
221              }
222          }
223          try {
224              super.write(b, off, len);
225          } catch (IOException e) {
226              shutdown();
227              throw new StreamException(e);
228          }
229      }
230  
231      /**
232       * scans and then writes the given byte to the underlying {@code
233       * OutputStream}.
234       * 
235       * @param b
236       *            the byte
237       * @throws StreamException
238       *             on errors writing to the underlying stream
239       * @throws InacceptableStreamException
240       *             if the filtered stream is not acceptable (e.g. it contains a
241       *             virus)
242       */
243      @Override
244      public void write(final int b) throws StreamException,
245              InacceptableStreamException {
246          for (IStreamScanner filter : this.filterChain) {
247              filter.scan(b);
248          }
249          try {
250              super.write(b);
251          } catch (IOException e) {
252              shutdown();
253              throw new StreamException(e);
254          }
255      }
256  
257      /**
258       * closes the scanner chain and its underlying stream.
259       * 
260       * @throws IOException
261       *             on errors closing the underlying stream or any of the filters
262       * @throws InacceptableStreamException
263       *             if the filtered stream is not acceptable (e.g. it contains a
264       *             virus)
265       */
266      @Override
267      public void close() throws IOException, InacceptableStreamException {
268          for (IStreamScanner filter : this.filterChain) {
269              filter.close();
270          }
271          super.close();
272      }
273  
274      /**
275       * unconditionally frees all resources of the filters in the chain.
276       */
277      private void shutdown() {
278          for (IStreamScanner filter : this.filterChain) {
279              filter.shutdown();
280          }
281          try {
282              super.close();
283          } catch (Exception e) {
284              return; // ignore
285          }
286      }
287  }
288