From fada57a307d66d696d9286fc943c579a3fd22de6 Mon Sep 17 00:00:00 2001
From: Omar Roth <omarroth@hotmail.com>
Date: Mon, 13 Aug 2018 09:17:28 -0500
Subject: [PATCH] Add geo-bypass for video info

---
 src/invidious/helpers/proxy.cr | 143 +++++++++++++++++++++++++++++++++
 src/invidious/videos.cr        |  46 +++++++++++
 src/invidious/views/watch.ecr  |   9 +++
 3 files changed, 198 insertions(+)
 create mode 100644 src/invidious/helpers/proxy.cr

diff --git a/src/invidious/helpers/proxy.cr b/src/invidious/helpers/proxy.cr
new file mode 100644
index 00000000..3e94042d
--- /dev/null
+++ b/src/invidious/helpers/proxy.cr
@@ -0,0 +1,143 @@
+# See https://github.com/crystal-lang/crystal/issues/2963
+class HTTPProxy
+  getter proxy_host : String
+  getter proxy_port : Int32
+  getter options : Hash(Symbol, String)
+  getter tls : OpenSSL::SSL::Context::Client?
+
+  def initialize(@proxy_host, @proxy_port = 80, @options = {} of Symbol => String)
+  end
+
+  def open(host, port, tls = nil, connection_options = {} of Symbol => Float64 | Nil)
+    dns_timeout = connection_options.fetch(:dns_timeout, nil)
+    connect_timeout = connection_options.fetch(:connect_timeout, nil)
+    read_timeout = connection_options.fetch(:read_timeout, nil)
+
+    socket = TCPSocket.new @proxy_host, @proxy_port, dns_timeout, connect_timeout
+    socket.read_timeout = read_timeout if read_timeout
+    socket.sync = true
+
+    socket << "CONNECT #{host}:#{port} HTTP/1.1\r\n"
+
+    if options[:user]?
+      credentials = Base64.strict_encode("#{options[:user]}:#{options[:password]}")
+      credentials = "#{credentials}\n".gsub(/\s/, "")
+      socket << "Proxy-Authorization: Basic #{credentials}\r\n"
+    end
+
+    socket << "\r\n"
+
+    resp = parse_response(socket)
+
+    if resp[:code]? == 200
+      {% if !flag?(:without_openssl) %}
+        if tls
+          tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
+          socket = tls_socket
+        end
+      {% end %}
+
+      return socket
+    else
+      socket.close
+      raise IO::Error.new(resp.inspect)
+    end
+  end
+
+  private def parse_response(socket)
+    resp = {} of Symbol => Int32 | String | Hash(String, String)
+
+    begin
+      version, code, reason = socket.gets.as(String).chomp.split(/ /, 3)
+
+      headers = {} of String => String
+
+      while (line = socket.gets.as(String)) && (line.chomp != "")
+        name, value = line.split(/:/, 2)
+        headers[name.strip] = value.strip
+      end
+
+      resp[:version] = version
+      resp[:code] = code.to_i
+      resp[:reason] = reason
+      resp[:headers] = headers
+    rescue
+    end
+
+    return resp
+  end
+end
+
+class HTTPClient < HTTP::Client
+  def set_proxy(proxy : HTTPProxy)
+    begin
+      @socket = proxy.open(host: @host, port: @port, tls: @tls, connection_options: proxy_connection_options)
+    rescue IO::Error
+      @socket = nil
+    end
+  end
+
+  def proxy_connection_options
+    opts = {} of Symbol => Float64 | Nil
+
+    opts[:dns_timeout] = @dns_timeout
+    opts[:connect_timeout] = @connect_timeout
+    opts[:read_timeout] = @read_timeout
+
+    return opts
+  end
+end
+
+def get_proxies(country_code = "US")
+  client = HTTP::Client.new(URI.parse("http://spys.one"))
+  client.read_timeout = 10.seconds
+  client.connect_timeout = 10.seconds
+
+  headers = HTTP::Headers.new
+  headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"
+  headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
+  headers["Accept-Language"] = "Accept-Language: en-US,en;q=0.9"
+  headers["Host"] = "spys.one"
+  headers["Content-Type"] = "application/x-www-form-urlencoded"
+  body = {
+    "xpp" => "2",
+    "xf1" => "0",
+    "xf2" => "2",
+    "xf4" => "1",
+    "xf5" => "1",
+  }
+  response = client.post("/free-proxy-list/#{country_code}/", headers, form: body)
+  response = XML.parse_html(response.body)
+
+  proxies = [] of {ip: String, port: Int32, score: Float64}
+  response = response.xpath_nodes(%q(//table))[1]
+  response.xpath_nodes(%q(.//tr)).each do |node|
+    if !node["onmouseover"]?
+      next
+    end
+
+    ip = node.xpath_node(%q(.//td[1]/font[2])).to_s.match(/<font class="spy14">(?<address>[^<]+)</).not_nil!["address"]
+    port = 3128
+
+    latency = node.xpath_node(%q(.//td[6])).not_nil!.content.to_f
+    speed = node.xpath_node(%q(.//td[7]/font/table)).not_nil!["width"].to_f
+    uptime = node.xpath_node(%q(.//td[8]/font/acronym)).not_nil!
+
+    # Skip proxies that are down
+    if uptime["title"].ends_with? "?"
+      next
+    end
+
+    if md = uptime.content.match(/^\d+/)
+      uptime = md[0].to_f
+    else
+      next
+    end
+
+    score = (uptime*4 + speed*2 + latency)/7
+
+    proxies << {ip: ip, port: port, score: score}
+  end
+
+  return proxies
+end
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index ed2c3d43..74a1b2c4 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -108,6 +108,9 @@ CAPTION_LANGUAGES = {
   "Zulu",
 }
 
+REGIONS        = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"}
+BYPASS_REGIONS = {"CA", "DE", "FR", "JP", "RU", "UK"}
+
 VIDEO_THUMBNAILS = {
   {name: "default", url: "default", height: 90, width: 120},
   {name: "medium", url: "mqdefault", height: 180, width: 320},
@@ -409,6 +412,49 @@ def fetch_video(id)
   html = html_channel.receive
   info = info_channel.receive
 
+  if info["reason"]? && info["reason"].includes? "your country"
+    bypass_channel = Channel({HTTP::Params | Nil, XML::Node | Nil}).new
+
+    BYPASS_REGIONS.each do |country_code|
+      spawn do
+        begin
+          proxies = get_proxies(country_code)
+
+          # Try not to overload single proxy
+          proxy = proxies[0, 5].sample(1)[0]
+          proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port])
+
+          client = HTTPClient.new(URI.parse("https://www.youtube.com"))
+          client.read_timeout = 10.seconds
+          client.connect_timeout = 10.seconds
+          client.set_proxy(proxy)
+
+          proxy_info = client.get("/get_video_info?video_id=#{id}&el=detailpage&ps=default&eurl=&gl=US&hl=en&disable_polymer=1")
+          proxy_info = HTTP::Params.parse(proxy_info.body)
+
+          if !proxy_info["reason"]?
+            proxy_html = client.get("/watch?v=#{id}&bpctr=#{Time.new.epoch + 2000}&gl=US&hl=en&disable_polymer=1")
+            proxy_html = XML.parse_html(proxy_html.body)
+
+            bypass_channel.send({proxy_info, proxy_html})
+          else
+            bypass_channel.send({nil, nil})
+          end
+        rescue ex
+          bypass_channel.send({nil, nil})
+        end
+      end
+    end
+
+    BYPASS_REGIONS.size.times do
+      response = bypass_channel.receive
+      if response[0] || response[1]
+        info = response[0].not_nil!
+        html = response[1].not_nil!
+      end
+    end
+  end
+
   if info["reason"]?
     raise info["reason"]
   end
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 822c87e8..ff27feb0 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -199,6 +199,15 @@ get_youtube_comments();
             <p id="Wilson">Wilson Score : <%= video.wilson_score.round(4) %></p>
             <p id="Rating">Rating : <%= rating.round(4) %> / 5</p>
             <p id="Engagement">Engagement : <%= engagement.round(2) %>%</p>
+            <% if video.allowed_regions.size != REGIONS.size %>
+                <p id="AllowedRegions">
+                <% if video.allowed_regions.size < REGIONS.size / 2 %>
+                Whitelisted regions: <%= video.allowed_regions.join(", ") %>
+                <% else %>
+                Blacklisted regions: <%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
+                <% end %>
+                </p>
+            <% end %>
             <% if ad_slots %>
             <p id="Slots">Ad Slots : <%= ad_slots %></p>
             <% end %>