Prohlížeč zdrojového kódu
app/services/source_browser.rb
class SourceBrowser
class NotAllowed < StandardError; end
BINARY_EXTENSIONS = %w[.png .jpg .jpeg .gif .ico .webp .woff .woff2 .ttf .eot .pdf].freeze
class << self
def list(relative_dir = "")
validate_path!(relative_dir) if relative_dir.present?
allowed = whitelisted_files
prefix = relative_dir.present? ? "#{relative_dir}/" : ""
entries = Set.new
allowed.each do |file|
next unless file.start_with?(prefix)
remainder = file.delete_prefix(prefix)
top = remainder.split("/").first
full = prefix + top
is_dir = remainder.include?("/")
entries << { name: top, path: full, directory: is_dir }
end
entries.sort_by { |e| [e[:directory] ? 0 : 1, e[:name]] }
end
def read(relative_path)
validate_path!(relative_path)
unless whitelisted_files.include?(relative_path)
raise NotAllowed, "File not in whitelist"
end
full_path = Rails.root.join(relative_path)
raise NotAllowed, "File not found" unless File.file?(full_path)
File.read(full_path)
end
def file?(relative_path)
whitelisted_files.include?(relative_path)
end
def binary?(relative_path)
BINARY_EXTENSIONS.include?(File.extname(relative_path).downcase)
end
def full_path(relative_path)
validate_path!(relative_path)
raise NotAllowed, "File not in whitelist" unless whitelisted_files.include?(relative_path)
full = Rails.root.join(relative_path)
raise NotAllowed, "File not found" unless File.file?(full)
full.to_s
end
def tree
root = { dirs: {}, files: [] }
whitelisted_files.to_a.sort.each do |path|
parts = path.split("/")
node = root
parts.each_with_index do |part, i|
if i == parts.length - 1
node[:files] << { name: part, path: path, binary: binary?(path) }
else
node[:dirs][part] ||= { name: part, path: parts[0..i].join("/"), dirs: {}, files: [] }
node = node[:dirs][part]
end
end
end
root
end
def language_for(path)
basename = File.basename(path)
return "ruby" if %w[Gemfile Rakefile Guardfile].include?(basename)
case File.extname(path)
when ".rb" then "ruby"
when ".erb" then "erb"
when ".yml", ".yaml" then "yaml"
when ".js" then "javascript"
when ".css" then "css"
when ".scss" then "scss"
else "plaintext"
end
end
private
def validate_path!(path)
raise NotAllowed, "Invalid path" if path.nil?
raise NotAllowed, "Null bytes not allowed" if path.include?("\0")
raise NotAllowed, "Invalid path" if path.include?("..")
expanded = File.expand_path(path, Rails.root)
root = Rails.root.to_s
unless expanded.start_with?("#{root}/")
raise NotAllowed, "Path traversal detected"
end
end
def whitelisted_files
@whitelisted_files ||= compute_whitelist
end
def compute_whitelist
globs = Rails.application.config.source_browser_whitelist
root = Rails.root.to_s
files = Set.new
globs.each do |glob|
Dir[Rails.root.join(glob)].each do |full_path|
next unless File.file?(full_path)
files << full_path.delete_prefix("#{root}/")
end
end
files
end
end
# Reload whitelist cache in development
def self.reload!
@whitelisted_files = nil
end
end