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