diff --git a/lib/theme_store/git_importer.rb b/lib/theme_store/git_importer.rb index f3cf368dd8..93878b4e21 100644 --- a/lib/theme_store/git_importer.rb +++ b/lib/theme_store/git_importer.rb @@ -8,8 +8,7 @@ class ThemeStore::GitImporter attr_reader :url def initialize(url, private_key: nil, branch: nil) - @url = url - @clone_url = GitUrl.normalize(url) + @url = GitUrl.normalize(url) @temp_folder = "#{Pathname.new(Dir.tmpdir).realpath}/discourse_theme_#{SecureRandom.hex}" @private_key = private_key @branch = branch @@ -84,12 +83,12 @@ class ThemeStore::GitImporter def clone! begin - @clone_uri = URI.parse(@clone_url) + @uri = URI.parse(@url) rescue URI::Error raise_import_error! end - case @clone_uri&.scheme + case @uri&.scheme when "http", "https" clone_http! when "ssh" @@ -109,49 +108,53 @@ class ThemeStore::GitImporter args << "clone" if @branch.present? - args.concat(["--single-branch", "-b", @branch]) + args.concat(["-b", @branch]) end - args.concat([@clone_url, @temp_folder]) + args.concat([@url, @temp_folder]) args end def clone_http! + uris = [@uri] + begin - @clone_uri = FinalDestination.resolve(@clone_uri.to_s) + resolved_uri = FinalDestination.resolve(@uri.to_s) + if resolved_uri && resolved_uri != @uri + uris.unshift(resolved_uri) + end rescue - raise_import_error! + # If this fails, we can stil attempt to clone using the original URI end - @clone_url = @clone_uri.to_s + uris.each do |uri| + @uri = uri + @url = @uri.to_s - unless ["http", "https"].include?(@clone_uri.scheme) - raise_import_error! + unless ["http", "https"].include?(@uri.scheme) + raise_import_error! + end + + addresses = FinalDestination::SSRFDetector.lookup_and_filter_ips(@uri.host) + + unless addresses.empty? + env = { "GIT_TERMINAL_PROMPT" => "0" } + + args = clone_args( + "http.followRedirects" => "false", + "http.curloptResolve" => "#{@uri.host}:#{@uri.port}:#{addresses.join(',')}", + ) + + begin + Discourse::Utils.execute_command(env, *args, timeout: COMMAND_TIMEOUT_SECONDS) + return + rescue RuntimeError + end + end end - begin - addresses = FinalDestination::SSRFDetector.lookup_and_filter_ips(@clone_uri.host) - rescue FinalDestination::SSRFDetector::DisallowedIpError - raise_import_error! - end - - if addresses.empty? - raise_import_error! - end - - env = { "GIT_TERMINAL_PROMPT" => "0" } - - args = clone_args( - "http.followRedirects" => "false", - "http.curloptResolve" => "#{@clone_uri.host}:#{@clone_uri.port}:#{addresses.join(',')}", - ) - - begin - Discourse::Utils.execute_command(env, *args, timeout: COMMAND_TIMEOUT_SECONDS) - rescue RuntimeError - raise_import_error! - end + raise_import_error! end def clone_ssh! @@ -162,31 +165,13 @@ class ThemeStore::GitImporter with_ssh_private_key do |ssh_folder| # Use only the specified SSH key env = { 'GIT_SSH_COMMAND' => "ssh -i #{ssh_folder}/id_rsa -o IdentitiesOnly=yes -o IdentityFile=#{ssh_folder}/id_rsa -o StrictHostKeyChecking=no" } + args = clone_args begin - addresses = FinalDestination::SSRFDetector.lookup_and_filter_ips(@clone_uri.host) - rescue FinalDestination::SSRFDetector::DisallowedIpError + Discourse::Utils.execute_command(env, *args, timeout: COMMAND_TIMEOUT_SECONDS) + rescue RuntimeError raise_import_error! end - - timeout_at = Time.zone.now + COMMAND_TIMEOUT_SECONDS - - addresses.each do |address| - remaining_timeout = timeout_at - Time.zone.now - raise_import_error! if remaining_timeout < 0 - - @clone_uri.host = address - @clone_url = @clone_uri.to_s - - args = clone_args - - begin - return Discourse::Utils.execute_command(env, *args, timeout: remaining_timeout) - rescue RuntimeError - end - end - - raise_import_error! end end diff --git a/spec/components/theme_store/git_importer_spec.rb b/spec/components/theme_store/git_importer_spec.rb index ba4e12a67b..2b1af5fd1c 100644 --- a/spec/components/theme_store/git_importer_spec.rb +++ b/spec/components/theme_store/git_importer_spec.rb @@ -5,22 +5,17 @@ require 'rails_helper' require 'theme_store/git_importer' -describe ThemeStore::GitImporter do - - context "#import" do - - let(:http_url) { "http://github.com/example/example.git" } - let(:https_url) { "https://github.com/example/example.git" } +RSpec.describe ThemeStore::GitImporter do + describe "#import" do + let(:url) { "https://github.com/example/example.git" } let(:trailing_slash_url) { "https://github.com/example/example/" } let(:ssh_url) { "git@github.com:example/example.git" } let(:branch) { "dev" } before do - freeze_time hex = "xxx" SecureRandom.stubs(:hex).returns(hex) - FinalDestination.stubs(:resolve).with(http_url).returns(URI.parse(http_url)) - FinalDestination.stubs(:resolve).with(https_url).returns(URI.parse(https_url)) + FinalDestination.stubs(:resolve).with(url).returns(URI.parse(url)) FinalDestination::SSRFDetector.stubs(:lookup_and_filter_ips).with("github.com").returns(["192.0.2.100"]) @temp_folder = "#{Pathname.new(Dir.tmpdir).realpath}/discourse_theme_#{hex}" @ssh_folder = "#{Pathname.new(Dir.tmpdir).realpath}/discourse_theme_ssh_#{hex}" @@ -31,22 +26,10 @@ describe ThemeStore::GitImporter do .expects(:execute_command) .with( { "GIT_TERMINAL_PROMPT" => "0" }, - "git", "-c", "http.followRedirects=false", "-c", "http.curloptResolve=github.com:80:192.0.2.100", "clone", "http://github.com/example/example.git", @temp_folder, { timeout: 20 } + "git", "-c", "http.followRedirects=false", "-c", "http.curloptResolve=github.com:443:192.0.2.100", "clone", "https://github.com/example/example.git", @temp_folder, timeout: 20 ) - importer = ThemeStore::GitImporter.new(http_url) - importer.import! - end - - it "imports https urls" do - Discourse::Utils - .expects(:execute_command) - .with( - { "GIT_TERMINAL_PROMPT" => "0" }, - "git", "-c", "http.followRedirects=false", "-c", "http.curloptResolve=github.com:443:192.0.2.100", "clone", "https://github.com/example/example.git", @temp_folder, { timeout: 20 } - ) - - importer = ThemeStore::GitImporter.new(https_url) + importer = ThemeStore::GitImporter.new(url) importer.import! end @@ -55,7 +38,7 @@ describe ThemeStore::GitImporter do .expects(:execute_command) .with( { "GIT_TERMINAL_PROMPT" => "0" }, - "git", "-c", "http.followRedirects=false", "-c", "http.curloptResolve=github.com:443:192.0.2.100", "clone", "https://github.com/example/example.git", @temp_folder, { timeout: 20 } + "git", "-c", "http.followRedirects=false", "-c", "http.curloptResolve=github.com:443:192.0.2.100", "clone", "https://github.com/example/example.git", @temp_folder, timeout: 20 ) importer = ThemeStore::GitImporter.new(trailing_slash_url) @@ -67,22 +50,22 @@ describe ThemeStore::GitImporter do .expects(:execute_command) .with( { "GIT_SSH_COMMAND" => "ssh -i #{@ssh_folder}/id_rsa -o IdentitiesOnly=yes -o IdentityFile=#{@ssh_folder}/id_rsa -o StrictHostKeyChecking=no" }, - "git", "clone", "ssh://git@192.0.2.100/example/example.git", @temp_folder, { timeout: 20 } + "git", "clone", "ssh://git@github.com/example/example.git", @temp_folder, timeout: 20 ) importer = ThemeStore::GitImporter.new(ssh_url, private_key: "private_key") importer.import! end - it "imports https urls with a particular branch" do + it "imports http urls with a particular branch" do Discourse::Utils .expects(:execute_command) .with( { "GIT_TERMINAL_PROMPT" => "0" }, - "git", "-c", "http.followRedirects=false", "-c", "http.curloptResolve=github.com:443:192.0.2.100", "clone", "--single-branch", "-b", branch, "https://github.com/example/example.git", @temp_folder, { timeout: 20 } + "git", "-c", "http.followRedirects=false", "-c", "http.curloptResolve=github.com:443:192.0.2.100", "clone", "-b", branch, "https://github.com/example/example.git", @temp_folder, timeout: 20 ) - importer = ThemeStore::GitImporter.new(https_url, branch: branch) + importer = ThemeStore::GitImporter.new(url, branch: branch) importer.import! end @@ -91,7 +74,7 @@ describe ThemeStore::GitImporter do .expects(:execute_command) .with( { "GIT_SSH_COMMAND" => "ssh -i #{@ssh_folder}/id_rsa -o IdentitiesOnly=yes -o IdentityFile=#{@ssh_folder}/id_rsa -o StrictHostKeyChecking=no" }, - "git", "clone", "--single-branch", "-b", branch, "ssh://git@192.0.2.100/example/example.git", @temp_folder, { timeout: 20 } + "git", "clone", "-b", branch, "ssh://git@github.com/example/example.git", @temp_folder, timeout: 20 ) importer = ThemeStore::GitImporter.new(ssh_url, private_key: "private_key", branch: branch)