001/* 002 * Copyright 2018-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2018-2020 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2018-2020 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.unboundidds.logs; 037 038 039 040import java.io.ByteArrayInputStream; 041import java.io.Serializable; 042import java.text.ParseException; 043import java.text.SimpleDateFormat; 044import java.util.ArrayList; 045import java.util.Collections; 046import java.util.Date; 047import java.util.LinkedHashMap; 048import java.util.List; 049import java.util.Map; 050import java.util.StringTokenizer; 051import java.util.regex.Pattern; 052 053import com.unboundid.ldap.sdk.ChangeType; 054import com.unboundid.ldap.sdk.Entry; 055import com.unboundid.ldap.sdk.ReadOnlyEntry; 056import com.unboundid.ldap.sdk.persist.PersistUtils; 057import com.unboundid.ldap.sdk.unboundidds.controls. 058 IntermediateClientRequestControl; 059import com.unboundid.ldap.sdk.unboundidds.controls. 060 IntermediateClientRequestValue; 061import com.unboundid.ldap.sdk.unboundidds.controls. 062 OperationPurposeRequestControl; 063import com.unboundid.ldif.LDIFChangeRecord; 064import com.unboundid.ldif.LDIFReader; 065import com.unboundid.util.ByteStringBuffer; 066import com.unboundid.util.Debug; 067import com.unboundid.util.NotExtensible; 068import com.unboundid.util.StaticUtils; 069import com.unboundid.util.ThreadSafety; 070import com.unboundid.util.ThreadSafetyLevel; 071import com.unboundid.util.json.JSONObject; 072import com.unboundid.util.json.JSONObjectReader; 073 074import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*; 075 076 077 078/** 079 * This class provides a data structure that holds information about a log 080 * message that may appear in the Directory Server audit log. 081 * <BR> 082 * <BLOCKQUOTE> 083 * <B>NOTE:</B> This class, and other classes within the 084 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 085 * supported for use against Ping Identity, UnboundID, and 086 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 087 * for proprietary functionality or for external specifications that are not 088 * considered stable or mature enough to be guaranteed to work in an 089 * interoperable way with other types of LDAP servers. 090 * </BLOCKQUOTE> 091 */ 092@NotExtensible() 093@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE) 094public abstract class AuditLogMessage 095 implements Serializable 096{ 097 /** 098 * A regular expression that can be used to determine if a line looks like an 099 * audit log message header. 100 */ 101 private static final Pattern STARTS_WITH_TIMESTAMP_PATTERN = Pattern.compile( 102 "^# " + // Starts with an octothorpe and a space. 103 "\\d\\d" + // Two digits for the day of the month. 104 "\\/" + // A slash to separate the day from the month. 105 "\\w\\w\\w" + // Three characters for the month. 106 "\\/" + // A slash to separate the month from the year. 107 "\\d\\d\\d\\d" + // Four digits for the year. 108 ":" + // A colon to separate the year from the hour. 109 "\\d\\d" + // Two digits for the hour. 110 ":" + // A colon to separate the hour from the minute. 111 "\\d\\d" + // Two digits for the minute. 112 ":" + // A colon to separate the minute from the second. 113 "\\d\\d" + // Two digits for the second. 114 ".*$"); // The rest of the line. 115 116 117 118 /** 119 * The format string that will be used for log message timestamps 120 * with second-level precision enabled. 121 */ 122 private static final String TIMESTAMP_SEC_FORMAT = "dd/MMM/yyyy:HH:mm:ss Z"; 123 124 125 126 /** 127 * The format string that will be used for log message timestamps 128 * with second-level precision enabled. 129 */ 130 private static final String TIMESTAMP_MS_FORMAT = 131 "dd/MMM/yyyy:HH:mm:ss.SSS Z"; 132 133 134 135 /** 136 * A set of thread-local date formatters that can be used to parse timestamps 137 * with second-level precision. 138 */ 139 private static final ThreadLocal<SimpleDateFormat> 140 TIMESTAMP_SEC_FORMAT_PARSERS = new ThreadLocal<>(); 141 142 143 144 /** 145 * A set of thread-local date formatters that can be used to parse timestamps 146 * with millisecond-level precision. 147 */ 148 private static final ThreadLocal<SimpleDateFormat> 149 TIMESTAMP_MS_FORMAT_PARSERS = new ThreadLocal<>(); 150 151 152 153 /** 154 * The serial version UID for this serializable class. 155 */ 156 private static final long serialVersionUID = 1817887018590767411L; 157 158 159 160 // Indicates whether the associated operation was processed using a worker 161 // thread from the administrative thread pool. 162 private final Boolean usingAdminSessionWorkerThread; 163 164 // The timestamp for this audit log message. 165 private final Date timestamp; 166 167 // The intermediate client request control for this audit log message. 168 private final IntermediateClientRequestControl 169 intermediateClientRequestControl; 170 171 // The lines that comprise the complete audit log message. 172 private final List<String> logMessageLines; 173 174 // The request control OIDs for this audit log message. 175 private final List<String> requestControlOIDs; 176 177 // The connection ID for this audit log message. 178 private final Long connectionID; 179 180 // The operation ID for this audit log message. 181 private final Long operationID; 182 183 // The thread ID for this audit log message. 184 private final Long threadID; 185 186 // The connection ID for the operation that triggered this audit log message. 187 private final Long triggeredByConnectionID; 188 189 // The operation ID for the operation that triggered this audit log message. 190 private final Long triggeredByOperationID; 191 192 // The map of named fields contained in this audit log message. 193 private final Map<String, String> namedValues; 194 195 // The operation purpose request control for this audit log message. 196 private final OperationPurposeRequestControl operationPurposeRequestControl; 197 198 // The DN of the alternate authorization identity for this audit log message. 199 private final String alternateAuthorizationDN; 200 201 // The line that comprises the header for this log message, including the 202 // opening comment sequence. 203 private final String commentedHeaderLine; 204 205 // The server instance name for this audit log message. 206 private final String instanceName; 207 208 // The origin for this audit log message. 209 private final String origin; 210 211 // The replication change ID for the audit log message. 212 private final String replicationChangeID; 213 214 // The requester DN for this audit log message. 215 private final String requesterDN; 216 217 // The requester IP address for this audit log message. 218 private final String requesterIP; 219 220 // The product name for this audit log message. 221 private final String productName; 222 223 // The startup ID for this audit log message. 224 private final String startupID; 225 226 // The transaction ID for this audit log message. 227 private final String transactionID; 228 229 // The line that comprises the header for this log message, without the 230 // opening comment sequence. 231 private final String uncommentedHeaderLine; 232 233 234 235 /** 236 * Creates a new audit log message from the provided set of lines. 237 * 238 * @param logMessageLines The lines that comprise the log message. It must 239 * not be {@code null} or empty, and it must not 240 * contain any blank lines, although it may contain 241 * comments. In fact, it must contain at least one 242 * comment line that appears before any non-comment 243 * lines (but possibly after other comment lines) 244 * that serves as the message header. 245 * 246 * @throws AuditLogException If a problem is encountered while processing 247 * the provided list of log message lines. 248 */ 249 protected AuditLogMessage(final List<String> logMessageLines) 250 throws AuditLogException 251 { 252 if (logMessageLines == null) 253 { 254 throw new AuditLogException(Collections.<String>emptyList(), 255 ERR_AUDIT_LOG_MESSAGE_LIST_NULL.get()); 256 } 257 258 if (logMessageLines.isEmpty()) 259 { 260 throw new AuditLogException(Collections.<String>emptyList(), 261 ERR_AUDIT_LOG_MESSAGE_LIST_EMPTY.get()); 262 } 263 264 for (final String line : logMessageLines) 265 { 266 if ((line == null) || line.isEmpty()) 267 { 268 throw new AuditLogException(logMessageLines, 269 ERR_AUDIT_LOG_MESSAGE_LIST_CONTAINS_EMPTY_LINE.get()); 270 } 271 } 272 273 this.logMessageLines = Collections.unmodifiableList( 274 new ArrayList<>(logMessageLines)); 275 276 277 // Iterate through the message lines until we find the commented header line 278 // (which is good) or until we find a non-comment line (which is bad because 279 // it means there is no header and we can't handle that). 280 String headerLine = null; 281 for (final String line : logMessageLines) 282 { 283 if (STARTS_WITH_TIMESTAMP_PATTERN.matcher(line).matches()) 284 { 285 headerLine = line; 286 break; 287 } 288 } 289 290 if (headerLine == null) 291 { 292 throw new AuditLogException(logMessageLines, 293 ERR_AUDIT_LOG_MESSAGE_LIST_DOES_NOT_START_WITH_COMMENT.get()); 294 } 295 296 commentedHeaderLine = headerLine; 297 uncommentedHeaderLine = commentedHeaderLine.substring(2); 298 299 final LinkedHashMap<String,String> nameValuePairs = 300 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 301 timestamp = parseHeaderLine(logMessageLines, uncommentedHeaderLine, 302 nameValuePairs); 303 namedValues = Collections.unmodifiableMap(nameValuePairs); 304 305 connectionID = getNamedValueAsLong("conn", namedValues); 306 operationID = getNamedValueAsLong("op", namedValues); 307 threadID = getNamedValueAsLong("threadID", namedValues); 308 triggeredByConnectionID = 309 getNamedValueAsLong("triggeredByConn", namedValues); 310 triggeredByOperationID = getNamedValueAsLong("triggeredByOp", namedValues); 311 alternateAuthorizationDN = namedValues.get("authzDN"); 312 instanceName = namedValues.get("instanceName"); 313 origin = namedValues.get("origin"); 314 replicationChangeID = namedValues.get("replicationChangeID"); 315 requesterDN = namedValues.get("requesterDN"); 316 requesterIP = namedValues.get("clientIP"); 317 productName = namedValues.get("productName"); 318 startupID = namedValues.get("startupID"); 319 transactionID = namedValues.get("txnID"); 320 usingAdminSessionWorkerThread = 321 getNamedValueAsBoolean("usingAdminSessionWorkerThread", namedValues); 322 operationPurposeRequestControl = 323 decodeOperationPurposeRequestControl(namedValues); 324 intermediateClientRequestControl = 325 decodeIntermediateClientRequestControl(namedValues); 326 327 final String oidsString = namedValues.get("requestControlOIDs"); 328 if (oidsString == null) 329 { 330 requestControlOIDs = null; 331 } 332 else 333 { 334 final ArrayList<String> oidList = new ArrayList<>(10); 335 final StringTokenizer tokenizer = new StringTokenizer(oidsString, ","); 336 while (tokenizer.hasMoreTokens()) 337 { 338 oidList.add(tokenizer.nextToken()); 339 } 340 requestControlOIDs = Collections.unmodifiableList(oidList); 341 } 342 } 343 344 345 346 /** 347 * Parses the provided header line for this audit log message. 348 * 349 * @param logMessageLines The lines that comprise the log message. It 350 * must not be {@code null} or empty. 351 * @param uncommentedHeaderLine The uncommented representation of the header 352 * line. It must not be {@code null}. 353 * @param nameValuePairs A map into which the parsed name-value pairs 354 * may be placed. It must not be {@code null} 355 * and must be updatable. 356 * 357 * @return The date parsed from the header line. The name-value pairs parsed 358 * from the header line will be added to the {@code nameValuePairs} 359 * map. 360 * 361 * @throws AuditLogException If the line cannot be parsed as a valid header. 362 */ 363 private static Date parseHeaderLine(final List<String> logMessageLines, 364 final String uncommentedHeaderLine, 365 final Map<String,String> nameValuePairs) 366 throws AuditLogException 367 { 368 final byte[] uncommentedHeaderBytes = 369 StaticUtils.getBytes(uncommentedHeaderLine); 370 371 final ByteStringBuffer buffer = 372 new ByteStringBuffer(uncommentedHeaderBytes.length); 373 374 final ByteArrayInputStream inputStream = 375 new ByteArrayInputStream(uncommentedHeaderBytes); 376 final Date timestamp = readTimestamp(logMessageLines, inputStream, buffer); 377 while (true) 378 { 379 if (! readNameValuePair(logMessageLines, inputStream, nameValuePairs, 380 buffer)) 381 { 382 break; 383 } 384 } 385 386 return timestamp; 387 } 388 389 390 391 /** 392 * Reads the timestamp from the provided input stream and parses it using one 393 * of the expected formats. 394 * 395 * @param logMessageLines The lines that comprise the log message. It must 396 * not be {@code null} or empty. 397 * @param inputStream The input stream from which to read the timestamp. 398 * It must not be {@code null}. 399 * @param buffer A buffer that may be used to hold temporary data 400 * for reading. It must not be {@code null} and it 401 * must be empty. 402 * 403 * @return The parsed timestamp. 404 * 405 * @throws AuditLogException If the provided string cannot be parsed as a 406 * timestamp. 407 */ 408 private static Date readTimestamp(final List<String> logMessageLines, 409 final ByteArrayInputStream inputStream, 410 final ByteStringBuffer buffer) 411 throws AuditLogException 412 { 413 while (true) 414 { 415 final int intRead = inputStream.read(); 416 if ((intRead < 0) || (intRead == ';')) 417 { 418 break; 419 } 420 421 buffer.append((byte) (intRead & 0xFF)); 422 } 423 424 SimpleDateFormat parser; 425 final String timestampString = buffer.toString().trim(); 426 if (timestampString.length() == 30) 427 { 428 parser = TIMESTAMP_MS_FORMAT_PARSERS.get(); 429 if (parser == null) 430 { 431 parser = new SimpleDateFormat(TIMESTAMP_MS_FORMAT); 432 parser.setLenient(false); 433 TIMESTAMP_MS_FORMAT_PARSERS.set(parser); 434 } 435 } 436 else if (timestampString.length() == 26) 437 { 438 parser = TIMESTAMP_SEC_FORMAT_PARSERS.get(); 439 if (parser == null) 440 { 441 parser = new SimpleDateFormat(TIMESTAMP_SEC_FORMAT); 442 parser.setLenient(false); 443 TIMESTAMP_SEC_FORMAT_PARSERS.set(parser); 444 } 445 } 446 else 447 { 448 throw new AuditLogException(logMessageLines, 449 ERR_AUDIT_LOG_MESSAGE_HEADER_MALFORMED_TIMESTAMP.get()); 450 } 451 452 try 453 { 454 return parser.parse(timestampString); 455 } 456 catch (final ParseException e) 457 { 458 Debug.debugException(e); 459 throw new AuditLogException(logMessageLines, 460 ERR_AUDIT_LOG_MESSAGE_HEADER_MALFORMED_TIMESTAMP.get(), e); 461 } 462 } 463 464 465 466 /** 467 * Reads a name-value pair from the provided buffer. 468 * 469 * @param logMessageLines The lines that comprise the log message. It must 470 * not be {@code null} or empty. 471 * @param inputStream The input stream from which to read the name-value 472 * pair. It must not be {@code null}. 473 * @param nameValuePairs A map to which the name-value pair should be 474 * added. 475 * @param buffer A buffer that may be used to hold temporary data 476 * for reading. It must not be {@code null}, but may 477 * not be empty and should be cleared before use. 478 * 479 * @return {@code true} if a name-value pair was read, or {@code false} if 480 * the end of the input stream was read without reading any more 481 * data. 482 * 483 * @throws AuditLogException If a problem is encountered while trying to 484 * read the name-value pair. 485 */ 486 private static boolean readNameValuePair(final List<String> logMessageLines, 487 final ByteArrayInputStream inputStream, 488 final Map<String,String> nameValuePairs, 489 final ByteStringBuffer buffer) 490 throws AuditLogException 491 { 492 // Read the property name. It will be followed by an equal sign to separate 493 // the name from the value. 494 buffer.clear(); 495 while (true) 496 { 497 final int intRead = inputStream.read(); 498 if (intRead < 0) 499 { 500 // We've hit the end of the input stream. This is okay if we haven't 501 // yet read any data. 502 if (buffer.isEmpty()) 503 { 504 return false; 505 } 506 else 507 { 508 throw new AuditLogException(logMessageLines, 509 ERR_AUDIT_LOG_MESSAGE_HEADER_ENDS_WITH_PROPERTY_NAME.get( 510 buffer.toString())); 511 } 512 } 513 else if (intRead == '=') 514 { 515 break; 516 } 517 else if (intRead != ' ') 518 { 519 buffer.append((byte) (intRead & 0xFF)); 520 } 521 } 522 523 final String name = buffer.toString(); 524 if (name.isEmpty()) 525 { 526 throw new AuditLogException(logMessageLines, 527 ERR_AUDIT_LOG_MESSAGE_HEADER_EMPTY_PROPERTY_NAME.get()); 528 } 529 530 531 // Read the property value. Start by peeking at the next byte in the 532 // input stream. If it's a space, then skip it and loop back to the next 533 // byte. If it's an opening curly brace ({), then read the value as a JSON 534 // object followed by a semicolon. If it's a double quote ("), then read 535 // the value as a quoted string followed by a semicolon. If it's anything 536 // else, then read the value as an unquoted string followed by a semicolon. 537 final String valueString; 538 while (true) 539 { 540 inputStream.mark(1); 541 final int intRead = inputStream.read(); 542 if (intRead < 0) 543 { 544 // We hit the end of the input stream after the equal sign. This is 545 // fine. We'll just use an empty value. 546 valueString = ""; 547 break; 548 } 549 else if (intRead == ' ') 550 { 551 continue; 552 } 553 else if (intRead == '{') 554 { 555 inputStream.reset(); 556 final JSONObject jsonObject = 557 readJSONObject(logMessageLines, name, inputStream); 558 valueString = jsonObject.toString(); 559 break; 560 } 561 else if (intRead == '"') 562 { 563 valueString = 564 readString(logMessageLines, name, true, inputStream, buffer); 565 break; 566 } 567 else if (intRead == ';') 568 { 569 valueString = ""; 570 break; 571 } 572 else 573 { 574 inputStream.reset(); 575 valueString = 576 readString(logMessageLines, name, false, inputStream, buffer); 577 break; 578 } 579 } 580 581 nameValuePairs.put(name, valueString); 582 return true; 583 } 584 585 586 587 /** 588 * Reads a JSON object from the provided input stream. 589 * 590 * @param logMessageLines The lines that comprise the log message. It must 591 * not be {@code null} or empty. 592 * @param propertyName The name of the property whose value is expected 593 * to be a JSON object. It must not be {@code null}. 594 * @param inputStream The input stream from which to read the JSON 595 * object. It must not be {@code null}. 596 * 597 * @return The JSON object that was read. 598 * 599 * @throws AuditLogException If a problem is encountered while trying to 600 * read the JSON object. 601 */ 602 private static JSONObject readJSONObject(final List<String> logMessageLines, 603 final String propertyName, 604 final ByteArrayInputStream inputStream) 605 throws AuditLogException 606 { 607 final JSONObject jsonObject; 608 try 609 { 610 final JSONObjectReader reader = new JSONObjectReader(inputStream, false); 611 jsonObject = reader.readObject(); 612 } 613 catch (final Exception e) 614 { 615 Debug.debugException(e); 616 throw new AuditLogException(logMessageLines, 617 ERR_AUDIT_LOG_MESSAGE_ERROR_READING_JSON_OBJECT.get(propertyName, 618 StaticUtils.getExceptionMessage(e)), 619 e); 620 } 621 622 readSpacesAndSemicolon(logMessageLines, propertyName, inputStream); 623 return jsonObject; 624 } 625 626 627 628 /** 629 * Reads a string from the provided input stream. It may optionally be 630 * treated as a quoted string, in which everything read up to an unescaped 631 * quote will be treated as part of the string, or an unquoted string, in 632 * which the first space or semicolon encountered will signal the end of the 633 * string. Any character prefixed by a backslash will be added to the string 634 * as-is (for example, a backslash followed by a quotation mark will cause the 635 * quotation mark to be part of the string rather than signalling the end of 636 * the quoted string). Any octothorpe (#) character must be followed by two 637 * hexadecimal digits that signify a single raw byte to add to the value. 638 * 639 * @param logMessageLines The lines that comprise the log message. It must 640 * not be {@code null} or empty. 641 * @param propertyName The name of the property with which the string 642 * value is associated. It must not be {@code null}. 643 * @param isQuoted Indicates whether to read a quoted string or an 644 * unquoted string. In the case of a a quoted 645 * string, the opening quote must have already been 646 * read. 647 * @param inputStream The input stream from which to read the string 648 * value. It must not be {@code null}. 649 * @param buffer A buffer that may be used while reading the 650 * string. It must not be {@code null}, but may not 651 * be empty and should be cleared before use. 652 * 653 * @return The string that was read. 654 * 655 * @throws AuditLogException If a problem is encountered while trying to 656 * read the string. 657 */ 658 private static String readString(final List<String> logMessageLines, 659 final String propertyName, 660 final boolean isQuoted, 661 final ByteArrayInputStream inputStream, 662 final ByteStringBuffer buffer) 663 throws AuditLogException 664 { 665 buffer.clear(); 666 667stringLoop: 668 while (true) 669 { 670 inputStream.mark(1); 671 final int intRead = inputStream.read(); 672 if (intRead < 0) 673 { 674 if (isQuoted) 675 { 676 throw new AuditLogException(logMessageLines, 677 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_CLOSING_QUOTE.get( 678 propertyName)); 679 } 680 else 681 { 682 return buffer.toString(); 683 } 684 } 685 686 switch (intRead) 687 { 688 case '\\': 689 final int literalCharacter = inputStream.read(); 690 if (literalCharacter < 0) 691 { 692 throw new AuditLogException(logMessageLines, 693 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_ESCAPED.get(propertyName)); 694 } 695 else 696 { 697 buffer.append((byte) (literalCharacter & 0xFF)); 698 } 699 break; 700 701 case '#': 702 int hexByte = 703 readHexDigit(logMessageLines, propertyName, inputStream); 704 hexByte = (hexByte << 4) | 705 readHexDigit(logMessageLines, propertyName, inputStream); 706 buffer.append((byte) (hexByte & 0xFF)); 707 break; 708 709 case '"': 710 if (isQuoted) 711 { 712 break stringLoop; 713 } 714 715 buffer.append('"'); 716 break; 717 718 case ' ': 719 if (! isQuoted) 720 { 721 break stringLoop; 722 } 723 724 buffer.append(' '); 725 break; 726 727 case ';': 728 if (! isQuoted) 729 { 730 inputStream.reset(); 731 break stringLoop; 732 } 733 734 buffer.append(';'); 735 break; 736 737 default: 738 buffer.append((byte) (intRead & 0xFF)); 739 break; 740 } 741 } 742 743 readSpacesAndSemicolon(logMessageLines, propertyName, inputStream); 744 return buffer.toString(); 745 } 746 747 748 749 /** 750 * Reads a single hexadecimal digit from the provided input stream and returns 751 * its integer value. 752 * 753 * @param logMessageLines The lines that comprise the log message. It must 754 * not be {@code null} or empty. 755 * @param propertyName The name of the property with which the string 756 * value is associated. It must not be {@code null}. 757 * @param inputStream The input stream from which to read the string 758 * value. It must not be {@code null}. 759 * 760 * @return The integer value of the hexadecimal digit that was read. 761 * 762 * @throws AuditLogException If the end of the input stream was reached 763 * before the byte could be read, or if the byte 764 * that was read did not represent a hexadecimal 765 * digit. 766 */ 767 private static int readHexDigit(final List<String> logMessageLines, 768 final String propertyName, 769 final ByteArrayInputStream inputStream) 770 throws AuditLogException 771 { 772 final int byteRead = inputStream.read(); 773 if (byteRead < 0) 774 { 775 throw new AuditLogException(logMessageLines, 776 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_HEX.get(propertyName)); 777 } 778 779 switch (byteRead) 780 { 781 case '0': 782 return 0; 783 case '1': 784 return 1; 785 case '2': 786 return 2; 787 case '3': 788 return 3; 789 case '4': 790 return 4; 791 case '5': 792 return 5; 793 case '6': 794 return 6; 795 case '7': 796 return 7; 797 case '8': 798 return 8; 799 case '9': 800 return 9; 801 case 'a': 802 case 'A': 803 return 10; 804 case 'b': 805 case 'B': 806 return 11; 807 case 'c': 808 case 'C': 809 return 12; 810 case 'd': 811 case 'D': 812 return 13; 813 case 'e': 814 case 'E': 815 return 14; 816 case 'f': 817 case 'F': 818 return 15; 819 default: 820 throw new AuditLogException(logMessageLines, 821 ERR_AUDIT_LOG_MESSAGE_INVALID_HEX_DIGIT.get(propertyName)); 822 } 823 } 824 825 826 827 /** 828 * Reads zero or more spaces and the following semicolon from the provided 829 * input stream. It is also acceptable to encounter the end of the stream. 830 * 831 * @param logMessageLines The lines that comprise the log message. It must 832 * not be {@code null} or empty. 833 * @param propertyName The name of the property that was just read. It 834 * must not be {@code null}. 835 * @param inputStream The input stream from which to read the spaces and 836 * semicolon. It must not be {@code null}. 837 * 838 * @throws AuditLogException If any byte is encountered that is not a space 839 * or a semicolon. 840 */ 841 private static void readSpacesAndSemicolon(final List<String> logMessageLines, 842 final String propertyName, 843 final ByteArrayInputStream inputStream) 844 throws AuditLogException 845 { 846 while (true) 847 { 848 final int intRead = inputStream.read(); 849 if ((intRead < 0) || (intRead == ';')) 850 { 851 return; 852 } 853 else if (intRead != ' ') 854 { 855 throw new AuditLogException(logMessageLines, 856 ERR_AUDIT_LOG_MESSAGE_UNEXPECTED_CHAR_AFTER_PROPERTY.get( 857 String.valueOf((char) intRead), propertyName)); 858 } 859 } 860 } 861 862 863 864 /** 865 * Retrieves the value of the header property with the given name as a 866 * {@code Boolean} object. 867 * 868 * @param name The name of the property to retrieve. It must not 869 * be {@code null}, and it will be treated in a 870 * case-sensitive manner. 871 * @param nameValuePairs The map containing the header properties as 872 * name-value pairs. It must not be {@code null}. 873 * 874 * @return The value of the specified property as a {@code Boolean}, or 875 * {@code null} if the property is not defined or if it cannot be 876 * parsed as a {@code Boolean}. 877 */ 878 protected static Boolean getNamedValueAsBoolean(final String name, 879 final Map<String,String> nameValuePairs) 880 { 881 final String valueString = nameValuePairs.get(name); 882 if (valueString == null) 883 { 884 return null; 885 } 886 887 final String lowerValueString = StaticUtils.toLowerCase(valueString); 888 if (lowerValueString.equals("true") || 889 lowerValueString.equals("t") || 890 lowerValueString.equals("yes") || 891 lowerValueString.equals("y") || 892 lowerValueString.equals("on") || 893 lowerValueString.equals("1")) 894 { 895 return Boolean.TRUE; 896 } 897 else if (lowerValueString.equals("false") || 898 lowerValueString.equals("f") || 899 lowerValueString.equals("no") || 900 lowerValueString.equals("n") || 901 lowerValueString.equals("off") || 902 lowerValueString.equals("0")) 903 { 904 return Boolean.FALSE; 905 } 906 else 907 { 908 return null; 909 } 910 } 911 912 913 914 /** 915 * Retrieves the value of the header property with the given name as a 916 * {@code Long} object. 917 * 918 * @param name The name of the property to retrieve. It must not 919 * be {@code null}, and it will be treated in a 920 * case-sensitive manner. 921 * @param nameValuePairs The map containing the header properties as 922 * name-value pairs. It must not be {@code null}. 923 * 924 * @return The value of the specified property as a {@code Long}, or 925 * {@code null} if the property is not defined or if it cannot be 926 * parsed as a {@code Long}. 927 */ 928 protected static Long getNamedValueAsLong(final String name, 929 final Map<String,String> nameValuePairs) 930 { 931 final String valueString = nameValuePairs.get(name); 932 if (valueString == null) 933 { 934 return null; 935 } 936 937 try 938 { 939 return Long.parseLong(valueString); 940 } 941 catch (final Exception e) 942 { 943 Debug.debugException(e); 944 return null; 945 } 946 } 947 948 949 950 /** 951 * Decodes an entry (or list of attributes) from the commented header 952 * contained in the log message lines. 953 * 954 * @param header The header line that appears before the encoded 955 * entry. 956 * @param logMessageLines The lines that comprise the audit log message. 957 * @param entryDN The DN to use for the entry that is read. It 958 * should be {@code null} if the commented entry 959 * includes a DN, and non-{@code null} if the 960 * commented entry does not include a DN. 961 * 962 * @return The entry that was decoded from the commented header, or 963 * {@code null} if it is not included in the header or if it cannot 964 * be decoded. If the commented entry does not include a DN, then 965 * the DN of the entry returned will be the null DN. 966 */ 967 protected static ReadOnlyEntry decodeCommentedEntry(final String header, 968 final List<String> logMessageLines, 969 final String entryDN) 970 { 971 List<String> ldifLines = null; 972 StringBuilder invalidLDAPNameReason = null; 973 for (final String line : logMessageLines) 974 { 975 final String uncommentedLine; 976 if (line.startsWith("# ")) 977 { 978 uncommentedLine = line.substring(2); 979 } 980 else 981 { 982 break; 983 } 984 985 if (ldifLines == null) 986 { 987 if (uncommentedLine.equalsIgnoreCase(header)) 988 { 989 ldifLines = new ArrayList<>(logMessageLines.size()); 990 if (entryDN != null) 991 { 992 ldifLines.add("dn: " + entryDN); 993 } 994 } 995 } 996 else 997 { 998 final int colonPos = uncommentedLine.indexOf(':'); 999 if (colonPos <= 0) 1000 { 1001 break; 1002 } 1003 1004 if (invalidLDAPNameReason == null) 1005 { 1006 invalidLDAPNameReason = new StringBuilder(); 1007 } 1008 1009 final String potentialAttributeName = 1010 uncommentedLine.substring(0, colonPos); 1011 if (PersistUtils.isValidLDAPName(potentialAttributeName, 1012 invalidLDAPNameReason)) 1013 { 1014 ldifLines.add(uncommentedLine); 1015 } 1016 else 1017 { 1018 break; 1019 } 1020 } 1021 } 1022 1023 if (ldifLines == null) 1024 { 1025 return null; 1026 } 1027 1028 try 1029 { 1030 final String[] ldifLineArray = ldifLines.toArray(StaticUtils.NO_STRINGS); 1031 final Entry ldifEntry = LDIFReader.decodeEntry(ldifLineArray); 1032 return new ReadOnlyEntry(ldifEntry); 1033 } 1034 catch (final Exception e) 1035 { 1036 Debug.debugException(e); 1037 return null; 1038 } 1039 } 1040 1041 1042 1043 /** 1044 * Decodes the operation purpose request control, if any, from the provided 1045 * set of name-value pairs. 1046 * 1047 * @param nameValuePairs The map containing the header properties as 1048 * name-value pairs. It must not be {@code null}. 1049 * 1050 * @return The operation purpose request control retrieved and decoded from 1051 * the provided set of name-value pairs, or {@code null} if no 1052 * valid operation purpose request control was included. 1053 */ 1054 private static OperationPurposeRequestControl 1055 decodeOperationPurposeRequestControl( 1056 final Map<String,String> nameValuePairs) 1057 { 1058 final String valueString = nameValuePairs.get("operationPurpose"); 1059 if (valueString == null) 1060 { 1061 return null; 1062 } 1063 1064 try 1065 { 1066 final JSONObject o = new JSONObject(valueString); 1067 1068 final String applicationName = o.getFieldAsString("applicationName"); 1069 final String applicationVersion = 1070 o.getFieldAsString("applicationVersion"); 1071 final String codeLocation = o.getFieldAsString("codeLocation"); 1072 final String requestPurpose = o.getFieldAsString("requestPurpose"); 1073 1074 return new OperationPurposeRequestControl(false, applicationName, 1075 applicationVersion, codeLocation, requestPurpose); 1076 } 1077 catch (final Exception e) 1078 { 1079 Debug.debugException(e); 1080 return null; 1081 } 1082 } 1083 1084 1085 1086 /** 1087 * Decodes the intermediate client request control, if any, from the provided 1088 * set of name-value pairs. 1089 * 1090 * @param nameValuePairs The map containing the header properties as 1091 * name-value pairs. It must not be {@code null}. 1092 * 1093 * @return The intermediate client request control retrieved and decoded from 1094 * the provided set of name-value pairs, or {@code null} if no 1095 * valid operation purpose request control was included. 1096 */ 1097 private static IntermediateClientRequestControl 1098 decodeIntermediateClientRequestControl( 1099 final Map<String,String> nameValuePairs) 1100 { 1101 final String valueString = 1102 nameValuePairs.get("intermediateClientRequestControl"); 1103 if (valueString == null) 1104 { 1105 return null; 1106 } 1107 1108 try 1109 { 1110 final JSONObject o = new JSONObject(valueString); 1111 return new IntermediateClientRequestControl( 1112 decodeIntermediateClientRequestValue(o)); 1113 } 1114 catch (final Exception e) 1115 { 1116 Debug.debugException(e); 1117 return null; 1118 } 1119 } 1120 1121 1122 1123 /** 1124 * decodes the provided JSON object as an intermediate client request control 1125 * value. 1126 * 1127 * @param o The JSON object to be decoded. It must not be {@code null}. 1128 * 1129 * @return The intermediate client request control value decoded from the 1130 * provided JSON object. 1131 */ 1132 private static IntermediateClientRequestValue 1133 decodeIntermediateClientRequestValue(final JSONObject o) 1134 { 1135 if (o == null) 1136 { 1137 return null; 1138 } 1139 1140 final String clientIdentity = o.getFieldAsString("clientIdentity"); 1141 final String downstreamClientAddress = 1142 o.getFieldAsString("downstreamClientAddress"); 1143 final Boolean downstreamClientSecure = 1144 o.getFieldAsBoolean("downstreamClientSecure"); 1145 final String clientName = o.getFieldAsString("clientName"); 1146 final String clientSessionID = o.getFieldAsString("clientSessionID"); 1147 final String clientRequestID = o.getFieldAsString("clientRequestID"); 1148 final IntermediateClientRequestValue downstreamRequest = 1149 decodeIntermediateClientRequestValue( 1150 o.getFieldAsObject("downstreamRequest")); 1151 1152 return new IntermediateClientRequestValue(downstreamRequest, 1153 downstreamClientAddress, downstreamClientSecure, clientIdentity, 1154 clientName, clientSessionID, clientRequestID); 1155 } 1156 1157 1158 1159 /** 1160 * Retrieves the lines that comprise the complete audit log message. 1161 * 1162 * @return The lines that comprise the complete audit log message. 1163 */ 1164 public final List<String> getLogMessageLines() 1165 { 1166 return logMessageLines; 1167 } 1168 1169 1170 1171 /** 1172 * Retrieves the line that comprises the header for this log message, 1173 * including the leading octothorpe (#) and space that make it a comment. 1174 * 1175 * @return The line that comprises the header for this log message, including 1176 * the leading octothorpe (#) and space that make it a comment. 1177 */ 1178 public final String getCommentedHeaderLine() 1179 { 1180 return commentedHeaderLine; 1181 } 1182 1183 1184 1185 /** 1186 * Retrieves the line that comprises the header for this log message, without 1187 * the leading octothorpe (#) and space that make it a comment. 1188 * 1189 * @return The line that comprises the header for this log message, without 1190 * the leading octothorpe (#) and space that make it a comment. 1191 */ 1192 public final String getUncommentedHeaderLine() 1193 { 1194 return uncommentedHeaderLine; 1195 } 1196 1197 1198 1199 /** 1200 * Retrieves the timestamp for this audit log message. 1201 * 1202 * @return The timestamp for this audit log message. 1203 */ 1204 public final Date getTimestamp() 1205 { 1206 return timestamp; 1207 } 1208 1209 1210 1211 /** 1212 * Retrieves a map of the name-value pairs contained in the header for this 1213 * log message. 1214 * 1215 * @return A map of the name-value pairs contained in the header for this log 1216 * message. 1217 */ 1218 public final Map<String,String> getHeaderNamedValues() 1219 { 1220 return namedValues; 1221 } 1222 1223 1224 1225 /** 1226 * Retrieves the server product name for this audit log message, if available. 1227 * 1228 * @return The server product name for this audit log message, or 1229 * {@code null} if it is not available. 1230 */ 1231 public final String getProductName() 1232 { 1233 return productName; 1234 } 1235 1236 1237 1238 /** 1239 * Retrieves the server instance name for this audit log message, if 1240 * available. 1241 * 1242 * @return The server instance name for this audit log message, or 1243 * {@code null} if it is not available. 1244 */ 1245 public final String getInstanceName() 1246 { 1247 return instanceName; 1248 } 1249 1250 1251 1252 /** 1253 * Retrieves the unique identifier generated when the server was started, if 1254 * available. 1255 * 1256 * @return The unique identifier generated when the server was started, or 1257 * {@code null} if it is not available. 1258 */ 1259 public final String getStartupID() 1260 { 1261 return startupID; 1262 } 1263 1264 1265 1266 /** 1267 * Retrieves the identifier for the server thread that processed the change, 1268 * if available. 1269 * 1270 * @return The identifier for the server thread that processed the change, or 1271 * {@code null} if it is not available. 1272 */ 1273 public final Long getThreadID() 1274 { 1275 return threadID; 1276 } 1277 1278 1279 1280 /** 1281 * Retrieves the DN of the user that requested the change, if available. 1282 * 1283 * @return The DN of the user that requested the change, or {@code null} if 1284 * it is not available. 1285 */ 1286 public final String getRequesterDN() 1287 { 1288 return requesterDN; 1289 } 1290 1291 1292 1293 /** 1294 * Retrieves the IP address of the client that requested the change, if 1295 * available. 1296 * 1297 * @return The IP address of the client that requested the change, or 1298 * {@code null} if it is not available. 1299 */ 1300 public final String getRequesterIPAddress() 1301 { 1302 return requesterIP; 1303 } 1304 1305 1306 1307 /** 1308 * Retrieves the connection ID for the connection on which the change was 1309 * requested, if available. 1310 * 1311 * @return The connection ID for the connection on which the change was 1312 * requested, or {@code null} if it is not available. 1313 */ 1314 public final Long getConnectionID() 1315 { 1316 return connectionID; 1317 } 1318 1319 1320 1321 /** 1322 * Retrieves the connection ID for the connection on which the change was 1323 * requested, if available. 1324 * 1325 * @return The connection ID for the connection on which the change was 1326 * requested, or {@code null} if it is not available. 1327 */ 1328 public final Long getOperationID() 1329 { 1330 return operationID; 1331 } 1332 1333 1334 1335 /** 1336 * Retrieves the connection ID for the external operation that triggered the 1337 * internal operation with which this audit log message is associated, if 1338 * available. 1339 * 1340 * @return The connection ID for the external operation that triggered the 1341 * internal operation with which this audit log message is 1342 * associated, or {@code null} if it is not available. 1343 */ 1344 public final Long getTriggeredByConnectionID() 1345 { 1346 return triggeredByConnectionID; 1347 } 1348 1349 1350 1351 /** 1352 * Retrieves the operation ID for the external operation that triggered the 1353 * internal operation with which this audit log message is associated, if 1354 * available. 1355 * 1356 * @return The operation ID for the external operation that triggered the 1357 * internal operation with which this audit log message is 1358 * associated, or {@code null} if it is not available. 1359 */ 1360 public final Long getTriggeredByOperationID() 1361 { 1362 return triggeredByOperationID; 1363 } 1364 1365 1366 1367 /** 1368 * Retrieves the replication change ID for this audit log message, if 1369 * available. 1370 * 1371 * @return The replication change ID for this audit log message, or 1372 * {@code null} if it is not available. 1373 */ 1374 public final String getReplicationChangeID() 1375 { 1376 return replicationChangeID; 1377 } 1378 1379 1380 1381 /** 1382 * Retrieves the alternate authorization DN for this audit log message, if 1383 * available. 1384 * 1385 * @return The alternate authorization DN for this audit log message, or 1386 * {@code null} if it is not available. 1387 */ 1388 public final String getAlternateAuthorizationDN() 1389 { 1390 return alternateAuthorizationDN; 1391 } 1392 1393 1394 1395 /** 1396 * Retrieves the transaction ID for this audit log message, if available. 1397 * 1398 * @return The transaction ID for this audit log message, or {@code null} if 1399 * it is not available. 1400 */ 1401 public final String getTransactionID() 1402 { 1403 return transactionID; 1404 } 1405 1406 1407 1408 /** 1409 * Retrieves the origin for this audit log message, if available. 1410 * 1411 * @return The origin for this audit log message, or {@code null} if it is 1412 * not available. 1413 */ 1414 public final String getOrigin() 1415 { 1416 return origin; 1417 } 1418 1419 1420 1421 /** 1422 * Retrieves the value of the flag indicating whether the associated operation 1423 * was processed using an administrative session worker thread, if available. 1424 * 1425 * @return {@code Boolean.TRUE} if it is known that the associated operation 1426 * was processed using an administrative session worker thread, 1427 * {@code Boolean.FALSE} if it is known that the associated operation 1428 * was not processed using an administrative session worker thread, 1429 * or {@code null} if it is not available. 1430 */ 1431 public final Boolean getUsingAdminSessionWorkerThread() 1432 { 1433 return usingAdminSessionWorkerThread; 1434 } 1435 1436 1437 1438 /** 1439 * Retrieves a list of the OIDs of the request controls included in the 1440 * operation request, if available. 1441 * 1442 * @return A list of the OIDs of the request controls included in the 1443 * operation, an empty list if it is known that there were no request 1444 * controls, or {@code null} if it is not available. 1445 */ 1446 public final List<String> getRequestControlOIDs() 1447 { 1448 return requestControlOIDs; 1449 } 1450 1451 1452 1453 /** 1454 * Retrieves an operation purpose request control with information about the 1455 * purpose for the associated operation, if available. 1456 * 1457 * @return An operation purpose request control with information about the 1458 * purpose for the associated operation, or {@code null} if it is not 1459 * available. 1460 */ 1461 public final OperationPurposeRequestControl 1462 getOperationPurposeRequestControl() 1463 { 1464 return operationPurposeRequestControl; 1465 } 1466 1467 1468 1469 /** 1470 * Retrieves an intermediate client request control with information about the 1471 * downstream processing for the associated operation, if available. 1472 * 1473 * @return An intermediate client request control with information about the 1474 * downstream processing for the associated operation, or 1475 * {@code null} if it is not available. 1476 */ 1477 public final IntermediateClientRequestControl 1478 getIntermediateClientRequestControl() 1479 { 1480 return intermediateClientRequestControl; 1481 } 1482 1483 1484 1485 /** 1486 * Retrieves the DN of the entry targeted by the associated operation. 1487 * 1488 * @return The DN of the entry targeted by the associated operation. 1489 */ 1490 public abstract String getDN(); 1491 1492 1493 1494 /** 1495 * Retrieves the change type for this audit log message. 1496 * 1497 * @return The change type for this audit log message. 1498 */ 1499 public abstract ChangeType getChangeType(); 1500 1501 1502 1503 /** 1504 * Retrieves an LDIF change record that encapsulates the change represented by 1505 * this audit log message. 1506 * 1507 * @return An LDIF change record that encapsulates the change represented by 1508 * this audit log message. 1509 */ 1510 public abstract LDIFChangeRecord getChangeRecord(); 1511 1512 1513 1514 /** 1515 * Indicates whether it is possible to use the 1516 * {@link #getRevertChangeRecords()} method to obtain a list of LDIF change 1517 * records that can be used to revert the changes described by this audit log 1518 * message. 1519 * 1520 * @return {@code true} if it is possible to use the 1521 * {@link #getRevertChangeRecords()} method to obtain a list of LDIF 1522 * change records that can be used to revert the changes described 1523 * by this audit log message, or {@code false} if not. 1524 */ 1525 public abstract boolean isRevertible(); 1526 1527 1528 1529 /** 1530 * Retrieves a list of the change records that can be used to revert the 1531 * changes described by this audit log message. 1532 * 1533 * @return A list of the change records that can be used to revert the 1534 * changes described by this audit log message. 1535 * 1536 * @throws AuditLogException If this audit log message cannot be reverted. 1537 */ 1538 public abstract List<LDIFChangeRecord> getRevertChangeRecords() 1539 throws AuditLogException; 1540 1541 1542 1543 /** 1544 * Retrieves a single-line string representation of this audit log message. 1545 * It will start with the string returned by 1546 * {@link #getUncommentedHeaderLine()}, but will also contain additional 1547 * name-value pairs that are pertinent to the type of operation that the audit 1548 * log message represents. 1549 * 1550 * @return A string representation of this audit log message. 1551 */ 1552 @Override() 1553 public final String toString() 1554 { 1555 final StringBuilder buffer = new StringBuilder(); 1556 toString(buffer); 1557 return buffer.toString(); 1558 } 1559 1560 1561 1562 /** 1563 * Appends a single-line string representation of this audit log message to 1564 * the provided buffer. The message will start with the string returned by 1565 * {@link #getUncommentedHeaderLine()}, but will also contain additional 1566 * name-value pairs that are pertinent to the type of operation that the audit 1567 * log message represents. 1568 * 1569 * @param buffer The buffer to which the information should be appended. 1570 */ 1571 public abstract void toString(StringBuilder buffer); 1572 1573 1574 1575 /** 1576 * Retrieves a multi-line string representation of this audit log message. It 1577 * will simply be a concatenation of all of the lines that comprise the 1578 * complete log message, with line breaks between them. 1579 * 1580 * @return A multi-line string representation of this audit log message. 1581 */ 1582 public final String toMultiLineString() 1583 { 1584 return StaticUtils.concatenateStrings(null, null, StaticUtils.EOL, null, 1585 null, logMessageLines); 1586 } 1587}