forked from midou/invidious
Add separate user accounts
This commit is contained in:
parent
5b41c0f81b
commit
d29ea79a5d
@ -10,6 +10,7 @@ CREATE TABLE public.users
|
|||||||
subscriptions text[] COLLATE pg_catalog."default",
|
subscriptions text[] COLLATE pg_catalog."default",
|
||||||
email text COLLATE pg_catalog."default" NOT NULL,
|
email text COLLATE pg_catalog."default" NOT NULL,
|
||||||
preferences text COLLATE pg_catalog."default",
|
preferences text COLLATE pg_catalog."default",
|
||||||
|
password text COLLATE pg_catalog."default",
|
||||||
CONSTRAINT users_email_key UNIQUE (email),
|
CONSTRAINT users_email_key UNIQUE (email),
|
||||||
CONSTRAINT users_id_key UNIQUE (id)
|
CONSTRAINT users_id_key UNIQUE (id)
|
||||||
)
|
)
|
||||||
|
135
src/invidious.cr
135
src/invidious.cr
@ -14,8 +14,10 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
require "crypto/bcrypt/password"
|
||||||
require "detect_language"
|
require "detect_language"
|
||||||
require "kemal"
|
require "kemal"
|
||||||
|
require "openssl/hmac"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
require "pg"
|
require "pg"
|
||||||
require "xml"
|
require "xml"
|
||||||
@ -23,6 +25,7 @@ require "yaml"
|
|||||||
require "./invidious/*"
|
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
|
crawl_threads = CONFIG.crawl_threads
|
||||||
channel_threads = CONFIG.channel_threads
|
channel_threads = CONFIG.channel_threads
|
||||||
@ -233,6 +236,14 @@ before_all do |env|
|
|||||||
|
|
||||||
sid = env.request.cookies["SID"].value
|
sid = env.request.cookies["SID"].value
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
if user
|
||||||
|
env.set "user", user
|
||||||
|
end
|
||||||
|
else
|
||||||
begin
|
begin
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
user = get_user(sid, client, headers, PG_DB, false)
|
user = get_user(sid, client, headers, PG_DB, false)
|
||||||
@ -242,6 +253,7 @@ before_all do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
get "/" do |env|
|
get "/" do |env|
|
||||||
templated "index"
|
templated "index"
|
||||||
@ -514,9 +526,21 @@ get "/search" do |env|
|
|||||||
end
|
end
|
||||||
|
|
||||||
get "/login" do |env|
|
get "/login" do |env|
|
||||||
|
user = env.get? "user"
|
||||||
|
if user
|
||||||
|
next env.redirect "/feed/subscriptions"
|
||||||
|
end
|
||||||
|
|
||||||
referer = env.request.headers["referer"]?
|
referer = env.request.headers["referer"]?
|
||||||
referer ||= "/feed/subscriptions"
|
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 = env.params.query["tfa"]?
|
||||||
tfa ||= false
|
tfa ||= false
|
||||||
|
|
||||||
@ -538,6 +562,11 @@ post "/login" do |env|
|
|||||||
|
|
||||||
email = env.params.body["email"]?
|
email = env.params.body["email"]?
|
||||||
password = env.params.body["password"]?
|
password = env.params.body["password"]?
|
||||||
|
|
||||||
|
account_type = env.params.query["type"]?
|
||||||
|
account_type ||= "google"
|
||||||
|
|
||||||
|
if account_type == "google"
|
||||||
tfa_code = env.params.body["tfa"]?.try &.lchop("G-")
|
tfa_code = env.params.body["tfa"]?.try &.lchop("G-")
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@ -680,6 +709,90 @@ post "/login" do |env|
|
|||||||
error_message = "Login failed. This may be because two-factor authentication is not enabled on your account."
|
error_message = "Login failed. This may be because two-factor authentication is not enabled on your account."
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
elsif account_type == "invidious"
|
||||||
|
challenge_response = env.params.body["challenge_response"]?
|
||||||
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
|
action = env.params.body["action"]?
|
||||||
|
action ||= "signin"
|
||||||
|
|
||||||
|
if !email
|
||||||
|
error_message = "User ID is a required field"
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
if !password
|
||||||
|
error_message = "Password is a required field"
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
if !challenge_response || !token
|
||||||
|
error_message = "CAPTCHA is a required field"
|
||||||
|
next templated "error"
|
||||||
|
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
|
||||||
|
|
||||||
|
if action == "signin"
|
||||||
|
user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1 AND password IS NOT NULL", email, as: User)
|
||||||
|
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/signout" do |env|
|
get "/signout" do |env|
|
||||||
@ -782,8 +895,10 @@ get "/feed/subscriptions" do |env|
|
|||||||
headers = HTTP::Headers.new
|
headers = HTTP::Headers.new
|
||||||
headers["Cookie"] = env.request.headers["Cookie"]
|
headers["Cookie"] = env.request.headers["Cookie"]
|
||||||
|
|
||||||
|
if !user.password
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
user = get_user(user.id, client, headers, PG_DB)
|
user = get_user(user.id, client, headers, PG_DB)
|
||||||
|
end
|
||||||
|
|
||||||
max_results = preferences.max_results
|
max_results = preferences.max_results
|
||||||
max_results ||= env.params.query["maxResults"]?.try &.to_i
|
max_results ||= env.params.query["maxResults"]?.try &.to_i
|
||||||
@ -903,15 +1018,15 @@ get "/subscription_manager" do |env|
|
|||||||
|
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
|
||||||
|
if !user.password
|
||||||
# Refresh account
|
# Refresh account
|
||||||
headers = HTTP::Headers.new
|
headers = HTTP::Headers.new
|
||||||
headers["Cookie"] = env.request.headers["Cookie"]
|
headers["Cookie"] = env.request.headers["Cookie"]
|
||||||
|
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
user = get_user(user.id, client, headers, PG_DB)
|
user = get_user(user.id, client, headers, PG_DB)
|
||||||
|
end
|
||||||
subscriptions = user.subscriptions
|
subscriptions = user.subscriptions
|
||||||
subscriptions ||= [] of String
|
|
||||||
|
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
subscriptions = subscriptions.map do |ucid|
|
subscriptions = subscriptions.map do |ucid|
|
||||||
@ -941,6 +1056,7 @@ get "/subscription_ajax" do |env|
|
|||||||
channel_id = env.params.query["c"]?
|
channel_id = env.params.query["c"]?
|
||||||
channel_id ||= ""
|
channel_id ||= ""
|
||||||
|
|
||||||
|
if !user.password
|
||||||
headers = HTTP::Headers.new
|
headers = HTTP::Headers.new
|
||||||
headers["Cookie"] = env.request.headers["Cookie"]
|
headers["Cookie"] = env.request.headers["Cookie"]
|
||||||
|
|
||||||
@ -973,6 +1089,21 @@ get "/subscription_ajax" do |env|
|
|||||||
PG_DB.exec("UPDATE users SET subscriptions = array_remove(subscriptions,$1) WHERE id = $2", channel_id, sid)
|
PG_DB.exec("UPDATE users SET subscriptions = array_remove(subscriptions,$1) WHERE id = $2", channel_id, sid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
sid = user.id
|
||||||
|
|
||||||
|
case action
|
||||||
|
when .starts_with? "action_create"
|
||||||
|
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
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
env.redirect referer
|
env.redirect referer
|
||||||
|
@ -138,6 +138,7 @@ class User
|
|||||||
default: DEFAULT_USER_PREFERENCES,
|
default: DEFAULT_USER_PREFERENCES,
|
||||||
converter: PreferencesConverter,
|
converter: PreferencesConverter,
|
||||||
},
|
},
|
||||||
|
password: String?,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -814,7 +815,14 @@ def fetch_user(sid, client, headers, db)
|
|||||||
email = ""
|
email = ""
|
||||||
end
|
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
|
return user
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -947,3 +955,54 @@ def write_var_int(value : Int)
|
|||||||
|
|
||||||
return bytes
|
return bytes
|
||||||
end
|
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-1-5"></div>
|
||||||
<div class="pure-u-1 pure-u-md-3-5">
|
<div class="pure-u-1 pure-u-md-3-5">
|
||||||
<div class="h-box">
|
<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">
|
<form class="pure-form pure-form-stacked" action="/login?referer=<%= referer %>" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Login to Google</legend>
|
|
||||||
|
|
||||||
<label for="email">Email</label>
|
<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>
|
<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 %>
|
<% if tfa %>
|
||||||
<label for="tfa">Google verification code</label>
|
<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 %>
|
<% end %>
|
||||||
|
|
||||||
<button type="submit" class="pure-button pure-button-primary">Sign in</button>
|
<button type="submit" class="pure-button pure-button-primary">Sign in</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-1-5"></div>
|
<div class="pure-u-1 pure-u-md-1-5"></div>
|
||||||
|
Loading…
Reference in New Issue
Block a user