class PeriscopeController < ActionController::Base before_filter :authenticate protect_from_forgery :except => [:look, :login] def look if !params[:sql].nil? render :json => merge_on_metadata(run_sql(params[:sql])) else render :json => merge_on_metadata({:error => "Command not understood"}) end end def login render :json => merge_on_metadata(get_info()) end private def authenticate unless PeriscopeRails::Config.check_password(params[:password].to_s) render :json => merge_on_metadata({:error => "Password invalid."}) end end def merge_on_metadata(hash) version = nil begin version = Gem.loaded_specs["periscope_rails"].version.version rescue version = "error" end return hash.merge({ :version => version, :database_type => get_db_type }) 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 #for whole query active_record = PeriscopeRails::Config.get_active_record() begin #just for costing active_record.transaction do #costing if PeriscopeRails::Config.block_expensive_queries? and get_db_type == "postgres" active_record.connection.select_all("explain #{command}")[0]["QUERY PLAN"] =~ /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 raise "OK" #abort all transactions for extra protection end rescue Exception => e puts "Warning: Periscope was unable to cost this query (1): #{command}" unless e.message == "OK" raise e if e.message.include?("Command blocked") end active_record.transaction do #execution 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 def get_db_type begin return "postgres" if ActiveRecord::Base.connection.instance_of? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter rescue Exception => e begin return "mysql" if ActiveRecord::Base.connection.instance_of? ActiveRecord::ConnectionAdapters::MysqlAdapter rescue Exception => e begin return "mysql" if ActiveRecord::Base.connection.instance_of? ActiveRecord::ConnectionAdapters::Mysql2Adapter rescue return nil end end end end end