001/* 002 * Copyright 2015-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-2023 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2015-2023 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.listener; 037 038 039 040import java.io.File; 041import java.io.FileOutputStream; 042import java.io.IOException; 043import java.io.OutputStream; 044import java.io.PrintStream; 045import java.util.ArrayList; 046import java.util.Date; 047import java.util.List; 048import java.util.concurrent.atomic.AtomicBoolean; 049 050import com.unboundid.ldap.protocol.AbandonRequestProtocolOp; 051import com.unboundid.ldap.protocol.AddRequestProtocolOp; 052import com.unboundid.ldap.protocol.BindRequestProtocolOp; 053import com.unboundid.ldap.protocol.CompareRequestProtocolOp; 054import com.unboundid.ldap.protocol.DeleteRequestProtocolOp; 055import com.unboundid.ldap.protocol.ExtendedRequestProtocolOp; 056import com.unboundid.ldap.protocol.LDAPMessage; 057import com.unboundid.ldap.protocol.ModifyRequestProtocolOp; 058import com.unboundid.ldap.protocol.ModifyDNRequestProtocolOp; 059import com.unboundid.ldap.protocol.SearchRequestProtocolOp; 060import com.unboundid.ldap.protocol.UnbindRequestProtocolOp; 061import com.unboundid.ldap.sdk.AddRequest; 062import com.unboundid.ldap.sdk.BindRequest; 063import com.unboundid.ldap.sdk.CompareRequest; 064import com.unboundid.ldap.sdk.Control; 065import com.unboundid.ldap.sdk.DeleteRequest; 066import com.unboundid.ldap.sdk.ExtendedRequest; 067import com.unboundid.ldap.sdk.LDAPException; 068import com.unboundid.ldap.sdk.ModifyRequest; 069import com.unboundid.ldap.sdk.ModifyDNRequest; 070import com.unboundid.ldap.sdk.SearchRequest; 071import com.unboundid.ldap.sdk.ToCodeArgHelper; 072import com.unboundid.ldap.sdk.ToCodeHelper; 073import com.unboundid.util.NotMutable; 074import com.unboundid.util.NotNull; 075import com.unboundid.util.Nullable; 076import com.unboundid.util.StaticUtils; 077import com.unboundid.util.ThreadSafety; 078import com.unboundid.util.ThreadSafetyLevel; 079 080 081 082/** 083 * This class provides a request handler that may be used to create a log file 084 * with code that may be used to generate the requests received from clients. 085 * It will be also be associated with another request handler that will actually 086 * be used to handle the request. 087 */ 088@NotMutable() 089@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 090public final class ToCodeRequestHandler 091 extends LDAPListenerRequestHandler 092{ 093 // Indicates whether any messages have been written to the log so far. 094 @NotNull private final AtomicBoolean firstMessage; 095 096 // Indicates whether the output should include code that may be used to 097 // process the request and handle the response. 098 private final boolean includeProcessing; 099 100 // The client connection with which this request handler is associated. 101 @Nullable private final LDAPListenerClientConnection clientConnection; 102 103 // The request handler that actually will be used to process any requests 104 // received. 105 @NotNull private final LDAPListenerRequestHandler requestHandler; 106 107 // The stream to which the generated code will be written. 108 @NotNull private final PrintStream logStream; 109 110 // Thread-local lists used to hold the generated code. 111 @NotNull private final ThreadLocal<List<String>> lineLists; 112 113 114 115 /** 116 * Creates a new LDAP listener request handler that will write a log file with 117 * LDAP SDK code that corresponds to requests received from clients. The 118 * requests will be forwarded on to another request handler for further 119 * processing. 120 * 121 * @param outputFilePath The path to the output file to be which the 122 * generated code should be written. It must not 123 * be {@code null}, and the parent directory must 124 * exist. If a file already exists with the 125 * specified path, then new generated code will be 126 * appended to it. 127 * @param includeProcessing Indicates whether the output should include 128 * sample code for processing the request and 129 * handling the response. 130 * @param requestHandler The request handler that will actually be used 131 * to process any requests received. It must not 132 * be {@code null}. 133 * 134 * @throws IOException If a problem is encountered while opening the 135 * output file for writing. 136 */ 137 public ToCodeRequestHandler(@NotNull final String outputFilePath, 138 final boolean includeProcessing, 139 @NotNull final LDAPListenerRequestHandler requestHandler) 140 throws IOException 141 { 142 this(new File(outputFilePath), includeProcessing, requestHandler); 143 } 144 145 146 147 /** 148 * Creates a new LDAP listener request handler that will write a log file with 149 * LDAP SDK code that corresponds to requests received from clients. The 150 * requests will be forwarded on to another request handler for further 151 * processing. 152 * 153 * @param outputFile The output file to be which the generated code 154 * should be written. It must not be {@code null}, 155 * and the parent directory must exist. If the 156 * file already exists, then new generated code 157 * will be appended to it. 158 * @param includeProcessing Indicates whether the output should include 159 * sample code for processing the request and 160 * handling the response. 161 * @param requestHandler The request handler that will actually be used 162 * to process any requests received. It must not 163 * be {@code null}. 164 * 165 * @throws IOException If a problem is encountered while opening the 166 * output file for writing. 167 */ 168 public ToCodeRequestHandler(@NotNull final File outputFile, 169 final boolean includeProcessing, 170 @NotNull final LDAPListenerRequestHandler requestHandler) 171 throws IOException 172 { 173 this(new FileOutputStream(outputFile, true), includeProcessing, 174 requestHandler); 175 } 176 177 178 179 /** 180 * Creates a new LDAP listener request handler that will write a log file with 181 * LDAP SDK code that corresponds to requests received from clients. The 182 * requests will be forwarded on to another request handler for further 183 * processing. 184 * 185 * @param outputStream The output stream to which the generated code 186 * will be written. It must not be {@code null}. 187 * @param includeProcessing Indicates whether the output should include 188 * sample code for processing the request and 189 * handling the response. 190 * @param requestHandler The request handler that will actually be used 191 * to process any requests received. It must not 192 * be {@code null}. 193 */ 194 public ToCodeRequestHandler(@NotNull final OutputStream outputStream, 195 final boolean includeProcessing, 196 @NotNull final LDAPListenerRequestHandler requestHandler) 197 { 198 logStream = new PrintStream(outputStream, true); 199 200 this.includeProcessing = includeProcessing; 201 this.requestHandler = requestHandler; 202 203 firstMessage = new AtomicBoolean(true); 204 lineLists = new ThreadLocal<>(); 205 clientConnection = null; 206 } 207 208 209 210 /** 211 * Creates a new to code request handler instance for the provided client 212 * connection. 213 * 214 * @param parentHandler The parent handler with which this instance will be 215 * associated. 216 * @param connection The client connection for this instance. 217 * 218 * @throws LDAPException If a problem is encountered while creating a new 219 * instance of the downstream request handler. 220 */ 221 private ToCodeRequestHandler( 222 @NotNull final ToCodeRequestHandler parentHandler, 223 @NotNull final LDAPListenerClientConnection connection) 224 throws LDAPException 225 { 226 logStream = parentHandler.logStream; 227 includeProcessing = parentHandler.includeProcessing; 228 requestHandler = parentHandler.requestHandler.newInstance(connection); 229 firstMessage = parentHandler.firstMessage; 230 clientConnection = connection; 231 lineLists = parentHandler.lineLists; 232 } 233 234 235 236 /** 237 * {@inheritDoc} 238 */ 239 @Override() 240 @NotNull() 241 public ToCodeRequestHandler newInstance( 242 @NotNull final LDAPListenerClientConnection connection) 243 throws LDAPException 244 { 245 return new ToCodeRequestHandler(this, connection); 246 } 247 248 249 250 /** 251 * {@inheritDoc} 252 */ 253 @Override() 254 public void closeInstance() 255 { 256 // We'll always close the downstream request handler instance. 257 requestHandler.closeInstance(); 258 259 260 // We only want to close the log stream if this is the parent instance that 261 // is not associated with any specific connection. 262 if (clientConnection == null) 263 { 264 synchronized (logStream) 265 { 266 logStream.close(); 267 } 268 } 269 } 270 271 272 273 /** 274 * {@inheritDoc} 275 */ 276 @Override() 277 public void processAbandonRequest(final int messageID, 278 @NotNull final AbandonRequestProtocolOp request, 279 @NotNull final List<Control> controls) 280 { 281 // The LDAP SDK doesn't provide an AbandonRequest object. In order to 282 // process abandon operations, the LDAP SDK requires the client to have 283 // invoked an asynchronous operation in order to get an AsyncRequestID. 284 // Since this uses LDAPConnection.abandon, then that falls under the 285 // "processing" umbrella. So we'll only log something if we should include 286 // processing details. 287 if (includeProcessing) 288 { 289 final List<String> lineList = getLineList(messageID); 290 291 final ArrayList<ToCodeArgHelper> args = new ArrayList<>(2); 292 args.add(ToCodeArgHelper.createRaw( 293 "asyncRequestID" + request.getIDToAbandon(), "Async Request ID")); 294 if (! controls.isEmpty()) 295 { 296 final Control[] controlArray = new Control[controls.size()]; 297 controls.toArray(controlArray); 298 args.add(ToCodeArgHelper.createControlArray(controlArray, 299 "Request Controls")); 300 } 301 302 ToCodeHelper.generateMethodCall(lineList, 0, null, null, 303 "connection.abandon", args); 304 305 writeLines(lineList); 306 } 307 308 requestHandler.processAbandonRequest(messageID, request, controls); 309 } 310 311 312 313 /** 314 * {@inheritDoc} 315 */ 316 @Override() 317 @NotNull() 318 public LDAPMessage processAddRequest(final int messageID, 319 @NotNull final AddRequestProtocolOp request, 320 @NotNull final List<Control> controls) 321 { 322 final List<String> lineList = getLineList(messageID); 323 324 final String requestID = "conn" + clientConnection.getConnectionID() + 325 "Msg" + messageID + "Add"; 326 final AddRequest addRequest = 327 request.toAddRequest(getControlArray(controls)); 328 addRequest.toCode(lineList, requestID, 0, includeProcessing); 329 writeLines(lineList); 330 331 return requestHandler.processAddRequest(messageID, request, controls); 332 } 333 334 335 336 /** 337 * {@inheritDoc} 338 */ 339 @Override() 340 @NotNull() 341 public LDAPMessage processBindRequest(final int messageID, 342 @NotNull final BindRequestProtocolOp request, 343 @NotNull final List<Control> controls) 344 { 345 final List<String> lineList = getLineList(messageID); 346 347 final String requestID = "conn" + clientConnection.getConnectionID() + 348 "Msg" + messageID + "Bind"; 349 final BindRequest bindRequest = 350 request.toBindRequest(getControlArray(controls)); 351 bindRequest.toCode(lineList, requestID, 0, includeProcessing); 352 writeLines(lineList); 353 354 return requestHandler.processBindRequest(messageID, request, controls); 355 } 356 357 358 359 /** 360 * {@inheritDoc} 361 */ 362 @Override() 363 @NotNull() 364 public LDAPMessage processCompareRequest(final int messageID, 365 @NotNull final CompareRequestProtocolOp request, 366 @NotNull final List<Control> controls) 367 { 368 final List<String> lineList = getLineList(messageID); 369 370 final String requestID = "conn" + clientConnection.getConnectionID() + 371 "Msg" + messageID + "Compare"; 372 final CompareRequest compareRequest = 373 request.toCompareRequest(getControlArray(controls)); 374 compareRequest.toCode(lineList, requestID, 0, includeProcessing); 375 writeLines(lineList); 376 377 return requestHandler.processCompareRequest(messageID, request, controls); 378 } 379 380 381 382 /** 383 * {@inheritDoc} 384 */ 385 @Override() 386 @NotNull() 387 public LDAPMessage processDeleteRequest(final int messageID, 388 @NotNull final DeleteRequestProtocolOp request, 389 @NotNull final List<Control> controls) 390 { 391 final List<String> lineList = getLineList(messageID); 392 393 final String requestID = "conn" + clientConnection.getConnectionID() + 394 "Msg" + messageID + "Delete"; 395 final DeleteRequest deleteRequest = 396 request.toDeleteRequest(getControlArray(controls)); 397 deleteRequest.toCode(lineList, requestID, 0, includeProcessing); 398 writeLines(lineList); 399 400 return requestHandler.processDeleteRequest(messageID, request, controls); 401 } 402 403 404 405 /** 406 * {@inheritDoc} 407 */ 408 @Override() 409 @NotNull() 410 public LDAPMessage processExtendedRequest(final int messageID, 411 @NotNull final ExtendedRequestProtocolOp request, 412 @NotNull final List<Control> controls) 413 { 414 final List<String> lineList = getLineList(messageID); 415 416 final String requestID = "conn" + clientConnection.getConnectionID() + 417 "Msg" + messageID + "Extended"; 418 final ExtendedRequest extendedRequest = 419 request.toExtendedRequest(getControlArray(controls)); 420 extendedRequest.toCode(lineList, requestID, 0, includeProcessing); 421 writeLines(lineList); 422 423 return requestHandler.processExtendedRequest(messageID, request, controls); 424 } 425 426 427 428 /** 429 * {@inheritDoc} 430 */ 431 @Override() 432 @NotNull() 433 public LDAPMessage processModifyRequest(final int messageID, 434 @NotNull final ModifyRequestProtocolOp request, 435 @NotNull final List<Control> controls) 436 { 437 final List<String> lineList = getLineList(messageID); 438 439 final String requestID = "conn" + clientConnection.getConnectionID() + 440 "Msg" + messageID + "Modify"; 441 final ModifyRequest modifyRequest = 442 request.toModifyRequest(getControlArray(controls)); 443 modifyRequest.toCode(lineList, requestID, 0, includeProcessing); 444 writeLines(lineList); 445 446 return requestHandler.processModifyRequest(messageID, request, controls); 447 } 448 449 450 451 /** 452 * {@inheritDoc} 453 */ 454 @Override() 455 @NotNull() 456 public LDAPMessage processModifyDNRequest(final int messageID, 457 @NotNull final ModifyDNRequestProtocolOp request, 458 @NotNull final List<Control> controls) 459 { 460 final List<String> lineList = getLineList(messageID); 461 462 final String requestID = "conn" + clientConnection.getConnectionID() + 463 "Msg" + messageID + "ModifyDN"; 464 final ModifyDNRequest modifyDNRequest = 465 request.toModifyDNRequest(getControlArray(controls)); 466 modifyDNRequest.toCode(lineList, requestID, 0, includeProcessing); 467 writeLines(lineList); 468 469 return requestHandler.processModifyDNRequest(messageID, request, controls); 470 } 471 472 473 474 /** 475 * {@inheritDoc} 476 */ 477 @Override() 478 @NotNull() 479 public LDAPMessage processSearchRequest(final int messageID, 480 @NotNull final SearchRequestProtocolOp request, 481 @NotNull final List<Control> controls) 482 { 483 final List<String> lineList = getLineList(messageID); 484 485 final String requestID = "conn" + clientConnection.getConnectionID() + 486 "Msg" + messageID + "Search"; 487 final SearchRequest searchRequest = 488 request.toSearchRequest(getControlArray(controls)); 489 searchRequest.toCode(lineList, requestID, 0, includeProcessing); 490 writeLines(lineList); 491 492 return requestHandler.processSearchRequest(messageID, request, controls); 493 } 494 495 496 497 /** 498 * {@inheritDoc} 499 */ 500 @Override() 501 public void processUnbindRequest(final int messageID, 502 @NotNull final UnbindRequestProtocolOp request, 503 @NotNull final List<Control> controls) 504 { 505 // The LDAP SDK doesn't provide an UnbindRequest object, because it is not 506 // possible to separate an unbind request from a connection closure, which 507 // is done by using LDAPConnection.close method. That falls under the 508 // "processing" umbrella, so we'll only log something if we should include 509 // processing details. 510 if (includeProcessing) 511 { 512 final List<String> lineList = getLineList(messageID); 513 514 final ArrayList<ToCodeArgHelper> args = new ArrayList<>(1); 515 if (! controls.isEmpty()) 516 { 517 final Control[] controlArray = new Control[controls.size()]; 518 controls.toArray(controlArray); 519 args.add(ToCodeArgHelper.createControlArray(controlArray, 520 "Request Controls")); 521 } 522 523 ToCodeHelper.generateMethodCall(lineList, 0, null, null, 524 "connection.close", args); 525 526 writeLines(lineList); 527 } 528 529 requestHandler.processUnbindRequest(messageID, request, controls); 530 } 531 532 533 534 /** 535 * Retrieves a list to use to hold the lines of output. It will include 536 * comments with information about the client that submitted the request. 537 * 538 * @param messageID The message ID for the associated request. 539 * 540 * @return A list to use to hold the lines of output. 541 */ 542 @NotNull() 543 private List<String> getLineList(final int messageID) 544 { 545 // Get a thread-local string list, creating it if necessary. 546 List<String> lineList = lineLists.get(); 547 if (lineList == null) 548 { 549 lineList = new ArrayList<>(20); 550 lineLists.set(lineList); 551 } 552 else 553 { 554 lineList.clear(); 555 } 556 557 558 // Add the appropriate header content to the list. 559 lineList.add("// Time: " + new Date()); 560 lineList.add("// Client Address: " + 561 clientConnection.getSocket().getInetAddress().getHostAddress() + ':' + 562 clientConnection.getSocket().getPort()); 563 lineList.add("// Server Address: " + 564 clientConnection.getSocket().getLocalAddress().getHostAddress() + ':' + 565 clientConnection.getSocket().getLocalPort()); 566 lineList.add("// Connection ID: " + clientConnection.getConnectionID()); 567 lineList.add("// Message ID: " + messageID); 568 569 return lineList; 570 } 571 572 573 574 /** 575 * Writes the lines contained in the provided list to the output stream. 576 * 577 * @param lineList The list containing the lines to be written. 578 */ 579 private void writeLines(@NotNull final List<String> lineList) 580 { 581 synchronized (logStream) 582 { 583 if (! firstMessage.compareAndSet(true, false)) 584 { 585 logStream.println(); 586 logStream.println(); 587 } 588 589 for (final String s : lineList) 590 { 591 logStream.println(s); 592 } 593 } 594 } 595 596 597 598 /** 599 * Converts the provided list of controls into an array of controls. 600 * 601 * @param controls The list of controls to convert to an array. 602 * 603 * @return An array of controls that corresponds to the provided list. 604 */ 605 @NotNull() 606 private static Control[] getControlArray( 607 @Nullable final List<Control> controls) 608 { 609 if ((controls == null) || controls.isEmpty()) 610 { 611 return StaticUtils.NO_CONTROLS; 612 } 613 614 final Control[] controlArray = new Control[controls.size()]; 615 return controls.toArray(controlArray); 616 } 617}