001/* 002 * Copyright 2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2018 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 = new LinkedHashMap<>(10); 285 timestamp = parseHeaderLine(logMessageLines, uncommentedHeaderLine, 286 nameValuePairs); 287 namedValues = Collections.unmodifiableMap(nameValuePairs); 288 289 connectionID = getNamedValueAsLong("conn", namedValues); 290 operationID = getNamedValueAsLong("op", namedValues); 291 threadID = getNamedValueAsLong("threadID", namedValues); 292 triggeredByConnectionID = 293 getNamedValueAsLong("triggeredByConn", namedValues); 294 triggeredByOperationID = getNamedValueAsLong("triggeredByOp", namedValues); 295 alternateAuthorizationDN = namedValues.get("authzDN"); 296 instanceName = namedValues.get("instanceName"); 297 origin = namedValues.get("origin"); 298 replicationChangeID = namedValues.get("replicationChangeID"); 299 requesterDN = namedValues.get("requesterDN"); 300 requesterIP = namedValues.get("clientIP"); 301 productName = namedValues.get("productName"); 302 startupID = namedValues.get("startupID"); 303 transactionID = namedValues.get("txnID"); 304 usingAdminSessionWorkerThread = 305 getNamedValueAsBoolean("usingAdminSessionWorkerThread", namedValues); 306 operationPurposeRequestControl = 307 decodeOperationPurposeRequestControl(namedValues); 308 intermediateClientRequestControl = 309 decodeIntermediateClientRequestControl(namedValues); 310 311 final String oidsString = namedValues.get("requestControlOIDs"); 312 if (oidsString == null) 313 { 314 requestControlOIDs = null; 315 } 316 else 317 { 318 final ArrayList<String> oidList = new ArrayList<>(10); 319 final StringTokenizer tokenizer = new StringTokenizer(oidsString, ","); 320 while (tokenizer.hasMoreTokens()) 321 { 322 oidList.add(tokenizer.nextToken()); 323 } 324 requestControlOIDs = Collections.unmodifiableList(oidList); 325 } 326 } 327 328 329 330 /** 331 * Parses the provided header line for this audit log message. 332 * 333 * @param logMessageLines The lines that comprise the log message. It 334 * must not be {@code null} or empty. 335 * @param uncommentedHeaderLine The uncommented representation of the header 336 * line. It must not be {@code null}. 337 * @param nameValuePairs A map into which the parsed name-value pairs 338 * may be placed. It must not be {@code null} 339 * and must be updatable. 340 * 341 * @return The date parsed from the header line. The name-value pairs parsed 342 * from the header line will be added to the {@code nameValuePairs} 343 * map. 344 * 345 * @throws AuditLogException If the line cannot be parsed as a valid header. 346 */ 347 private static Date parseHeaderLine(final List<String> logMessageLines, 348 final String uncommentedHeaderLine, 349 final Map<String,String> nameValuePairs) 350 throws AuditLogException 351 { 352 final byte[] uncommentedHeaderBytes = 353 StaticUtils.getBytes(uncommentedHeaderLine); 354 355 final ByteStringBuffer buffer = 356 new ByteStringBuffer(uncommentedHeaderBytes.length); 357 358 final ByteArrayInputStream inputStream = 359 new ByteArrayInputStream(uncommentedHeaderBytes); 360 final Date timestamp = readTimestamp(logMessageLines, inputStream, buffer); 361 while (true) 362 { 363 if (! readNameValuePair(logMessageLines, inputStream, nameValuePairs, 364 buffer)) 365 { 366 break; 367 } 368 } 369 370 return timestamp; 371 } 372 373 374 375 /** 376 * Reads the timestamp from the provided input stream and parses it using one 377 * of the expected formats. 378 * 379 * @param logMessageLines The lines that comprise the log message. It must 380 * not be {@code null} or empty. 381 * @param inputStream The input stream from which to read the timestamp. 382 * It must not be {@code null}. 383 * @param buffer A buffer that may be used to hold temporary data 384 * for reading. It must not be {@code null} and it 385 * must be empty. 386 * 387 * @return The parsed timestamp. 388 * 389 * @throws AuditLogException If the provided string cannot be parsed as a 390 * timestamp. 391 */ 392 private static Date readTimestamp(final List<String> logMessageLines, 393 final ByteArrayInputStream inputStream, 394 final ByteStringBuffer buffer) 395 throws AuditLogException 396 { 397 while (true) 398 { 399 final int intRead = inputStream.read(); 400 if ((intRead < 0) || (intRead == ';')) 401 { 402 break; 403 } 404 405 buffer.append((byte) (intRead & 0xFF)); 406 } 407 408 SimpleDateFormat parser; 409 final String timestampString = buffer.toString().trim(); 410 if (timestampString.length() == 30) 411 { 412 parser = TIMESTAMP_MS_FORMAT_PARSERS.get(); 413 if (parser == null) 414 { 415 parser = new SimpleDateFormat(TIMESTAMP_MS_FORMAT); 416 parser.setLenient(false); 417 TIMESTAMP_MS_FORMAT_PARSERS.set(parser); 418 } 419 } 420 else if (timestampString.length() == 26) 421 { 422 parser = TIMESTAMP_SEC_FORMAT_PARSERS.get(); 423 if (parser == null) 424 { 425 parser = new SimpleDateFormat(TIMESTAMP_SEC_FORMAT); 426 parser.setLenient(false); 427 TIMESTAMP_SEC_FORMAT_PARSERS.set(parser); 428 } 429 } 430 else 431 { 432 throw new AuditLogException(logMessageLines, 433 ERR_AUDIT_LOG_MESSAGE_HEADER_MALFORMED_TIMESTAMP.get()); 434 } 435 436 try 437 { 438 return parser.parse(timestampString); 439 } 440 catch (final ParseException e) 441 { 442 Debug.debugException(e); 443 throw new AuditLogException(logMessageLines, 444 ERR_AUDIT_LOG_MESSAGE_HEADER_MALFORMED_TIMESTAMP.get(), e); 445 } 446 } 447 448 449 450 /** 451 * Reads a name-value pair from the provided buffer. 452 * 453 * @param logMessageLines The lines that comprise the log message. It must 454 * not be {@code null} or empty. 455 * @param inputStream The input stream from which to read the name-value 456 * pair. It must not be {@code null}. 457 * @param nameValuePairs A map to which the name-value pair should be 458 * added. 459 * @param buffer A buffer that may be used to hold temporary data 460 * for reading. It must not be {@code null}, but may 461 * not be empty and should be cleared before use. 462 * 463 * @return {@code true} if a name-value pair was read, or {@code false} if 464 * the end of the input stream was read without reading any more 465 * data. 466 * 467 * @throws AuditLogException If a problem is encountered while trying to 468 * read the name-value pair. 469 */ 470 private static boolean readNameValuePair(final List<String> logMessageLines, 471 final ByteArrayInputStream inputStream, 472 final Map<String,String> nameValuePairs, 473 final ByteStringBuffer buffer) 474 throws AuditLogException 475 { 476 // Read the property name. It will be followed by an equal sign to separate 477 // the name from the value. 478 buffer.clear(); 479 while (true) 480 { 481 final int intRead = inputStream.read(); 482 if (intRead < 0) 483 { 484 // We've hit the end of the input stream. This is okay if we haven't 485 // yet read any data. 486 if (buffer.isEmpty()) 487 { 488 return false; 489 } 490 else 491 { 492 throw new AuditLogException(logMessageLines, 493 ERR_AUDIT_LOG_MESSAGE_HEADER_ENDS_WITH_PROPERTY_NAME.get( 494 buffer.toString())); 495 } 496 } 497 else if (intRead == '=') 498 { 499 break; 500 } 501 else if (intRead != ' ') 502 { 503 buffer.append((byte) (intRead & 0xFF)); 504 } 505 } 506 507 final String name = buffer.toString(); 508 if (name.isEmpty()) 509 { 510 throw new AuditLogException(logMessageLines, 511 ERR_AUDIT_LOG_MESSAGE_HEADER_EMPTY_PROPERTY_NAME.get()); 512 } 513 514 515 // Read the property value. Start by peeking at the next byte in the 516 // input stream. If it's a space, then skip it and loop back to the next 517 // byte. If it's an opening curly brace ({), then read the value as a JSON 518 // object followed by a semicolon. If it's a double quote ("), then read 519 // the value as a quoted string followed by a semicolon. If it's anything 520 // else, then read the value as an unquoted string followed by a semicolon. 521 final String valueString; 522 while (true) 523 { 524 inputStream.mark(1); 525 final int intRead = inputStream.read(); 526 if (intRead < 0) 527 { 528 // We hit the end of the input stream after the equal sign. This is 529 // fine. We'll just use an empty value. 530 valueString = ""; 531 break; 532 } 533 else if (intRead == ' ') 534 { 535 continue; 536 } 537 else if (intRead == '{') 538 { 539 inputStream.reset(); 540 final JSONObject jsonObject = 541 readJSONObject(logMessageLines, name, inputStream); 542 valueString = jsonObject.toString(); 543 break; 544 } 545 else if (intRead == '"') 546 { 547 valueString = 548 readString(logMessageLines, name, true, inputStream, buffer); 549 break; 550 } 551 else if (intRead == ';') 552 { 553 valueString = ""; 554 break; 555 } 556 else 557 { 558 inputStream.reset(); 559 valueString = 560 readString(logMessageLines, name, false, inputStream, buffer); 561 break; 562 } 563 } 564 565 nameValuePairs.put(name, valueString); 566 return true; 567 } 568 569 570 571 /** 572 * Reads a JSON object from the provided input stream. 573 * 574 * @param logMessageLines The lines that comprise the log message. It must 575 * not be {@code null} or empty. 576 * @param propertyName The name of the property whose value is expected 577 * to be a JSON object. It must not be {@code null}. 578 * @param inputStream The input stream from which to read the JSON 579 * object. It must not be {@code null}. 580 * 581 * @return The JSON object that was read. 582 * 583 * @throws AuditLogException If a problem is encountered while trying to 584 * read the JSON object. 585 */ 586 private static JSONObject readJSONObject(final List<String> logMessageLines, 587 final String propertyName, 588 final ByteArrayInputStream inputStream) 589 throws AuditLogException 590 { 591 final JSONObject jsonObject; 592 try 593 { 594 final JSONObjectReader reader = new JSONObjectReader(inputStream, false); 595 jsonObject = reader.readObject(); 596 } 597 catch (final Exception e) 598 { 599 Debug.debugException(e); 600 throw new AuditLogException(logMessageLines, 601 ERR_AUDIT_LOG_MESSAGE_ERROR_READING_JSON_OBJECT.get(propertyName, 602 StaticUtils.getExceptionMessage(e)), 603 e); 604 } 605 606 readSpacesAndSemicolon(logMessageLines, propertyName, inputStream); 607 return jsonObject; 608 } 609 610 611 612 /** 613 * Reads a string from the provided input stream. It may optionally be 614 * treated as a quoted string, in which everything read up to an unescaped 615 * quote will be treated as part of the string, or an unquoted string, in 616 * which the first space or semicolon encountered will signal the end of the 617 * string. Any character prefixed by a backslash will be added to the string 618 * as-is (for example, a backslash followed by a quotation mark will cause the 619 * quotation mark to be part of the string rather than signalling the end of 620 * the quoted string). Any octothorpe (#) character must be followed by two 621 * hexadecimal digits that signify a single raw byte to add to the value. 622 * 623 * @param logMessageLines The lines that comprise the log message. It must 624 * not be {@code null} or empty. 625 * @param propertyName The name of the property with which the string 626 * value is associated. It must not be {@code null}. 627 * @param isQuoted Indicates whether to read a quoted string or an 628 * unquoted string. In the case of a a quoted 629 * string, the opening quote must have already been 630 * read. 631 * @param inputStream The input stream from which to read the string 632 * value. It must not be {@code null}. 633 * @param buffer A buffer that may be used while reading the 634 * string. It must not be {@code null}, but may not 635 * be empty and should be cleared before use. 636 * 637 * @return The string that was read. 638 * 639 * @throws AuditLogException If a problem is encountered while trying to 640 * read the string. 641 */ 642 private static String readString(final List<String> logMessageLines, 643 final String propertyName, 644 final boolean isQuoted, 645 final ByteArrayInputStream inputStream, 646 final ByteStringBuffer buffer) 647 throws AuditLogException 648 { 649 buffer.clear(); 650 651stringLoop: 652 while (true) 653 { 654 inputStream.mark(1); 655 final int intRead = inputStream.read(); 656 if (intRead < 0) 657 { 658 if (isQuoted) 659 { 660 throw new AuditLogException(logMessageLines, 661 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_CLOSING_QUOTE.get( 662 propertyName)); 663 } 664 else 665 { 666 return buffer.toString(); 667 } 668 } 669 670 switch (intRead) 671 { 672 case '\\': 673 final int literalCharacter = inputStream.read(); 674 if (literalCharacter < 0) 675 { 676 throw new AuditLogException(logMessageLines, 677 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_ESCAPED.get(propertyName)); 678 } 679 else 680 { 681 buffer.append((byte) (literalCharacter & 0xFF)); 682 } 683 break; 684 685 case '#': 686 int hexByte = 687 readHexDigit(logMessageLines, propertyName, inputStream); 688 hexByte = (hexByte << 4) | 689 readHexDigit(logMessageLines, propertyName, inputStream); 690 buffer.append((byte) (hexByte & 0xFF)); 691 break; 692 693 case '"': 694 if (isQuoted) 695 { 696 break stringLoop; 697 } 698 699 buffer.append('"'); 700 break; 701 702 case ' ': 703 if (! isQuoted) 704 { 705 break stringLoop; 706 } 707 708 buffer.append(' '); 709 break; 710 711 case ';': 712 if (! isQuoted) 713 { 714 inputStream.reset(); 715 break stringLoop; 716 } 717 718 buffer.append(';'); 719 break; 720 721 default: 722 buffer.append((byte) (intRead & 0xFF)); 723 break; 724 } 725 } 726 727 readSpacesAndSemicolon(logMessageLines, propertyName, inputStream); 728 return buffer.toString(); 729 } 730 731 732 733 /** 734 * Reads a single hexadecimal digit from the provided input stream and returns 735 * its integer value. 736 * 737 * @param logMessageLines The lines that comprise the log message. It must 738 * not be {@code null} or empty. 739 * @param propertyName The name of the property with which the string 740 * value is associated. It must not be {@code null}. 741 * @param inputStream The input stream from which to read the string 742 * value. It must not be {@code null}. 743 * 744 * @return The integer value of the hexadecimal digit that was read. 745 * 746 * @throws AuditLogException If the end of the input stream was reached 747 * before the byte could be read, or if the byte 748 * that was read did not represent a hexadecimal 749 * digit. 750 */ 751 private static int readHexDigit(final List<String> logMessageLines, 752 final String propertyName, 753 final ByteArrayInputStream inputStream) 754 throws AuditLogException 755 { 756 final int byteRead = inputStream.read(); 757 if (byteRead < 0) 758 { 759 throw new AuditLogException(logMessageLines, 760 ERR_AUDIT_LOG_MESSAGE_END_BEFORE_HEX.get(propertyName)); 761 } 762 763 switch (byteRead) 764 { 765 case '0': 766 return 0; 767 case '1': 768 return 1; 769 case '2': 770 return 2; 771 case '3': 772 return 3; 773 case '4': 774 return 4; 775 case '5': 776 return 5; 777 case '6': 778 return 6; 779 case '7': 780 return 7; 781 case '8': 782 return 8; 783 case '9': 784 return 9; 785 case 'a': 786 case 'A': 787 return 10; 788 case 'b': 789 case 'B': 790 return 11; 791 case 'c': 792 case 'C': 793 return 12; 794 case 'd': 795 case 'D': 796 return 13; 797 case 'e': 798 case 'E': 799 return 14; 800 case 'f': 801 case 'F': 802 return 15; 803 default: 804 throw new AuditLogException(logMessageLines, 805 ERR_AUDIT_LOG_MESSAGE_INVALID_HEX_DIGIT.get(propertyName)); 806 } 807 } 808 809 810 811 /** 812 * Reads zero or more spaces and the following semicolon from the provided 813 * input stream. It is also acceptable to encounter the end of the stream. 814 * 815 * @param logMessageLines The lines that comprise the log message. It must 816 * not be {@code null} or empty. 817 * @param propertyName The name of the property that was just read. It 818 * must not be {@code null}. 819 * @param inputStream The input stream from which to read the spaces and 820 * semicolon. It must not be {@code null}. 821 * 822 * @throws AuditLogException If any byte is encountered that is not a space 823 * or a semicolon. 824 */ 825 private static void readSpacesAndSemicolon(final List<String> logMessageLines, 826 final String propertyName, 827 final ByteArrayInputStream inputStream) 828 throws AuditLogException 829 { 830 while (true) 831 { 832 final int intRead = inputStream.read(); 833 if ((intRead < 0) || (intRead == ';')) 834 { 835 return; 836 } 837 else if (intRead != ' ') 838 { 839 throw new AuditLogException(logMessageLines, 840 ERR_AUDIT_LOG_MESSAGE_UNEXPECTED_CHAR_AFTER_PROPERTY.get( 841 String.valueOf((char) intRead), propertyName)); 842 } 843 } 844 } 845 846 847 848 /** 849 * Retrieves the value of the header property with the given name as a 850 * {@code Boolean} object. 851 * 852 * @param name The name of the property to retrieve. It must not 853 * be {@code null}, and it will be treated in a 854 * case-sensitive manner. 855 * @param nameValuePairs The map containing the header properties as 856 * name-value pairs. It must not be {@code null}. 857 * 858 * @return The value of the specified property as a {@code Boolean}, or 859 * {@code null} if the property is not defined or if it cannot be 860 * parsed as a {@code Boolean}. 861 */ 862 protected static Boolean getNamedValueAsBoolean(final String name, 863 final Map<String,String> nameValuePairs) 864 { 865 final String valueString = nameValuePairs.get(name); 866 if (valueString == null) 867 { 868 return null; 869 } 870 871 final String lowerValueString = StaticUtils.toLowerCase(valueString); 872 if (lowerValueString.equals("true") || 873 lowerValueString.equals("t") || 874 lowerValueString.equals("yes") || 875 lowerValueString.equals("y") || 876 lowerValueString.equals("on") || 877 lowerValueString.equals("1")) 878 { 879 return Boolean.TRUE; 880 } 881 else if (lowerValueString.equals("false") || 882 lowerValueString.equals("f") || 883 lowerValueString.equals("no") || 884 lowerValueString.equals("n") || 885 lowerValueString.equals("off") || 886 lowerValueString.equals("0")) 887 { 888 return Boolean.FALSE; 889 } 890 else 891 { 892 return null; 893 } 894 } 895 896 897 898 /** 899 * Retrieves the value of the header property with the given name as a 900 * {@code Long} object. 901 * 902 * @param name The name of the property to retrieve. It must not 903 * be {@code null}, and it will be treated in a 904 * case-sensitive manner. 905 * @param nameValuePairs The map containing the header properties as 906 * name-value pairs. It must not be {@code null}. 907 * 908 * @return The value of the specified property as a {@code Long}, or 909 * {@code null} if the property is not defined or if it cannot be 910 * parsed as a {@code Long}. 911 */ 912 protected static Long getNamedValueAsLong(final String name, 913 final Map<String,String> nameValuePairs) 914 { 915 final String valueString = nameValuePairs.get(name); 916 if (valueString == null) 917 { 918 return null; 919 } 920 921 try 922 { 923 return Long.parseLong(valueString); 924 } 925 catch (final Exception e) 926 { 927 Debug.debugException(e); 928 return null; 929 } 930 } 931 932 933 934 /** 935 * Decodes an entry (or list of attributes) from the commented header 936 * contained in the log message lines. 937 * 938 * @param header The header line that appears before the encoded 939 * entry. 940 * @param logMessageLines The lines that comprise the audit log message. 941 * @param entryDN The DN to use for the entry that is read. It 942 * should be {@code null} if the commented entry 943 * includes a DN, and non-{@code null} if the 944 * commented entry does not include a DN. 945 * 946 * @return The entry that was decoded from the commented header, or 947 * {@code null} if it is not included in the header or if it cannot 948 * be decoded. If the commented entry does not include a DN, then 949 * the DN of the entry returned will be the null DN. 950 */ 951 protected static ReadOnlyEntry decodeCommentedEntry(final String header, 952 final List<String> logMessageLines, 953 final String entryDN) 954 { 955 List<String> ldifLines = null; 956 StringBuilder invalidLDAPNameReason = null; 957 for (final String line : logMessageLines) 958 { 959 final String uncommentedLine; 960 if (line.startsWith("# ")) 961 { 962 uncommentedLine = line.substring(2); 963 } 964 else 965 { 966 break; 967 } 968 969 if (ldifLines == null) 970 { 971 if (uncommentedLine.equalsIgnoreCase(header)) 972 { 973 ldifLines = new ArrayList<>(logMessageLines.size()); 974 if (entryDN != null) 975 { 976 ldifLines.add("dn: " + entryDN); 977 } 978 } 979 } 980 else 981 { 982 final int colonPos = uncommentedLine.indexOf(':'); 983 if (colonPos <= 0) 984 { 985 break; 986 } 987 988 if (invalidLDAPNameReason == null) 989 { 990 invalidLDAPNameReason = new StringBuilder(); 991 } 992 993 final String potentialAttributeName = 994 uncommentedLine.substring(0, colonPos); 995 if (PersistUtils.isValidLDAPName(potentialAttributeName, 996 invalidLDAPNameReason)) 997 { 998 ldifLines.add(uncommentedLine); 999 } 1000 else 1001 { 1002 break; 1003 } 1004 } 1005 } 1006 1007 if (ldifLines == null) 1008 { 1009 return null; 1010 } 1011 1012 try 1013 { 1014 final String[] ldifLineArray = ldifLines.toArray(StaticUtils.NO_STRINGS); 1015 final Entry ldifEntry = LDIFReader.decodeEntry(ldifLineArray); 1016 return new ReadOnlyEntry(ldifEntry); 1017 } 1018 catch (final Exception e) 1019 { 1020 Debug.debugException(e); 1021 return null; 1022 } 1023 } 1024 1025 1026 1027 /** 1028 * Decodes the operation purpose request control, if any, from the provided 1029 * set of name-value pairs. 1030 * 1031 * @param nameValuePairs The map containing the header properties as 1032 * name-value pairs. It must not be {@code null}. 1033 * 1034 * @return The operation purpose request control retrieved and decoded from 1035 * the provided set of name-value pairs, or {@code null} if no 1036 * valid operation purpose request control was included. 1037 */ 1038 private static OperationPurposeRequestControl 1039 decodeOperationPurposeRequestControl( 1040 final Map<String,String> nameValuePairs) 1041 { 1042 final String valueString = nameValuePairs.get("operationPurpose"); 1043 if (valueString == null) 1044 { 1045 return null; 1046 } 1047 1048 try 1049 { 1050 final JSONObject o = new JSONObject(valueString); 1051 1052 final String applicationName = o.getFieldAsString("applicationName"); 1053 final String applicationVersion = 1054 o.getFieldAsString("applicationVersion"); 1055 final String codeLocation = o.getFieldAsString("codeLocation"); 1056 final String requestPurpose = o.getFieldAsString("requestPurpose"); 1057 1058 return new OperationPurposeRequestControl(false, applicationName, 1059 applicationVersion, codeLocation, requestPurpose); 1060 } 1061 catch (final Exception e) 1062 { 1063 Debug.debugException(e); 1064 return null; 1065 } 1066 } 1067 1068 1069 1070 /** 1071 * Decodes the intermediate client request control, if any, from the provided 1072 * set of name-value pairs. 1073 * 1074 * @param nameValuePairs The map containing the header properties as 1075 * name-value pairs. It must not be {@code null}. 1076 * 1077 * @return The intermediate client request control retrieved and decoded from 1078 * the provided set of name-value pairs, or {@code null} if no 1079 * valid operation purpose request control was included. 1080 */ 1081 private static IntermediateClientRequestControl 1082 decodeIntermediateClientRequestControl( 1083 final Map<String,String> nameValuePairs) 1084 { 1085 final String valueString = 1086 nameValuePairs.get("intermediateClientRequestControl"); 1087 if (valueString == null) 1088 { 1089 return null; 1090 } 1091 1092 try 1093 { 1094 final JSONObject o = new JSONObject(valueString); 1095 return new IntermediateClientRequestControl( 1096 decodeIntermediateClientRequestValue(o)); 1097 } 1098 catch (final Exception e) 1099 { 1100 Debug.debugException(e); 1101 return null; 1102 } 1103 } 1104 1105 1106 1107 /** 1108 * decodes the provided JSON object as an intermediate client request control 1109 * value. 1110 * 1111 * @param o The JSON object to be decoded. It must not be {@code null}. 1112 * 1113 * @return The intermediate client request control value decoded from the 1114 * provided JSON object. 1115 */ 1116 private static IntermediateClientRequestValue 1117 decodeIntermediateClientRequestValue(final JSONObject o) 1118 { 1119 if (o == null) 1120 { 1121 return null; 1122 } 1123 1124 final String clientIdentity = o.getFieldAsString("clientIdentity"); 1125 final String downstreamClientAddress = 1126 o.getFieldAsString("downstreamClientAddress"); 1127 final Boolean downstreamClientSecure = 1128 o.getFieldAsBoolean("downstreamClientSecure"); 1129 final String clientName = o.getFieldAsString("clientName"); 1130 final String clientSessionID = o.getFieldAsString("clientSessionID"); 1131 final String clientRequestID = o.getFieldAsString("clientRequestID"); 1132 final IntermediateClientRequestValue downstreamRequest = 1133 decodeIntermediateClientRequestValue( 1134 o.getFieldAsObject("downstreamRequest")); 1135 1136 return new IntermediateClientRequestValue(downstreamRequest, 1137 downstreamClientAddress, downstreamClientSecure, clientIdentity, 1138 clientName, clientSessionID, clientRequestID); 1139 } 1140 1141 1142 1143 /** 1144 * Retrieves the lines that comprise the complete audit log message. 1145 * 1146 * @return The lines that comprise the complete audit log message. 1147 */ 1148 public final List<String> getLogMessageLines() 1149 { 1150 return logMessageLines; 1151 } 1152 1153 1154 1155 /** 1156 * Retrieves the line that comprises the header for this log message, 1157 * including the leading octothorpe (#) and space that make it a comment. 1158 * 1159 * @return The line that comprises the header for this log message, including 1160 * the leading octothorpe (#) and space that make it a comment. 1161 */ 1162 public final String getCommentedHeaderLine() 1163 { 1164 return commentedHeaderLine; 1165 } 1166 1167 1168 1169 /** 1170 * Retrieves the line that comprises the header for this log message, without 1171 * the leading octothorpe (#) and space that make it a comment. 1172 * 1173 * @return The line that comprises the header for this log message, without 1174 * the leading octothorpe (#) and space that make it a comment. 1175 */ 1176 public final String getUncommentedHeaderLine() 1177 { 1178 return uncommentedHeaderLine; 1179 } 1180 1181 1182 1183 /** 1184 * Retrieves the timestamp for this audit log message. 1185 * 1186 * @return The timestamp for this audit log message. 1187 */ 1188 public final Date getTimestamp() 1189 { 1190 return timestamp; 1191 } 1192 1193 1194 1195 /** 1196 * Retrieves a map of the name-value pairs contained in the header for this 1197 * log message. 1198 * 1199 * @return A map of the name-value pairs contained in the header for this log 1200 * message. 1201 */ 1202 public final Map<String,String> getHeaderNamedValues() 1203 { 1204 return namedValues; 1205 } 1206 1207 1208 1209 /** 1210 * Retrieves the server product name for this audit log message, if available. 1211 * 1212 * @return The server product name for this audit log message, or 1213 * {@code null} if it is not available. 1214 */ 1215 public final String getProductName() 1216 { 1217 return productName; 1218 } 1219 1220 1221 1222 /** 1223 * Retrieves the server instance name for this audit log message, if 1224 * available. 1225 * 1226 * @return The server instance name for this audit log message, or 1227 * {@code null} if it is not available. 1228 */ 1229 public final String getInstanceName() 1230 { 1231 return instanceName; 1232 } 1233 1234 1235 1236 /** 1237 * Retrieves the unique identifier generated when the server was started, if 1238 * available. 1239 * 1240 * @return The unique identifier generated when the server was started, or 1241 * {@code null} if it is not available. 1242 */ 1243 public final String getStartupID() 1244 { 1245 return startupID; 1246 } 1247 1248 1249 1250 /** 1251 * Retrieves the identifier for the server thread that processed the change, 1252 * if available. 1253 * 1254 * @return The identifier for the server thread that processed the change, or 1255 * {@code null} if it is not available. 1256 */ 1257 public final Long getThreadID() 1258 { 1259 return threadID; 1260 } 1261 1262 1263 1264 /** 1265 * Retrieves the DN of the user that requested the change, if available. 1266 * 1267 * @return The DN of the user that requested the change, or {@code null} if 1268 * it is not available. 1269 */ 1270 public final String getRequesterDN() 1271 { 1272 return requesterDN; 1273 } 1274 1275 1276 1277 /** 1278 * Retrieves the IP address of the client that requested the change, if 1279 * available. 1280 * 1281 * @return The IP address of the client that requested the change, or 1282 * {@code null} if it is not available. 1283 */ 1284 public final String getRequesterIPAddress() 1285 { 1286 return requesterIP; 1287 } 1288 1289 1290 1291 /** 1292 * Retrieves the connection ID for the connection on which the change was 1293 * requested, if available. 1294 * 1295 * @return The connection ID for the connection on which the change was 1296 * requested, or {@code null} if it is not available. 1297 */ 1298 public final Long getConnectionID() 1299 { 1300 return connectionID; 1301 } 1302 1303 1304 1305 /** 1306 * Retrieves the connection ID for the connection on which the change was 1307 * requested, if available. 1308 * 1309 * @return The connection ID for the connection on which the change was 1310 * requested, or {@code null} if it is not available. 1311 */ 1312 public final Long getOperationID() 1313 { 1314 return operationID; 1315 } 1316 1317 1318 1319 /** 1320 * Retrieves the connection ID for the external operation that triggered the 1321 * internal operation with which this audit log message is associated, if 1322 * available. 1323 * 1324 * @return The connection ID for the external operation that triggered the 1325 * internal operation with which this audit log message is 1326 * associated, or {@code null} if it is not available. 1327 */ 1328 public final Long getTriggeredByConnectionID() 1329 { 1330 return triggeredByConnectionID; 1331 } 1332 1333 1334 1335 /** 1336 * Retrieves the operation 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 operation 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 getTriggeredByOperationID() 1345 { 1346 return triggeredByOperationID; 1347 } 1348 1349 1350 1351 /** 1352 * Retrieves the replication change ID for this audit log message, if 1353 * available. 1354 * 1355 * @return The replication change ID for this audit log message, or 1356 * {@code null} if it is not available. 1357 */ 1358 public final String getReplicationChangeID() 1359 { 1360 return replicationChangeID; 1361 } 1362 1363 1364 1365 /** 1366 * Retrieves the alternate authorization DN for this audit log message, if 1367 * available. 1368 * 1369 * @return The alternate authorization DN for this audit log message, or 1370 * {@code null} if it is not available. 1371 */ 1372 public final String getAlternateAuthorizationDN() 1373 { 1374 return alternateAuthorizationDN; 1375 } 1376 1377 1378 1379 /** 1380 * Retrieves the transaction ID for this audit log message, if available. 1381 * 1382 * @return The transaction ID for this audit log message, or {@code null} if 1383 * it is not available. 1384 */ 1385 public final String getTransactionID() 1386 { 1387 return transactionID; 1388 } 1389 1390 1391 1392 /** 1393 * Retrieves the origin for this audit log message, if available. 1394 * 1395 * @return The origin for this audit log message, or {@code null} if it is 1396 * not available. 1397 */ 1398 public final String getOrigin() 1399 { 1400 return origin; 1401 } 1402 1403 1404 1405 /** 1406 * Retrieves the value of the flag indicating whether the associated operation 1407 * was processed using an administrative session worker thread, if available. 1408 * 1409 * @return {@code Boolean.TRUE} if it is known that the associated operation 1410 * was processed using an administrative session worker thread, 1411 * {@code Boolean.FALSE} if it is known that the associated operation 1412 * was not processed using an administrative session worker thread, 1413 * or {@code null} if it is not available. 1414 */ 1415 public final Boolean getUsingAdminSessionWorkerThread() 1416 { 1417 return usingAdminSessionWorkerThread; 1418 } 1419 1420 1421 1422 /** 1423 * Retrieves a list of the OIDs of the request controls included in the 1424 * operation request, if available. 1425 * 1426 * @return A list of the OIDs of the request controls included in the 1427 * operation, an empty list if it is known that there were no request 1428 * controls, or {@code null} if it is not available. 1429 */ 1430 public final List<String> getRequestControlOIDs() 1431 { 1432 return requestControlOIDs; 1433 } 1434 1435 1436 1437 /** 1438 * Retrieves an operation purpose request control with information about the 1439 * purpose for the associated operation, if available. 1440 * 1441 * @return An operation purpose request control with information about the 1442 * purpose for the associated operation, or {@code null} if it is not 1443 * available. 1444 */ 1445 public final OperationPurposeRequestControl 1446 getOperationPurposeRequestControl() 1447 { 1448 return operationPurposeRequestControl; 1449 } 1450 1451 1452 1453 /** 1454 * Retrieves an intermediate client request control with information about the 1455 * downstream processing for the associated operation, if available. 1456 * 1457 * @return An intermediate client request control with information about the 1458 * downstream processing for the associated operation, or 1459 * {@code null} if it is not available. 1460 */ 1461 public final IntermediateClientRequestControl 1462 getIntermediateClientRequestControl() 1463 { 1464 return intermediateClientRequestControl; 1465 } 1466 1467 1468 1469 /** 1470 * Retrieves the DN of the entry targeted by the associated operation. 1471 * 1472 * @return The DN of the entry targeted by the associated operation. 1473 */ 1474 public abstract String getDN(); 1475 1476 1477 1478 /** 1479 * Retrieves the change type for this audit log message. 1480 * 1481 * @return The change type for this audit log message. 1482 */ 1483 public abstract ChangeType getChangeType(); 1484 1485 1486 1487 /** 1488 * Retrieves an LDIF change record that encapsulates the change represented by 1489 * this audit log message. 1490 * 1491 * @return An LDIF change record that encapsulates the change represented by 1492 * this audit log message. 1493 */ 1494 public abstract LDIFChangeRecord getChangeRecord(); 1495 1496 1497 1498 /** 1499 * Indicates whether it is possible to use the 1500 * {@link #getRevertChangeRecords()} method to obtain a list of LDIF change 1501 * records that can be used to revert the changes described by this audit log 1502 * message. 1503 * 1504 * @return {@code true} if it is possible to use the 1505 * {@link #getRevertChangeRecords()} method to obtain a list of LDIF 1506 * change records that can be used to revert the changes described 1507 * by this audit log message, or {@code false} if not. 1508 */ 1509 public abstract boolean isRevertible(); 1510 1511 1512 1513 /** 1514 * Retrieves a list of the change records that can be used to revert the 1515 * changes described by this audit log message. 1516 * 1517 * @return A list of the change records that can be used to revert the 1518 * changes described by this audit log message. 1519 * 1520 * @throws AuditLogException If this audit log message cannot be reverted. 1521 */ 1522 public abstract List<LDIFChangeRecord> getRevertChangeRecords() 1523 throws AuditLogException; 1524 1525 1526 1527 /** 1528 * Retrieves a single-line string representation of this audit log message. 1529 * It will start with the string returned by 1530 * {@link #getUncommentedHeaderLine()}, but will also contain additional 1531 * name-value pairs that are pertinent to the type of operation that the audit 1532 * log message represents. 1533 * 1534 * @return A string representation of this audit log message. 1535 */ 1536 @Override() 1537 public final String toString() 1538 { 1539 final StringBuilder buffer = new StringBuilder(); 1540 toString(buffer); 1541 return buffer.toString(); 1542 } 1543 1544 1545 1546 /** 1547 * Appends a single-line string representation of this audit log message to 1548 * the provided buffer. The message will start with the string returned by 1549 * {@link #getUncommentedHeaderLine()}, but will also contain additional 1550 * name-value pairs that are pertinent to the type of operation that the audit 1551 * log message represents. 1552 * 1553 * @param buffer The buffer to which the information should be appended. 1554 */ 1555 public abstract void toString(final StringBuilder buffer); 1556 1557 1558 1559 /** 1560 * Retrieves a multi-line string representation of this audit log message. It 1561 * will simply be a concatenation of all of the lines that comprise the 1562 * complete log message, with line breaks between them. 1563 * 1564 * @return A multi-line string representation of this audit log message. 1565 */ 1566 public final String toMultiLineString() 1567 { 1568 return StaticUtils.concatenateStrings(null, null, StaticUtils.EOL, null, 1569 null, logMessageLines); 1570 } 1571}