Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package org.jumpserver.chen.framework.console;

import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.SQLUtils;

import java.sql.SQLException;

/**
* Validates that raw console input is one database command without rewriting it.
*/
final class ConsoleStatementBoundaryScanner {
private static final String MULTIPLE_STATEMENTS_ERROR =
"Console raw execution accepts only one top-level database command";

private ConsoleStatementBoundaryScanner() {
}

static void requireSingleStatement(String sql, DbType dbType) throws SQLException {
if (sql == null || sql.isBlank()) {
throw new SQLException("Console raw execution requires a database command");
}

try {
var statements = SQLUtils.parseStatements(sql, dbType);
if (statements.size() == 1) {
return;
}
if (statements.size() > 1) {
throw new SQLException(MULTIPLE_STATEMENTS_ERROR);
}
} catch (RuntimeException ignored) {
// Raw mode exists for commands the dialect parser does not understand. The conservative
// fallback below recognizes only top-level delimiters and keeps quoted/dollar-quoted
// procedure bodies opaque; it never changes the command sent to the JDBC driver.
}

requireSingleOpaqueStatement(sql);
}

private static void requireSingleOpaqueStatement(String sql) throws SQLException {
int completedStatements = 0;
boolean hasStatementContent = false;
int blockCommentDepth = 0;
char quote = 0;
String dollarQuote = null;

for (int index = 0; index < sql.length(); index++) {
char current = sql.charAt(index);
char next = index + 1 < sql.length() ? sql.charAt(index + 1) : 0;

if (dollarQuote != null) {
if (sql.startsWith(dollarQuote, index)) {
index += dollarQuote.length() - 1;
dollarQuote = null;
}
continue;
}

if (quote != 0) {
if (current == '\\' && index + 1 < sql.length()) {
index++;
continue;
}
if (current == quote) {
if (next == quote) {
index++;
} else {
quote = 0;
}
}
continue;
}

if (blockCommentDepth > 0) {
if (current == '/' && next == '*') {
blockCommentDepth++;
index++;
} else if (current == '*' && next == '/') {
blockCommentDepth--;
index++;
}
continue;
}

if (current == '-' && next == '-') {
index = skipLineComment(sql, index + 2);
continue;
}
if (current == '/' && next == '*') {
blockCommentDepth = 1;
index++;
continue;
}
if (current == '\'' || current == '"' || current == '`') {
quote = current;
hasStatementContent = true;
continue;
}
if (current == '$') {
String delimiter = dollarQuoteDelimiter(sql, index);
if (delimiter != null) {
dollarQuote = delimiter;
hasStatementContent = true;
index += delimiter.length() - 1;
continue;
}
}
if (current == ';') {
if (hasStatementContent) {
completedStatements++;
hasStatementContent = false;
if (completedStatements > 1) {
throw new SQLException(MULTIPLE_STATEMENTS_ERROR);
}
}
continue;
}
if (!Character.isWhitespace(current)) {
if (completedStatements > 0) {
throw new SQLException(MULTIPLE_STATEMENTS_ERROR);
}
hasStatementContent = true;
}
}

if (hasStatementContent) {
completedStatements++;
}
if (completedStatements == 0) {
throw new SQLException("Console raw execution requires a database command");
}
if (completedStatements > 1) {
throw new SQLException(MULTIPLE_STATEMENTS_ERROR);
}
}

private static int skipLineComment(String sql, int index) {
while (index < sql.length()) {
char current = sql.charAt(index);
if (current == '\n' || current == '\r') {
return index;
}
index++;
}
return sql.length() - 1;
}

private static String dollarQuoteDelimiter(String sql, int start) {
if (start > 0 && isIdentifierPart(sql.charAt(start - 1))) {
return null;
}
int end = sql.indexOf('$', start + 1);
if (end < 0) {
return null;
}
String tag = sql.substring(start + 1, end);
if (!tag.isEmpty()) {
if (!isTagStart(tag.charAt(0))) {
return null;
}
for (int index = 1; index < tag.length(); index++) {
if (!isTagPart(tag.charAt(index))) {
return null;
}
}
}
String delimiter = sql.substring(start, end + 1);
return sql.indexOf(delimiter, end + 1) >= 0 ? delimiter : null;
}

private static boolean isTagStart(char value) {
return value == '_' || Character.isLetter(value);
}

private static boolean isTagPart(char value) {
return isTagStart(value) || Character.isDigit(value);
}

private static boolean isIdentifierPart(char value) {
return isTagPart(value) || value == '$';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.jumpserver.chen.framework.datasource.sql.SQL;
import org.jumpserver.chen.framework.datasource.sql.SQLActuator;
import org.jumpserver.chen.framework.datasource.sql.SQLExecutePlan;
import org.jumpserver.chen.framework.datasource.sql.SQLQueryResult;
import org.jumpserver.chen.framework.i18n.MessageUtils;
import org.jumpserver.chen.framework.jms.acl.ACLResult;
import org.jumpserver.chen.framework.jms.entity.CommandRecord;
Expand All @@ -52,6 +53,7 @@
import java.nio.file.StandardOpenOption;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -99,7 +101,7 @@ public class QueryConsole extends AbstractConsole {
// SQL Server, Dameng or DB2, so the server-side probe is the real source of truth for whether
// a user transaction is open before a DataView save commits or stages behind a savepoint.
private volatile QueryTransactionStateInspector transactionStateInspector;
private volatile SQLExecutePlan currentPlan;
private volatile ActiveExecution currentExecution;
private StateManager<QueryConsoleState> stateManager;
private final Map<String, DataView> dataViews = new LinkedHashMap<>();
// Manual context changes remain restricted to values returned by the current server-side actuator.
Expand Down Expand Up @@ -633,10 +635,10 @@ private SaveChangesResult rejectedSave(DataView dataView, String reason) {
public void onCancel() {
this.getState().setExecutionStatus(EXECUTION_STATUS_CANCELLED);
try {
var plan = this.currentPlan;
if (plan != null) {
plan.cancel();
this.getConsoleLogger().warn("cancel query: %s", plan.getTargetSQL());
var execution = this.currentExecution;
if (execution != null) {
execution.cancelAction().cancel();
this.getConsoleLogger().warn("cancel query: %s", execution.sql());
}
} catch (SQLException | RuntimeException e) {
log.error("cancel failed ", e);
Expand Down Expand Up @@ -767,22 +769,10 @@ public void onSQL(String sql) {
var session = SessionManager.getCurrentSession();

try {
var stmts = this.getSqlActuator().parseSQL(SQL.of(sql));
var clearOthers = true;
for (String stmt : stmts) {
var aclResult = session.checkACL(stmt, this.getConnection());
if (!this.canExecuteStatement(session, stmt, aclResult)) {
break;
}
var dataView = this.runSingleSQL(stmt, aclResult);
if (!dataView.isHasTable()) {
this.getConsoleLogger().success("%s , %s: %d",
MessageUtils.get("ExecuteSuccess"),
MessageUtils.get("AffectedRows"), dataView.getUpdateCount());
} else {
this.sendDataView(dataView, clearOthers);
clearOthers = false;
}
if (this.consoleMode) {
this.runRawConsoleSQL(sql, session);
} else {
this.runQuerySQL(sql, session);
}
this.ensureCurrentSchema();
} catch (ParserException e) {
Expand All @@ -807,6 +797,80 @@ public void onSQL(String sql) {
}
}

private void runQuerySQL(String sql, Session session) throws SQLException {
var statements = this.getSqlActuator().parseSQL(SQL.of(sql));
var clearOthers = true;
for (String statement : statements) {
var aclResult = session.checkACL(statement, this.getConnection());
if (!this.canExecuteStatement(session, statement, aclResult)) {
break;
}
var dataView = this.runSingleSQL(statement, aclResult);
if (!dataView.isHasTable()) {
this.logAffectedRows(dataView);
} else {
this.sendDataView(dataView, clearOthers);
clearOthers = false;
}
}
}

private void runRawConsoleSQL(String sql, Session session) throws SQLException {
ConsoleStatementBoundaryScanner.requireSingleStatement(sql, this.datasource.getDruidDbType());
Connection connection = this.getConnection();
ACLResult aclResult = session.checkACL(sql, connection);
if (!this.canExecuteStatement(session, sql, aclResult)) {
return;
}

SQLQueryResult executionResult = this.executeRawConsoleSQL(sql, aclResult, connection);
if (executionResult.getResults().isEmpty()) {
this.getConsoleLogger().success("%s", MessageUtils.get("ExecuteSuccess"));
}
for (SQLQueryResult result : executionResult.getResults()) {
this.getConsoleLogger().success(result);
if (!result.isHasResultSet()) {
continue;
}

DataView dataView = new DataView(
UUID.randomUUID().toString(), sql, this.getPacketIO(), this.getConsoleLogger()
);
dataView.setSql(sql);
dataView.setLoadDataInterface((ignored) -> result);
dataView.loadData();
this.sendDataView(dataView, false);
}
}

private SQLQueryResult executeRawConsoleSQL(String sql, ACLResult aclResult, Connection connection)
throws SQLException {
SQLActuator actuator = this.datasource.getConnectionManager().getSqlActuator().withConnection(connection);
SQLExecutePlan plan = actuator.createPlan(SQL.of(sql));
plan.setAclResult(aclResult);
Statement statement = plan.createStatement();
ActiveExecution execution = new ActiveExecution(sql, statement::cancel);
this.currentExecution = execution;
this.getState().setCanCancel(true);
this.stateManager.commit();
try {
this.getConsoleLogger().info("execute sql: %s", sql);
return actuator.executeRawWithAudit(plan);
} finally {
if (this.currentExecution == execution) {
this.currentExecution = null;
}
this.getState().setCanCancel(false);
this.stateManager.commit();
}
}

private void logAffectedRows(DataView dataView) {
this.getConsoleLogger().success("%s , %s: %d",
MessageUtils.get("ExecuteSuccess"),
MessageUtils.get("AffectedRows"), dataView.getUpdateCount());
}

private void sendSQLError(String kind, String title, String message, String sql, SQLException exception) {
var error = new LinkedHashMap<String, Object>();
error.put("kind", kind);
Expand Down Expand Up @@ -928,7 +992,8 @@ private DataView runSingleSQL(String sql, ACLResult aclResult) throws SQLExcepti
.createPlan(SQL.of(sourceSQL));
plan.setAclResult(aclResult);
plan.setSqlQueryParams(sqlQueryParams);
this.currentPlan = plan;
ActiveExecution execution = new ActiveExecution(sourceSQL, plan::cancel);
this.currentExecution = execution;
this.getState().setCanCancel(true);
this.stateManager.commit();

Expand All @@ -939,7 +1004,9 @@ private DataView runSingleSQL(String sql, ACLResult aclResult) throws SQLExcepti
this.getConsoleLogger().success(result);
return result;
} finally {
this.currentPlan = null;
if (this.currentExecution == execution) {
this.currentExecution = null;
}
this.getState().setCanCancel(false);
this.stateManager.commit();
}
Expand Down Expand Up @@ -1019,12 +1086,12 @@ public void close() {
currentSession.getController().cancelDialogs(this.getPacketIO().getWsSession().getId());
}

var plan = this.currentPlan;
if (plan != null) {
var execution = this.currentExecution;
if (execution != null) {
try {
// flush
var session = SessionManager.getCurrentSession();
var lastCmd = plan.getTargetSQL();
var lastCmd = execution.sql();
var cmdRecord = new CommandRecord(lastCmd);
cmdRecord.setError("Abnormal exit");
if (session != null) {
Expand All @@ -1044,4 +1111,12 @@ public void close() {
}
log.info("console closed");
}

@FunctionalInterface
private interface ExecutionCancel {
void cancel() throws SQLException;
}

private record ActiveExecution(String sql, ExecutionCancel cancelAction) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ private void fullData(SQLQueryResult result) {
}

this.state.setPaged(result.isPaged());
this.state.setTruncated(result.isTruncated());
this.state.setRowLimit(result.getRowLimit());

this.data.getFields().clear();
this.data.getData().clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class DataViewState extends State {
private int page;
private int limit;
private int total;
private boolean truncated;
private int rowLimit;
private boolean pinned;
private boolean paged;
private String filter;
Expand Down
Loading