001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Portions Copyright 2013-2015 ForgeRock AS
025 */
026package org.opends.server.loggers;
027
028import static org.opends.messages.ConfigMessages.*;
029import static org.opends.server.util.StaticUtils.*;
030
031import java.io.File;
032import java.io.IOException;
033import java.text.SimpleDateFormat;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042
043import org.forgerock.i18n.LocalizableMessage;
044import org.forgerock.opendj.config.server.ConfigException;
045import org.forgerock.util.Utils;
046import org.opends.server.admin.server.ConfigurationChangeListener;
047import org.opends.server.admin.std.server.FileBasedHTTPAccessLogPublisherCfg;
048import org.opends.server.core.DirectoryServer;
049import org.opends.server.core.ServerContext;
050import org.forgerock.opendj.config.server.ConfigChangeResult;
051import org.opends.server.types.DN;
052import org.opends.server.types.DirectoryException;
053import org.opends.server.types.FilePermission;
054import org.opends.server.types.InitializationException;
055import org.opends.server.util.TimeThread;
056
057/**
058 * This class provides the implementation of the HTTP access logger used by the
059 * directory server.
060 */
061public final class TextHTTPAccessLogPublisher extends
062    HTTPAccessLogPublisher<FileBasedHTTPAccessLogPublisherCfg>
063    implements ConfigurationChangeListener<FileBasedHTTPAccessLogPublisherCfg>
064{
065
066  // Extended log format standard fields
067  private static final String ELF_C_IP = "c-ip";
068  private static final String ELF_C_PORT = "c-port";
069  private static final String ELF_CS_HOST = "cs-host";
070  private static final String ELF_CS_METHOD = "cs-method";
071  private static final String ELF_CS_URI_QUERY = "cs-uri-query";
072  private static final String ELF_CS_USER_AGENT = "cs(User-Agent)";
073  private static final String ELF_CS_USERNAME = "cs-username";
074  private static final String ELF_CS_VERSION = "cs-version";
075  private static final String ELF_S_COMPUTERNAME = "s-computername";
076  private static final String ELF_S_IP = "s-ip";
077  private static final String ELF_S_PORT = "s-port";
078  private static final String ELF_SC_STATUS = "sc-status";
079  // Application specific fields (eXtensions)
080  private static final String X_CONNECTION_ID = "x-connection-id";
081  private static final String X_DATETIME = "x-datetime";
082  private static final String X_ETIME = "x-etime";
083
084  private static final Set<String> ALL_SUPPORTED_FIELDS = new HashSet<>(
085      Arrays.asList(ELF_C_IP, ELF_C_PORT, ELF_CS_HOST, ELF_CS_METHOD,
086          ELF_CS_URI_QUERY, ELF_CS_USER_AGENT, ELF_CS_USERNAME, ELF_CS_VERSION,
087          ELF_S_COMPUTERNAME, ELF_S_IP, ELF_S_PORT, ELF_SC_STATUS,
088          X_CONNECTION_ID, X_DATETIME, X_ETIME));
089
090  /**
091   * Returns an instance of the text HTTP access log publisher that will print
092   * all messages to the provided writer. This is used to print the messages to
093   * the console when the server starts up.
094   *
095   * @param writer
096   *          The text writer where the message will be written to.
097   * @return The instance of the text error log publisher that will print all
098   *         messages to standard out.
099   */
100  public static TextHTTPAccessLogPublisher getStartupTextHTTPAccessPublisher(
101      final TextWriter writer)
102  {
103    final TextHTTPAccessLogPublisher startupPublisher =
104      new TextHTTPAccessLogPublisher();
105    startupPublisher.writer = writer;
106    return startupPublisher;
107  }
108
109
110
111  private TextWriter writer;
112  private FileBasedHTTPAccessLogPublisherCfg cfg;
113  private List<String> logFormatFields;
114  private String timeStampFormat = "dd/MMM/yyyy:HH:mm:ss Z";
115
116
117  /** {@inheritDoc} */
118  @Override
119  public ConfigChangeResult applyConfigurationChange(
120      final FileBasedHTTPAccessLogPublisherCfg config)
121  {
122    final ConfigChangeResult ccr = new ConfigChangeResult();
123
124    final File logFile = getFileForPath(config.getLogFile());
125    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
126    try
127    {
128      final FilePermission perm = FilePermission.decodeUNIXMode(config
129          .getLogFilePermissions());
130
131      final boolean writerAutoFlush = config.isAutoFlush()
132          && !config.isAsynchronous();
133
134      TextWriter currentWriter;
135      // Determine the writer we are using. If we were writing
136      // asynchronously, we need to modify the underlying writer.
137      if (writer instanceof AsynchronousTextWriter)
138      {
139        currentWriter = ((AsynchronousTextWriter) writer).getWrappedWriter();
140      }
141      else if (writer instanceof ParallelTextWriter)
142      {
143        currentWriter = ((ParallelTextWriter) writer).getWrappedWriter();
144      }
145      else
146      {
147        currentWriter = writer;
148      }
149
150      if (currentWriter instanceof MultifileTextWriter)
151      {
152        final MultifileTextWriter mfWriter =
153          (MultifileTextWriter) currentWriter;
154
155        mfWriter.setNamingPolicy(fnPolicy);
156        mfWriter.setFilePermissions(perm);
157        mfWriter.setAppend(config.isAppend());
158        mfWriter.setAutoFlush(writerAutoFlush);
159        mfWriter.setBufferSize((int) config.getBufferSize());
160        mfWriter.setInterval(config.getTimeInterval());
161
162        mfWriter.removeAllRetentionPolicies();
163        mfWriter.removeAllRotationPolicies();
164
165        for (final DN dn : config.getRotationPolicyDNs())
166        {
167          mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
168        }
169
170        for (final DN dn : config.getRetentionPolicyDNs())
171        {
172          mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
173        }
174
175        if (writer instanceof AsynchronousTextWriter
176            && !config.isAsynchronous())
177        {
178          // The asynchronous setting is being turned off.
179          final AsynchronousTextWriter asyncWriter =
180              (AsynchronousTextWriter) writer;
181          writer = mfWriter;
182          asyncWriter.shutdown(false);
183        }
184
185        if (writer instanceof ParallelTextWriter && !config.isAsynchronous())
186        {
187          // The asynchronous setting is being turned off.
188          final ParallelTextWriter asyncWriter = (ParallelTextWriter) writer;
189          writer = mfWriter;
190          asyncWriter.shutdown(false);
191        }
192
193        if (!(writer instanceof AsynchronousTextWriter)
194            && config.isAsynchronous())
195        {
196          // The asynchronous setting is being turned on.
197          final AsynchronousTextWriter asyncWriter = new AsynchronousTextWriter(
198              "Asynchronous Text Writer for " + config.dn(),
199              config.getQueueSize(), config.isAutoFlush(), mfWriter);
200          writer = asyncWriter;
201        }
202
203        if (!(writer instanceof ParallelTextWriter) && config.isAsynchronous())
204        {
205          // The asynchronous setting is being turned on.
206          final ParallelTextWriter asyncWriter = new ParallelTextWriter(
207              "Parallel Text Writer for " + config.dn(),
208              config.isAutoFlush(), mfWriter);
209          writer = asyncWriter;
210        }
211
212        if (cfg.isAsynchronous() && config.isAsynchronous()
213            && cfg.getQueueSize() != config.getQueueSize())
214        {
215          ccr.setAdminActionRequired(true);
216        }
217
218        if (!config.getLogRecordTimeFormat().equals(timeStampFormat))
219        {
220          TimeThread.removeUserDefinedFormatter(timeStampFormat);
221          timeStampFormat = config.getLogRecordTimeFormat();
222        }
223
224        cfg = config;
225        logFormatFields = extractFieldsOrder(cfg.getLogFormat());
226        LocalizableMessage errorMessage = validateLogFormat(logFormatFields);
227        if (errorMessage != null)
228        {
229          ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
230          ccr.setAdminActionRequired(true);
231          ccr.addMessage(errorMessage);
232        }
233      }
234    }
235    catch (final Exception e)
236    {
237      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
238      ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
239          config.dn(), stackTraceToSingleLineString(e)));
240    }
241
242    return ccr;
243  }
244
245
246  private List<String> extractFieldsOrder(String logFormat)
247  {
248    // there will always be at least one field value due to the regexp
249    // validating the log format
250    return Arrays.asList(logFormat.split(" "));
251  }
252
253  /**
254   * Validates the provided fields for the log format.
255   *
256   * @param fields
257   *          the fields comprising the log format.
258   * @return an error message when validation fails, null otherwise
259   */
260  private LocalizableMessage validateLogFormat(List<String> fields)
261  {
262    final Collection<String> unsupportedFields =
263        subtract(fields, ALL_SUPPORTED_FIELDS);
264    if (!unsupportedFields.isEmpty())
265    { // there are some unsupported fields. List them.
266      return WARN_CONFIG_LOGGING_UNSUPPORTED_FIELDS_IN_LOG_FORMAT.get(
267          cfg.dn(), Utils.joinAsString(", ", unsupportedFields));
268    }
269    if (fields.size() == unsupportedFields.size())
270    { // all fields are unsupported
271      return ERR_CONFIG_LOGGING_EMPTY_LOG_FORMAT.get(cfg.dn());
272    }
273    return null;
274  }
275
276  /**
277   * Returns a new Collection containing a - b.
278   *
279   * @param <T>
280   * @param a
281   *          the collection to subtract from, must not be null
282   * @param b
283   *          the collection to subtract, must not be null
284   * @return a new collection with the results
285   */
286  private <T> Collection<T> subtract(Collection<T> a, Collection<T> b)
287  {
288    final Collection<T> result = new ArrayList<>();
289    for (T elem : a)
290    {
291      if (!b.contains(elem))
292      {
293        result.add(elem);
294      }
295    }
296    return result;
297  }
298
299  /** {@inheritDoc} */
300  @Override
301  public void initializeLogPublisher(
302      final FileBasedHTTPAccessLogPublisherCfg cfg, ServerContext serverContext)
303      throws ConfigException, InitializationException
304  {
305    final File logFile = getFileForPath(cfg.getLogFile());
306    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
307
308    try
309    {
310      final FilePermission perm = FilePermission.decodeUNIXMode(cfg
311          .getLogFilePermissions());
312
313      final LogPublisherErrorHandler errorHandler =
314        new LogPublisherErrorHandler(cfg.dn());
315
316      final boolean writerAutoFlush = cfg.isAutoFlush()
317          && !cfg.isAsynchronous();
318
319      final MultifileTextWriter theWriter = new MultifileTextWriter(
320          "Multifile Text Writer for " + cfg.dn(),
321          cfg.getTimeInterval(), fnPolicy, perm, errorHandler, "UTF-8",
322          writerAutoFlush, cfg.isAppend(), (int) cfg.getBufferSize());
323
324      // Validate retention and rotation policies.
325      for (final DN dn : cfg.getRotationPolicyDNs())
326      {
327        theWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
328      }
329
330      for (final DN dn : cfg.getRetentionPolicyDNs())
331      {
332        theWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
333      }
334
335      if (cfg.isAsynchronous())
336      {
337        if (cfg.getQueueSize() > 0)
338        {
339          this.writer = new AsynchronousTextWriter("Asynchronous Text Writer for " + cfg.dn(),
340              cfg.getQueueSize(), cfg.isAutoFlush(), theWriter);
341        }
342        else
343        {
344          this.writer = new ParallelTextWriter("Parallel Text Writer for " + cfg.dn(),
345              cfg.isAutoFlush(), theWriter);
346        }
347      }
348      else
349      {
350        this.writer = theWriter;
351      }
352    }
353    catch (final DirectoryException e)
354    {
355      throw new InitializationException(
356          ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(cfg.dn(), e), e);
357    }
358    catch (final IOException e)
359    {
360      throw new InitializationException(
361          ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, cfg.dn(), e), e);
362    }
363
364    this.cfg = cfg;
365    logFormatFields = extractFieldsOrder(cfg.getLogFormat());
366    LocalizableMessage error = validateLogFormat(logFormatFields);
367    if (error != null)
368    {
369      throw new InitializationException(error);
370    }
371    timeStampFormat = cfg.getLogRecordTimeFormat();
372
373    cfg.addFileBasedHTTPAccessChangeListener(this);
374  }
375
376
377  /** {@inheritDoc} */
378  @Override
379  public boolean isConfigurationAcceptable(
380      final FileBasedHTTPAccessLogPublisherCfg configuration,
381      final List<LocalizableMessage> unacceptableReasons)
382  {
383    return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
384  }
385
386
387  /** {@inheritDoc} */
388  @Override
389  public boolean isConfigurationChangeAcceptable(
390      final FileBasedHTTPAccessLogPublisherCfg config,
391      final List<LocalizableMessage> unacceptableReasons)
392  {
393    // Validate the time-stamp formatter.
394    final String formatString = config.getLogRecordTimeFormat();
395    try
396    {
397       new SimpleDateFormat(formatString);
398    }
399    catch (final Exception e)
400    {
401      unacceptableReasons.add(ERR_CONFIG_LOGGING_INVALID_TIME_FORMAT.get(formatString));
402      return false;
403    }
404
405    // Make sure the permission is valid.
406    try
407    {
408      final FilePermission filePerm = FilePermission.decodeUNIXMode(config
409          .getLogFilePermissions());
410      if (!filePerm.isOwnerWritable())
411      {
412        final LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config
413            .getLogFilePermissions());
414        unacceptableReasons.add(message);
415        return false;
416      }
417    }
418    catch (final DirectoryException e)
419    {
420      unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
421      return false;
422    }
423
424    return true;
425  }
426
427
428  /** {@inheritDoc} */
429  @Override
430  public final void close()
431  {
432    writer.shutdown();
433    TimeThread.removeUserDefinedFormatter(timeStampFormat);
434    if (cfg != null)
435    {
436      cfg.removeFileBasedHTTPAccessChangeListener(this);
437    }
438  }
439
440  /** {@inheritDoc} */
441  @Override
442  public final DN getDN()
443  {
444    return cfg != null ? cfg.dn() : null;
445  }
446
447  /** {@inheritDoc} */
448  @Override
449  public void logRequestInfo(HTTPRequestInfo ri)
450  {
451    final Map<String, Object> fields = new HashMap<>();
452    fields.put(ELF_C_IP, ri.getClientAddress());
453    fields.put(ELF_C_PORT, ri.getClientPort());
454    fields.put(ELF_CS_HOST, ri.getClientHost());
455    fields.put(ELF_CS_METHOD, ri.getMethod());
456    fields.put(ELF_CS_URI_QUERY, ri.getQuery());
457    fields.put(ELF_CS_USER_AGENT, ri.getUserAgent());
458    fields.put(ELF_CS_USERNAME, ri.getAuthUser());
459    fields.put(ELF_CS_VERSION, ri.getProtocol());
460    fields.put(ELF_S_IP, ri.getServerAddress());
461    fields.put(ELF_S_COMPUTERNAME, ri.getServerHost());
462    fields.put(ELF_S_PORT, ri.getServerPort());
463    fields.put(ELF_SC_STATUS, ri.getStatusCode());
464    fields.put(X_CONNECTION_ID, ri.getConnectionID());
465    fields.put(X_DATETIME, TimeThread.getUserDefinedTime(timeStampFormat));
466    fields.put(X_ETIME, ri.getTotalProcessingTime());
467
468    writeLogRecord(fields, logFormatFields);
469  }
470
471  private void writeLogRecord(Map<String, Object> fields,
472      List<String> fieldnames)
473  {
474    if (fieldnames == null)
475    {
476      return;
477    }
478    final StringBuilder sb = new StringBuilder(100);
479    for (String fieldname : fieldnames)
480    {
481      append(sb, fields.get(fieldname));
482    }
483    writer.writeRecord(sb.toString());
484  }
485
486  /**
487   * Appends the value to the string builder using the default separator if
488   * needed.
489   *
490   * @param sb
491   *          the StringBuilder where to append.
492   * @param value
493   *          the value to append.
494   */
495  private void append(final StringBuilder sb, Object value)
496  {
497    final char separator = '\t'; // as encouraged by the W3C working draft
498    if (sb.length() > 0)
499    {
500      sb.append(separator);
501    }
502
503    if (value != null)
504    {
505      String val = String.valueOf(value);
506      boolean useQuotes = val.contains(Character.toString(separator));
507      if (useQuotes)
508      {
509        sb.append('"').append(val.replaceAll("\"", "\"\"")).append('"');
510      }
511      else
512      {
513        sb.append(val);
514      }
515    }
516    else
517    {
518      sb.append('-');
519    }
520  }
521
522}