forked from midou/invidious
		
	Add separate user accounts
This commit is contained in:
		| @@ -10,6 +10,7 @@ CREATE TABLE public.users | ||||
|     subscriptions text[] COLLATE pg_catalog."default", | ||||
|     email text COLLATE pg_catalog."default" NOT NULL, | ||||
|     preferences text COLLATE pg_catalog."default", | ||||
|     password text COLLATE pg_catalog."default", | ||||
|     CONSTRAINT users_email_key UNIQUE (email), | ||||
|     CONSTRAINT users_id_key UNIQUE (id) | ||||
| ) | ||||
|   | ||||
							
								
								
									
										431
									
								
								src/invidious.cr
									
									
									
									
									
								
							
							
						
						
									
										431
									
								
								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" | ||||
|  | ||||
|     login_page = client.get("/ServiceLogin") | ||||
|     headers = login_page.cookies.add_request_headers(headers) | ||||
|   if account_type == "google" | ||||
|     tfa_code = env.params.body["tfa"]?.try &.lchop("G-") | ||||
|  | ||||
|     login_page = XML.parse_html(login_page.body) | ||||
|     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" | ||||
|  | ||||
|     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 ||= "" | ||||
|       login_page = client.get("/ServiceLogin") | ||||
|       headers = login_page.cookies.add_request_headers(headers) | ||||
|  | ||||
|       if name != "" && value != "" | ||||
|         inputs[name] = value | ||||
|       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 ||= "" | ||||
|  | ||||
|         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 | ||||
|  | ||||
|       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 = lookup_results.body | ||||
|       lookup_results = lookup_results[5..-1] | ||||
|       lookup_results = JSON.parse(lookup_results) | ||||
|  | ||||
|       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_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) | ||||
|  | ||||
|       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][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_code | ||||
|             next env.redirect "/login?tfa=true" | ||||
|           end | ||||
|  | ||||
|           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 | ||||
|  | ||||
|           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) | ||||
|  | ||||
|       login = client.get(login.headers["Location"], headers) | ||||
|  | ||||
|       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) | ||||
|  | ||||
|       # 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"]? | ||||
|  | ||||
|     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}"]) | ||||
|     action = env.params.body["action"]? | ||||
|     action ||= "signin" | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     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_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) | ||||
|  | ||||
|     headers["Cookie"] = URI.unescape(headers["Cookie"]) | ||||
|  | ||||
|     if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED" | ||||
|       error_message = "Incorrect password" | ||||
|     if !email | ||||
|       error_message = "User ID is a required field" | ||||
|       next templated "error" | ||||
|     end | ||||
|  | ||||
|     if challenge_results[0][-1][0].as_a? | ||||
|       tfa = challenge_results[0][-1][0][0] | ||||
|     if !password | ||||
|       error_message = "Password is a required field" | ||||
|       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 !challenge_response || !token | ||||
|       error_message = "CAPTCHA is a required field" | ||||
|       next templated "error" | ||||
|     end | ||||
|  | ||||
|         if !tfa_code | ||||
|           next env.redirect "/login?tfa=true" | ||||
|         end | ||||
|     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 | ||||
|  | ||||
|         tl = challenge_results[1][2] | ||||
|     if action == "signin" | ||||
|       user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1 AND password IS NOT NULL", email, as: User) | ||||
|  | ||||
|         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]]]) | ||||
|       if !user | ||||
|         error_message = "Cannot find user with ID #{email}." | ||||
|         next templated "error" | ||||
|       end | ||||
|  | ||||
|       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 | ||||
|           error_message = "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled." | ||||
|           next templated "error" | ||||
|           secure = false | ||||
|         end | ||||
|  | ||||
|         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) | ||||
|  | ||||
|     login = client.get(login.headers["Location"], headers) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     # 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 | ||||
|         env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, secure: secure, http_only: true) | ||||
|       else | ||||
|         cookie.secure = false | ||||
|         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 | ||||
|  | ||||
|       cookie.extension = cookie.extension.not_nil!.gsub(".youtube.com", host) | ||||
|       cookie.extension = cookie.extension.not_nil!.gsub("Secure; ", "") | ||||
|     end | ||||
|       sid = Base64.encode(Random::Secure.random_bytes(50)) | ||||
|       user = create_user(sid, email, password) | ||||
|  | ||||
|     login.cookies.add_response_headers(env.response.headers) | ||||
|       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) | ||||
|  | ||||
|       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) | ||||
|     end | ||||
|  | ||||
|     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"] | ||||
|       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" | ||||
|  | ||||
|       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 | ||||
|         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 | ||||
|       next env.redirect "/" | ||||
|     end | ||||
|  | ||||
|     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}" | ||||
|  | ||||
|     # 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) | ||||
|         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 | ||||
|   | ||||
| @@ -138,6 +138,7 @@ class User | ||||
|       default:   DEFAULT_USER_PREFERENCES, | ||||
|       converter: PreferencesConverter, | ||||
|     }, | ||||
|     password: String?, | ||||
|   }) | ||||
| end | ||||
|  | ||||
| @@ -814,7 +815,14 @@ def fetch_user(sid, client, headers, db) | ||||
|     email = "" | ||||
|   end | ||||
|  | ||||
|   user = User.new(sid, Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES) | ||||
|   user = User.new(sid, Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil) | ||||
|   return user | ||||
| end | ||||
|  | ||||
| def create_user(sid, email, password) | ||||
|   password = Crypto::Bcrypt::Password.create(password, cost: 10) | ||||
|   user = User.new(sid, Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s) | ||||
|  | ||||
|   return user | ||||
| end | ||||
|  | ||||
| @@ -947,3 +955,54 @@ def write_var_int(value : Int) | ||||
|  | ||||
|   return bytes | ||||
| end | ||||
|  | ||||
| def generate_captcha(key) | ||||
|   minute = Random::Secure.rand(12) | ||||
|   minute_angle = minute * 30 | ||||
|   minute = minute * 5 | ||||
|  | ||||
|   hour = Random::Secure.rand(12) | ||||
|   hour_angle = hour * 30 + minute_angle.to_f / 12 | ||||
|   if hour == 0 | ||||
|     hour = 12 | ||||
|   end | ||||
|  | ||||
|   clock_svg = <<-END_SVG | ||||
|   <svg viewBox="0 0 100 100" width="200px"> | ||||
|   <circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle> | ||||
|    | ||||
|   <circle id="hour1" cx="69" cy="17.091" r="2" fill="black"></circle> | ||||
|   <circle id="hour2" cx="82.909" cy="31" r="2" fill="black"></circle> | ||||
|   <circle id="hour3" cx="88" cy="50" r="2" fill="black"></circle> | ||||
|    | ||||
|   <circle id="hour4" cx="82.909" cy="69" r="2" fill="black"></circle> | ||||
|   <circle id="hour5" cx="69" cy="82.909" r="2" fill="black"></circle> | ||||
|   <circle id="hour6" cx="50" cy="88" r="2" fill="black"></circle> | ||||
|    | ||||
|   <circle id="hour7" cx="31" cy="82.909" r="2" fill="black"></circle> | ||||
|   <circle id="hour8" cx="17.091" cy="69" r="2" fill="black"></circle> | ||||
|   <circle id="hour9" cx="12" cy="50" r="2" fill="black"></circle> | ||||
|    | ||||
|   <circle id="hour10" cx="17.091" cy="31" r="2" fill="black"></circle> | ||||
|   <circle id="hour11" cx="31" cy="17.091" r="2" fill="black"></circle> | ||||
|   <circle id="hour12" cx="50" cy="12" r="2" fill="black"></circle> | ||||
|    | ||||
|   <circle cx="50" cy="50" r="3" fill="black"></circle> | ||||
|   <line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line> | ||||
|   <line id="hour"   transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line> | ||||
|   </svg> | ||||
|   END_SVG | ||||
|  | ||||
|   challenge = "" | ||||
|   convert = Process.run(%(convert -density 1200 -resize 400x400 -background none svg:- png:-), shell: true, input: IO::Memory.new(clock_svg), output: Process::Redirect::Pipe) do |proc| | ||||
|     challenge = proc.output.gets_to_end | ||||
|     challenge = Base64.encode(challenge) | ||||
|     challenge = "data:image/png; base64, #{challenge}" | ||||
|   end | ||||
|  | ||||
|   answer = "#{hour}:#{minute.to_s.rjust(2, '0')}" | ||||
|   token = OpenSSL::HMAC.digest(:sha256, key, answer) | ||||
|   token = Base64.encode(token) | ||||
|  | ||||
|   return {challenge: challenge, token: token} | ||||
| end | ||||
|   | ||||
| @@ -6,24 +6,51 @@ | ||||
|     <div class="pure-u-1 pure-u-md-1-5"></div> | ||||
|     <div class="pure-u-1 pure-u-md-3-5"> | ||||
|         <div class="h-box"> | ||||
|             <div class="pure-g"> | ||||
|                 <div class="pure-u-1 pure-u-md-1-2"> | ||||
|                     <a class="pure-button <% if account_type == "google" %>pure-button-disabled<% end %>" href="/login">Login to Google</a> | ||||
|                 </div> | ||||
|                 <div class="pure-u-1 pure-u-md-1-2"> | ||||
|                     <a class="pure-button <% if account_type == "invidious" %>pure-button-disabled<% end %>" href="/login?type=invidious">Login/Register</a> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <hr> | ||||
|             <% if account_type == "google" %> | ||||
|             <form class="pure-form pure-form-stacked" action="/login?referer=<%= referer %>" method="post"> | ||||
|                 <fieldset> | ||||
|                     <legend>Login to Google</legend> | ||||
|  | ||||
|                     <label for="email">Email</label> | ||||
|                     <input class="pure-input-1" name="email" type="email" placeholder="Email"> | ||||
|                     <input required class="pure-input-1" name="email" type="email" placeholder="Email"> | ||||
|  | ||||
|                     <label for="password">Password</label> | ||||
|                     <input class="pure-input-1" name="password" type="password" placeholder="Password"> | ||||
|                     <input required class="pure-input-1" name="password" type="password" placeholder="Password"> | ||||
|                      | ||||
|                     <% if tfa %> | ||||
|                     <label for="tfa">Google verification code</label> | ||||
|                     <input class="pure-input-1" name="tfa" type="text" placeholder="Google verification code"> | ||||
|                     <input required class="pure-input-1" name="tfa" type="text" placeholder="Google verification code"> | ||||
|                     <% end %> | ||||
|  | ||||
|                     <button type="submit" class="pure-button pure-button-primary">Sign in</button> | ||||
|                 </fieldset> | ||||
|             </form> | ||||
|             <% elsif account_type == "invidious" %> | ||||
|             <form class="pure-form pure-form-stacked" action="/login?referer=<%= referer %>&type=invidious" method="post"> | ||||
|                 <fieldset> | ||||
|                     <label for="email">User ID:</label> | ||||
|                     <input required class="pure-input-1" name="email" type="text" placeholder="User ID"> | ||||
|  | ||||
|                     <label for="password">Password</label> | ||||
|                     <input required class="pure-input-1" name="password" type="password" placeholder="Password"> | ||||
|                  | ||||
|                     <img src='<%= captcha.not_nil![:challenge] %>'/> | ||||
|                     <input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>"> | ||||
|                     <label for="challenge_response">Time (hh:mm):</label> | ||||
|                     <input required type="text" name="challenge_response" type="text>" placeholder="hh:mm"> | ||||
|  | ||||
|                     <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">Sign In</button> | ||||
|                     <button type="submit" name="action" value="register" class="pure-button pure-button-primary">Register</button> | ||||
|                 </fieldset> | ||||
|             </form> | ||||
|             <% end %> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="pure-u-1 pure-u-md-1-5"></div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user