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