View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    *
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   *******************************************************************************/
11  package com.jsql.view.swing.terminal;
12  
13  import com.jsql.util.LogLevelUtil;
14  import org.apache.commons.lang3.StringUtils;
15  import org.apache.logging.log4j.LogManager;
16  import org.apache.logging.log4j.Logger;
17  
18  import javax.swing.*;
19  import javax.swing.text.BadLocationException;
20  import javax.swing.text.Element;
21  import java.awt.event.InputEvent;
22  import java.awt.event.KeyAdapter;
23  import java.awt.event.KeyEvent;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.concurrent.atomic.AtomicReference;
27  
28  /**
29   * Keyboard key processing for terminal.
30   */
31  public class KeyAdapterTerminal extends KeyAdapter {
32      
33      /**
34       * Log4j logger sent to view.
35       */
36      private static final Logger LOGGER = LogManager.getRootLogger();
37  
38      /**
39       * Terminal where keys are processed.
40       */
41      private final AbstractExploit terminal;
42  
43      /**
44       * Past commands entered by user.
45       */
46      private final List<String> commandsHistory = new ArrayList<>();
47  
48      /**
49       * Current position in array of past commands.
50       */
51      private int indexCommandsHistory = 0;
52  
53      /**
54       * Create a keyboard processor for a terminal.
55       * @param terminal Terminal where keys are processed
56       */
57      public KeyAdapterTerminal(AbstractExploit terminal) {
58          this.terminal = terminal;
59      }
60  
61      @Override
62      public void keyPressed(KeyEvent keyEvent) {
63          try {
64              var root = this.terminal.getDocument().getDefaultRootElement();
65              int caretPosition = this.terminal.getCaretPosition();
66              int lineNumber = this.terminal.getLineOfOffset(caretPosition);  // Get current line
67  
68              if (this.terminal.getIsEdited().get()) {  // Cancel every user keyboard input if another command has just been sent
69                  keyEvent.consume();
70                  return;
71              }
72      
73              // Get user input
74              var command = new AtomicReference<>(StringUtils.EMPTY);
75              command.set(
76                  this.terminal.getText(
77                      root.getElement(lineNumber).getStartOffset(),
78                      root.getElement(lineNumber).getEndOffset() - root.getElement(lineNumber).getStartOffset()
79                  )
80                  .replace(this.terminal.getPrompt(), StringUtils.EMPTY)
81                  .trim()
82              );
83      
84              if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER) {  // Validate user input ; disable text editing
85                  this.runCommand(keyEvent, command);
86              } else if (keyEvent.getKeyCode() == KeyEvent.VK_UP) {  // Get previous command
87                  this.appendPreviousCommand(keyEvent, root, lineNumber, command);
88              } else if (keyEvent.getKeyCode() == KeyEvent.VK_DOWN) {  // Get next command
89                  this.appendNextCommand(keyEvent, root, lineNumber, command);
90              } else if (keyEvent.getKeyCode() == KeyEvent.VK_LEFT || keyEvent.getKeyCode() == KeyEvent.VK_BACK_SPACE) {  // Go to the left until prompt
91                  this.moveCaretLeft(keyEvent, caretPosition, lineNumber);
92              } else if (keyEvent.getKeyCode() == KeyEvent.VK_HOME) {  // Get to the beginning of the line
93                  this.moveCaretHome(keyEvent, lineNumber);
94              } else if (this.isKeyNotAllowed(keyEvent, caretPosition)) {
95                  keyEvent.consume();
96              } else if (keyEvent.getKeyCode() == KeyEvent.VK_C && (keyEvent.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) {
97                  this.cancelCommand(keyEvent);
98              }
99          } catch (BadLocationException e) {
100             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
101         }
102     }
103 
104     private boolean isKeyNotAllowed(KeyEvent keyEvent, int caretPosition) {
105         return
106             // Cancel the select all shortcut Ctrl+A
107             keyEvent.getKeyCode() == KeyEvent.VK_A && (keyEvent.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0
108             // Cancel the *beep* sound if deleting while at the end of line
109             || keyEvent.getKeyCode() == KeyEvent.VK_DELETE && caretPosition == this.terminal.getDocument().getLength()
110             || (keyEvent.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0 && (keyEvent.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0
111             || keyEvent.getKeyCode() == KeyEvent.VK_PAGE_UP
112             || keyEvent.getKeyCode() == KeyEvent.VK_PAGE_DOWN
113             || keyEvent.getKeyCode() == KeyEvent.VK_TAB;
114     }
115 
116     private void cancelCommand(KeyEvent keyEvent) {
117         keyEvent.consume();
118         this.terminal.append("\n");
119         this.terminal.reset();
120     }
121 
122     private void moveCaretHome(KeyEvent keyEvent, int lineNumber) throws BadLocationException {
123         keyEvent.consume();
124         this.terminal.setCaretPosition(this.terminal.getLineStartOffset(lineNumber) + this.terminal.getPrompt().length());
125     }
126 
127     private void moveCaretLeft(KeyEvent keyEvent, int caretPosition, int lineNumber) throws BadLocationException {
128         int newCaretPosition = caretPosition - this.terminal.getLineStartOffset(lineNumber);
129         if (newCaretPosition <= this.terminal.getPrompt().length()) {
130             keyEvent.consume();
131         }
132     }
133 
134     private void appendNextCommand(
135         KeyEvent keyEvent,
136         Element root,
137         int lineNumber,
138         AtomicReference<String> command
139     ) throws BadLocationException {
140         keyEvent.consume();
141    
142         if (this.indexCommandsHistory < this.commandsHistory.size()) {
143             this.indexCommandsHistory++;
144         }
145    
146         if (!this.commandsHistory.isEmpty() && this.indexCommandsHistory < this.commandsHistory.size()) {
147             this.terminal.getDocument().remove(
148                 root.getElement(lineNumber).getStartOffset() + this.terminal.getPrompt().length(),
149                 command.get().length() - 1
150             );
151    
152             this.terminal.append(this.commandsHistory.get(this.indexCommandsHistory));
153             this.terminal.setCaretPosition(this.terminal.getDocument().getLength());
154         }
155     }
156 
157     private void appendPreviousCommand(
158         KeyEvent keyEvent,
159         Element root,
160         int lineNumber,
161         AtomicReference<String> command
162     ) throws BadLocationException {
163         keyEvent.consume();
164    
165         if (this.indexCommandsHistory > 0) {
166             this.indexCommandsHistory--;
167         }
168    
169         if (!this.commandsHistory.isEmpty()) {
170             if (
171                 this.commandsHistory.size() > 1
172                 && this.indexCommandsHistory == this.commandsHistory.size() - 1
173                 && StringUtils.isNotEmpty(command.get())
174             ) {
175                 this.indexCommandsHistory--;
176             }
177    
178             this.terminal.getDocument().remove(
179                 root.getElement(lineNumber).getStartOffset() + this.terminal.getPrompt().length(),
180                 command.get().length() - 1
181             );
182    
183             this.terminal.append(this.commandsHistory.get(this.indexCommandsHistory));
184             this.terminal.setCaretPosition(this.terminal.getDocument().getLength());
185         }
186     }
187 
188     private void runCommand(KeyEvent keyEvent, AtomicReference<String> command) {
189         this.terminal.getIsEdited().set(true);
190         keyEvent.consume();
191         this.terminal.setEditable(false);
192    
193         // Populate cmd list for key up/down
194         if (StringUtils.isNotEmpty(command.get())) {
195             this.commandsHistory.add(command.get());
196             this.indexCommandsHistory = this.commandsHistory.size();
197         }
198 
199         new SwingWorker<>() {  // Thread to give back control to user (SwingUtilities does not)
200             @Override
201             protected Object doInBackground() {
202                 // Inside Swing thread to avoid flickering
203                 Thread.currentThread().setName("SwingWorkerKeyAdapterTerminal");
204 
205                 AbstractExploit terminalCommand = KeyAdapterTerminal.this.terminal;
206                 terminalCommand.append("\n");
207                 
208                 if (StringUtils.isNotEmpty(command.get())) {
209                     terminalCommand.setCaretPosition(terminalCommand.getDocument().getLength());
210                     terminalCommand.action(
211                         command.get(),
212                         terminalCommand.getUuidShell(),
213                         terminalCommand.getUrlShell(),
214                         terminalCommand.loginPassword
215                     );
216                 } else {
217                     terminalCommand.reset();
218                 }
219                 return null;
220             }
221         }.execute();
222     }
223 }