summaryrefslogtreecommitdiffstats
path: root/src/invidious.cr
diff options
context:
space:
mode:
Diffstat (limited to 'src/invidious.cr')
-rw-r--r--src/invidious.cr391
1 files changed, 261 insertions, 130 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index fe205b53..ae3f463c 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -14,15 +14,18 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+require "crypto/bcrypt/password"
require "detect_language"
require "kemal"
+require "openssl/hmac"
require "option_parser"
require "pg"
require "xml"
require "yaml"
require "./invidious/*"
-CONFIG = Config.from_yaml(File.read("config/config.yml"))
+CONFIG = Config.from_yaml(File.read("config/config.yml"))
+HMAC_KEY = Random::Secure.random_bytes(32)
crawl_threads = CONFIG.crawl_threads
channel_threads = CONFIG.channel_threads
@@ -233,12 +236,21 @@ before_all do |env|
sid = env.request.cookies["SID"].value
- begin
- client = make_client(YT_URL)
- user = get_user(sid, client, headers, PG_DB, false)
+ # Invidious users only have SID
+ if !env.request.cookies.has_key? "SSID"
+ user = PG_DB.query_one?("SELECT * FROM users WHERE id = $1", sid, as: User)
- env.set "user", user
- rescue ex
+ if user
+ env.set "user", user
+ end
+ else
+ begin
+ client = make_client(YT_URL)
+ user = get_user(sid, client, headers, PG_DB, false)
+
+ env.set "user", user
+ rescue ex
+ end
end
end
end
@@ -514,9 +526,21 @@ get "/search" do |env|
end
get "/login" do |env|
+ user = env.get? "user"
+ if user
+ next env.redirect "/feed/subscriptions"
+ end
+
referer = env.request.headers["referer"]?
referer ||= "/feed/subscriptions"
+ account_type = env.params.query["type"]?
+ account_type ||= "google"
+
+ if account_type == "invidious"
+ captcha = generate_captcha(HMAC_KEY)
+ end
+
tfa = env.params.query["tfa"]?
tfa ||= false
@@ -538,147 +562,236 @@ post "/login" do |env|
email = env.params.body["email"]?
password = env.params.body["password"]?
- tfa_code = env.params.body["tfa"]?.try &.lchop("G-")
- begin
- client = make_client(LOGIN_URL)
- headers = HTTP::Headers.new
- headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8"
- headers["Google-Accounts-XSRF"] = "1"
+ account_type = env.params.query["type"]?
+ account_type ||= "google"
+
+ if account_type == "google"
+ tfa_code = env.params.body["tfa"]?.try &.lchop("G-")
+
+ begin
+ client = make_client(LOGIN_URL)
+ headers = HTTP::Headers.new
+ headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8"
+ headers["Google-Accounts-XSRF"] = "1"
- login_page = client.get("/ServiceLogin")
- headers = login_page.cookies.add_request_headers(headers)
+ login_page = client.get("/ServiceLogin")
+ headers = login_page.cookies.add_request_headers(headers)
- login_page = XML.parse_html(login_page.body)
+ login_page = XML.parse_html(login_page.body)
- inputs = {} of String => String
- login_page.xpath_nodes(%q(//input[@type="submit"])).each do |node|
- name = node["id"]? || node["name"]?
- name ||= ""
- value = node["value"]?
- value ||= ""
+ inputs = {} of String => String
+ login_page.xpath_nodes(%q(//input[@type="submit"])).each do |node|
+ name = node["id"]? || node["name"]?
+ name ||= ""
+ value = node["value"]?
+ value ||= ""
- if name != "" && value != ""
- inputs[name] = value
+ if name != "" && value != ""
+ inputs[name] = value
+ end
end
- end
- login_page.xpath_nodes(%q(//input[@type="hidden"])).each do |node|
- name = node["id"]? || node["name"]?
- name ||= ""
- value = node["value"]?
- value ||= ""
+ login_page.xpath_nodes(%q(//input[@type="hidden"])).each do |node|
+ name = node["id"]? || node["name"]?
+ name ||= ""
+ value = node["value"]?
+ value ||= ""
- if name != "" && value != ""
- inputs[name] = value
+ if name != "" && value != ""
+ inputs[name] = value
+ end
end
- end
- lookup_req = %(["#{email}",null,[],null,"US",null,null,2,false,true,[null,null,[2,1,null,1,"https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Faccounts.google.com%2FManageAccount&followup=https%3A%2F%2Faccounts.google.com%2FManageAccount",null,[],4,[]],1,[null,null,[]],null,null,null,true],"#{email}"])
+ lookup_req = %(["#{email}",null,[],null,"US",null,null,2,false,true,[null,null,[2,1,null,1,"https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Faccounts.google.com%2FManageAccount&followup=https%3A%2F%2Faccounts.google.com%2FManageAccount",null,[],4,[]],1,[null,null,[]],null,null,null,true],"#{email}"])
- lookup_results = client.post("/_/signin/sl/lookup", headers, login_req(inputs, lookup_req))
- headers = lookup_results.cookies.add_request_headers(headers)
+ lookup_results = client.post("/_/signin/sl/lookup", headers, login_req(inputs, lookup_req))
+ headers = lookup_results.cookies.add_request_headers(headers)
- lookup_results = lookup_results.body
- lookup_results = lookup_results[5..-1]
- lookup_results = JSON.parse(lookup_results)
+ lookup_results = lookup_results.body
+ lookup_results = lookup_results[5..-1]
+ lookup_results = JSON.parse(lookup_results)
- user_hash = lookup_results[0][2]
+ user_hash = lookup_results[0][2]
- challenge_req = %(["#{user_hash}",null,1,null,[1,null,null,null,["#{password}",null,true]],[null,null,[2,1,null,1,"https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Faccounts.google.com%2FManageAccount&followup=https%3A%2F%2Faccounts.google.com%2FManageAccount",null,[],4,[]],1,[null,null,[]],null,null,null,true]])
+ challenge_req = %(["#{user_hash}",null,1,null,[1,null,null,null,["#{password}",null,true]],[null,null,[2,1,null,1,"https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Faccounts.google.com%2FManageAccount&followup=https%3A%2F%2Faccounts.google.com%2FManageAccount",null,[],4,[]],1,[null,null,[]],null,null,null,true]])
- challenge_results = client.post("/_/signin/sl/challenge", headers, login_req(inputs, challenge_req))
- headers = challenge_results.cookies.add_request_headers(headers)
+ challenge_results = client.post("/_/signin/sl/challenge", headers, login_req(inputs, challenge_req))
+ headers = challenge_results.cookies.add_request_headers(headers)
- challenge_results = challenge_results.body
- challenge_results = challenge_results[5..-1]
- challenge_results = JSON.parse(challenge_results)
+ challenge_results = challenge_results.body
+ challenge_results = challenge_results[5..-1]
+ challenge_results = JSON.parse(challenge_results)
- headers["Cookie"] = URI.unescape(headers["Cookie"])
+ headers["Cookie"] = URI.unescape(headers["Cookie"])
- if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
- error_message = "Incorrect password"
- next templated "error"
- end
+ if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
+ error_message = "Incorrect password"
+ next templated "error"
+ end
- if challenge_results[0][-1][0].as_a?
- tfa = challenge_results[0][-1][0][0]
+ if challenge_results[0][-1][0].as_a?
+ tfa = challenge_results[0][-1][0][0]
- if tfa[2] == "TWO_STEP_VERIFICATION"
- if tfa[5] == "QUOTA_EXCEEDED"
- error_message = "Quota exceeded, try again in a few hours"
- next templated "error"
- end
+ if tfa[2] == "TWO_STEP_VERIFICATION"
+ if tfa[5] == "QUOTA_EXCEEDED"
+ error_message = "Quota exceeded, try again in a few hours"
+ next templated "error"
+ end
- if !tfa_code
- next env.redirect "/login?tfa=true"
- end
+ if !tfa_code
+ next env.redirect "/login?tfa=true"
+ end
- tl = challenge_results[1][2]
+ tl = challenge_results[1][2]
+
+ request_type = tfa[8]
+ case request_type
+ when 6
+ # Authenticator app
+ tfa_req = %(["#{user_hash}",null,2,null,[6,null,null,null,null,["#{tfa_code}",false]]])
+ when 9
+ # Voice or text message
+ tfa_req = %(["#{user_hash}",null,2,null,[9,null,null,null,null,null,null,null,[null,"#{tfa_code}",false,2]]])
+ else
+ error_message = "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled."
+ next templated "error"
+ end
- request_type = tfa[8]
- case request_type
- when 6
- # Authenticator app
- tfa_req = %(["#{user_hash}",null,2,null,[6,null,null,null,null,["#{tfa_code}",false]]])
- when 9
- # Voice or text message
- tfa_req = %(["#{user_hash}",null,2,null,[9,null,null,null,null,null,null,null,[null,"#{tfa_code}",false,2]]])
- else
- error_message = "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled."
- next templated "error"
+ challenge_results = client.post("/_/signin/challenge?hl=en&TL=#{tl}", headers, login_req(inputs, tfa_req))
+ headers = challenge_results.cookies.add_request_headers(headers)
+
+ challenge_results = challenge_results.body
+ challenge_results = challenge_results[5..-1]
+ challenge_results = JSON.parse(challenge_results)
+
+ if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
+ error_message = "Invalid TFA code"
+ next templated "error"
+ end
end
+ end
+
+ login_res = challenge_results[0][13][2].to_s
+
+ login = client.get(login_res, headers)
+ headers = login.cookies.add_request_headers(headers)
- challenge_results = client.post("/_/signin/challenge?hl=en&TL=#{tl}", headers, login_req(inputs, tfa_req))
- headers = challenge_results.cookies.add_request_headers(headers)
+ login = client.get(login.headers["Location"], headers)
- challenge_results = challenge_results.body
- challenge_results = challenge_results[5..-1]
- challenge_results = JSON.parse(challenge_results)
+ headers = HTTP::Headers.new
+ headers = login.cookies.add_request_headers(headers)
+
+ sid = login.cookies["SID"].value
+
+ client = make_client(YT_URL)
+ user = get_user(sid, client, headers, PG_DB)
- if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
- error_message = "Invalid TFA code"
- next templated "error"
+ # We are now logged in
+
+ host = URI.parse(env.request.headers["Host"]).host
+
+ login.cookies.each do |cookie|
+ if Kemal.config.ssl
+ cookie.secure = true
+ else
+ cookie.secure = false
end
+
+ cookie.extension = cookie.extension.not_nil!.gsub(".youtube.com", host)
+ cookie.extension = cookie.extension.not_nil!.gsub("Secure; ", "")
end
+
+ login.cookies.add_response_headers(env.response.headers)
+
+ env.redirect referer
+ rescue ex
+ error_message = "Login failed. This may be because two-factor authentication is not enabled on your account."
+ next templated "error"
end
+ elsif account_type == "invidious"
+ challenge_response = env.params.body["challenge_response"]?
+ token = env.params.body["token"]?
- login_res = challenge_results[0][13][2].to_s
+ action = env.params.body["action"]?
+ action ||= "signin"
- login = client.get(login_res, headers)
- headers = login.cookies.add_request_headers(headers)
+ if !email
+ error_message = "User ID is a required field"
+ next templated "error"
+ end
- login = client.get(login.headers["Location"], headers)
+ if !password
+ error_message = "Password is a required field"
+ next templated "error"
+ end
- headers = HTTP::Headers.new
- headers = login.cookies.add_request_headers(headers)
+ if !challenge_response || !token
+ error_message = "CAPTCHA is a required field"
+ next templated "error"
+ end
- sid = login.cookies["SID"].value
+ challenge_response = challenge_response.lstrip('0')
+ if OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge_response) == Base64.decode(token)
+ else
+ error_message = "Invalid CAPTCHA response"
+ next templated "error"
+ end
- client = make_client(YT_URL)
- user = get_user(sid, client, headers, PG_DB)
+ if action == "signin"
+ user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1 AND password IS NOT NULL", email, as: User)
- # We are now logged in
+ if !user
+ error_message = "Cannot find user with ID #{email}."
+ next templated "error"
+ end
- host = URI.parse(env.request.headers["Host"]).host
+ if !user.password
+ error_message = "Account appears to be a Google account."
+ next templated "error"
+ end
+
+ if Crypto::Bcrypt::Password.new(user.password.not_nil!) == password
+ sid = Base64.encode(Random::Secure.random_bytes(50))
+ PG_DB.exec("UPDATE users SET id = $1 WHERE email = $2", sid, email)
+
+ if Kemal.config.ssl
+ secure = true
+ else
+ secure = false
+ end
+
+ env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, secure: secure, http_only: true)
+ else
+ error_message = "Invalid password"
+ next templated "error"
+ end
+ elsif action == "register"
+ user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1 AND password IS NOT NULL", email, as: User)
+ if user
+ error_message = "User already exists, please sign in"
+ next templated "error"
+ end
+
+ sid = Base64.encode(Random::Secure.random_bytes(50))
+ user = create_user(sid, email, password)
+
+ user_array = user.to_a
+ user_array[5] = user_array[5].to_json
+ args = arg_array(user_array)
+
+ PG_DB.exec("INSERT INTO users VALUES (#{args})", user_array)
- login.cookies.each do |cookie|
if Kemal.config.ssl
- cookie.secure = true
+ secure = true
else
- cookie.secure = false
+ secure = false
end
- cookie.extension = cookie.extension.not_nil!.gsub(".youtube.com", host)
- cookie.extension = cookie.extension.not_nil!.gsub("Secure; ", "")
+ env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, secure: secure, http_only: true)
end
- login.cookies.add_response_headers(env.response.headers)
-
env.redirect referer
- rescue ex
- error_message = "Login failed. This may be because two-factor authentication is not enabled on your account."
- next templated "error"
end
end
@@ -782,8 +895,10 @@ get "/feed/subscriptions" do |env|
headers = HTTP::Headers.new
headers["Cookie"] = env.request.headers["Cookie"]
- client = make_client(YT_URL)
- user = get_user(user.id, client, headers, PG_DB)
+ if !user.password
+ client = make_client(YT_URL)
+ user = get_user(user.id, client, headers, PG_DB)
+ end
max_results = preferences.max_results
max_results ||= env.params.query["maxResults"]?.try &.to_i
@@ -903,15 +1018,15 @@ get "/subscription_manager" do |env|
user = user.as(User)
- # Refresh account
- headers = HTTP::Headers.new
- headers["Cookie"] = env.request.headers["Cookie"]
-
- client = make_client(YT_URL)
- user = get_user(user.id, client, headers, PG_DB)
+ if !user.password
+ # Refresh account
+ headers = HTTP::Headers.new
+ headers["Cookie"] = env.request.headers["Cookie"]
+ client = make_client(YT_URL)
+ user = get_user(user.id, client, headers, PG_DB)
+ end
subscriptions = user.subscriptions
- subscriptions ||= [] of String
client = make_client(YT_URL)
subscriptions = subscriptions.map do |ucid|
@@ -941,34 +1056,50 @@ get "/subscription_ajax" do |env|
channel_id = env.params.query["c"]?
channel_id ||= ""
- headers = HTTP::Headers.new
- headers["Cookie"] = env.request.headers["Cookie"]
+ if !user.password
+ headers = HTTP::Headers.new
+ headers["Cookie"] = env.request.headers["Cookie"]
- client = make_client(YT_URL)
- subs = client.get("/subscription_manager?disable_polymer=1", headers)
- headers["Cookie"] += "; " + subs.cookies.add_request_headers(headers)["Cookie"]
- match = subs.body.match(/'XSRF_TOKEN': "(?<session_token>[A-Za-z0-9\_\-\=]+)"/)
- if match
- session_token = match["session_token"]
- else
- next env.redirect "/"
- end
+ client = make_client(YT_URL)
+ subs = client.get("/subscription_manager?disable_polymer=1", headers)
+ headers["Cookie"] += "; " + subs.cookies.add_request_headers(headers)["Cookie"]
+ match = subs.body.match(/'XSRF_TOKEN': "(?<session_token>[A-Za-z0-9\_\-\=]+)"/)
+ if match
+ session_token = match["session_token"]
+ else
+ next env.redirect "/"
+ end
- headers["content-type"] = "application/x-www-form-urlencoded"
+ headers["content-type"] = "application/x-www-form-urlencoded"
- post_req = {
- "session_token" => session_token,
- }
- post_req = HTTP::Params.encode(post_req)
- post_url = "/subscription_ajax?#{action}=1&c=#{channel_id}"
+ post_req = {
+ "session_token" => session_token,
+ }
+ post_req = HTTP::Params.encode(post_req)
+ post_url = "/subscription_ajax?#{action}=1&c=#{channel_id}"
- # Update user
- if client.post(post_url, headers, post_req).status_code == 200
+ # Update user
+ if client.post(post_url, headers, post_req).status_code == 200
+ sid = user.id
+
+ case action
+ when .starts_with? "action_create"
+ PG_DB.exec("UPDATE users SET subscriptions = array_append(subscriptions,$1) WHERE id = $2", channel_id, sid)
+ when .starts_with? "action_remove"
+ PG_DB.exec("UPDATE users SET subscriptions = array_remove(subscriptions,$1) WHERE id = $2", channel_id, sid)
+ end
+ end
+ else
sid = user.id
case action
when .starts_with? "action_create"
- PG_DB.exec("UPDATE users SET subscriptions = array_append(subscriptions,$1) WHERE id = $2", channel_id, sid)
+ if !user.subscriptions.includes? channel_id
+ PG_DB.exec("UPDATE users SET subscriptions = array_append(subscriptions,$1) WHERE id = $2", channel_id, sid)
+
+ client = make_client(YT_URL)
+ get_channel(channel_id, client, PG_DB, false, false)
+ end
when .starts_with? "action_remove"
PG_DB.exec("UPDATE users SET subscriptions = array_remove(subscriptions,$1) WHERE id = $2", channel_id, sid)
end