app/periscope/periscope_controller.rb in periscope_rails-0.0.4 vs app/periscope/periscope_controller.rb in periscope_rails-0.0.5

- old
+ new

@@ -1,76 +1,83 @@ -class PeriscopeController < ApplicationController - before_filter :authenticate - protect_from_forgery :except => [:look, :login] - - def look - if !params[:sql].nil? - render :json => run_sql(params[:sql]) - else - render :json => {:error => "Command not understood"} - end - end - - def login - render :json => get_info() - end - - private - - def authenticate - unless PeriscopeRails::Config.check_password(params[:password].to_s) - render :json => {:error => "Password invalid."} - end - end - - def run_sql(sql_command) - #TODO: protect based on CFG, not blacklist - bad_words = %W{drop delete update into insert index add remove grant revoke create createdb} - bad_words += %W{createuser createrole destroy disconnect exec execute dropdb primary key rollback ; --} - - rows = nil - error_message = nil - command = sql_command.to_s.strip - command_words = command.downcase.gsub(/[^a-zA-Z0-9]/, " ").gsub(/\s+/, " ").split(" ") - if command == "" - #nothing - elsif (command_words & bad_words).size > 0 - error_message = "Potentially harmful keyword found, blocking script." - else - begin - #custom_db_creds = PeriscopeRails::Config.get_db_creds() - #if custom_db_creds.nil? - # active_record = ActiveRecord::Base - #else - # # TODO: Establish this connection once rather than every time a query is issued - # active_record = Class.new(ActiveRecord::Base) - # custom_db_config = ActiveRecord::Base.connection_config.merge(custom_db_creds) - # active_record.establish_connection(custom_db_config) - #end - active_record = PeriscopeRails::Config.get_active_record() - active_record.transaction do - rows = active_record.connection.select_all(command) - rows.each do |row| - row.each_key do |column| - if PeriscopeRails::Config.matches_filter(column) - row[column] = '[FILTERED]' - end - end - end - raise "OK" #abort all transactions for extra protection - end - rescue Exception => e - error_message = e.message unless e.message == "OK" - end - end - return {:error => error_message, :data => rows} - end - - def get_info - tables = [] - table_names = ActiveRecord::Base.connection.tables.sort - table_names.each do |table_name| - tables << {:name => table_name, :columns => ActiveRecord::Base.connection.columns(table_name)} - end - return {:tables => tables, :error => nil} - end -end +class PeriscopeController < ActionController::Base + before_filter :authenticate + protect_from_forgery :except => [:look, :login] + + def look + if !params[:sql].nil? + render :json => run_sql(params[:sql]) + else + render :json => {:error => "Command not understood"} + end + end + + def login + render :json => get_info() + end + + private + + def authenticate + unless PeriscopeRails::Config.check_password(params[:password].to_s) + render :json => {:error => "Password invalid."} + end + end + + def run_sql(sql_command) + #TODO: protect based on CFG, not blacklist + bad_words = %W{drop delete update into insert index add remove grant revoke create createdb} + bad_words += %W{createuser createrole destroy disconnect exec execute dropdb primary key rollback ; --} + + rows = nil + error_message = nil + command = sql_command.to_s.strip + command_words = command.downcase.gsub(/[^a-zA-Z0-9]/, " ").gsub(/\s+/, " ").split(" ") + if command == "" + #nothing + elsif (command_words & bad_words).size > 0 + error_message = "Potentially harmful keyword found, blocking script." + else + begin + active_record = PeriscopeRails::Config.get_active_record() + active_record.transaction do + if PeriscopeRails::Config.block_expensive_queries? + cost_row = "" + begin + cost_row = active_record.connection.select_all("explain #{command}")[0]["QUERY PLAN"] + rescue + puts "Warning: Periscope was unable to cost this query (1): #{command}" + end + cost_row =~ /rows=(\d+) width=(\d+)\)$/ + row_count, width = $1.to_i, $2.to_i + if row_count > 0 and width > 0 + raise "Command blocked, it may be too slow. Estimated at #{row_count} rows, commands must return fewer than #{PeriscopeRails::Config.max_rows} rows." if row_count > PeriscopeRails::Config.max_rows + raise "Command blocked, it may be too slow. Estimated at #{row_count * width} bytes, commands use less than #{PeriscopeRails::Config.max_size} bytes." if row_count * width > PeriscopeRails::Config.max_size + else + puts "Warning: Periscope was unable to cost this query (2): #{command}" + end + end + rows = active_record.connection.select_all(command) + rows.each do |row| + row.each_key do |column| + if PeriscopeRails::Config.matches_filter(column) + row[column] = '[FILTERED]' + end + end + end + raise "OK" #abort all transactions for extra protection + end + rescue Exception => e + error_message = e.message unless e.message == "OK" + end + end + return {:error => error_message, :data => rows} + end + + def get_info + tables = [] + table_names = ActiveRecord::Base.connection.tables.sort + table_names.each do |table_name| + tables << {:name => table_name, :columns => ActiveRecord::Base.connection.columns(table_name)} + end + return {:tables => tables, :error => nil} + end +end