SigHelper: Fix many issues

This commit is contained in:
Samantaz Fox 2024-07-03 18:20:35 +02:00
parent ec8b7916fa
commit b509aa91d5
No known key found for this signature in database
GPG Key ID: F42821059186176E
2 changed files with 133 additions and 102 deletions

View File

@ -3,6 +3,10 @@ require "socket"
require "socket/tcp_socket" require "socket/tcp_socket"
require "socket/unix_socket" require "socket/unix_socket"
{% if flag?(:advanced_debug) %}
require "io/hexdump"
{% end %}
private alias NetworkEndian = IO::ByteFormat::NetworkEndian private alias NetworkEndian = IO::ByteFormat::NetworkEndian
class Invidious::SigHelper class Invidious::SigHelper
@ -20,58 +24,63 @@ class Invidious::SigHelper
end end
struct StringPayload < Payload struct StringPayload < Payload
getter value : String getter string : String
def initialize(str : String) def initialize(str : String)
raise Exception.new("SigHelper: String can't be empty") if str.empty? raise Exception.new("SigHelper: String can't be empty") if str.empty?
@value = str @string = str
end end
def self.from_io(io : IO) def self.from_bytes(slice : Bytes)
size = io.read_bytes(UInt16, NetworkEndian) size = IO::ByteFormat::NetworkEndian.decode(UInt16, slice)
if size == 0 # Error code if size == 0 # Error code
raise Exception.new("SigHelper: Server encountered an error") raise Exception.new("SigHelper: Server encountered an error")
end end
if str = io.gets(limit: size) if (slice.bytesize - 2) != size
raise Exception.new("SigHelper: String size mismatch")
end
if str = String.new(slice[2..])
return self.new(str) return self.new(str)
else else
raise Exception.new("SigHelper: Can't read string from socket") raise Exception.new("SigHelper: Can't read string from socket")
end end
end end
def self.to_io(io : IO) def to_io(io)
# `.to_u16` raises if there is an overflow during the conversion # `.to_u16` raises if there is an overflow during the conversion
io.write_bytes(@value.bytesize.to_u16, NetworkEndian) io.write_bytes(@string.bytesize.to_u16, NetworkEndian)
io.write(@value.to_slice) io.write(@string.to_slice)
end end
end end
private enum Opcode private enum Opcode
FORCE_UPDATE = 0 FORCE_UPDATE = 0
DECRYPT_N_SIGNATURE = 1 DECRYPT_N_SIGNATURE = 1
DECRYPT_SIGNATURE = 2 DECRYPT_SIGNATURE = 2
GET_SIGNATURE_TIMESTAMP = 3 GET_SIGNATURE_TIMESTAMP = 3
GET_PLAYER_STATUS = 4 GET_PLAYER_STATUS = 4
end end
private struct Request private record Request,
def initialize(@opcode : Opcode, @payload : Payload?) opcode : Opcode,
end payload : Payload?
end
# ---------------------- # ----------------------
# High-level functions # High-level functions
# ---------------------- # ----------------------
module Client module Client
extend self
# Forces the server to re-fetch the YouTube player, and extract the necessary # Forces the server to re-fetch the YouTube player, and extract the necessary
# components from it (nsig function code, sig function code, signature timestamp). # components from it (nsig function code, sig function code, signature timestamp).
def force_update : UpdateStatus def force_update : UpdateStatus
request = Request.new(Opcode::FORCE_UPDATE, nil) request = Request.new(Opcode::FORCE_UPDATE, nil)
value = send_request(request) do |io| value = send_request(request) do |bytes|
io.read_bytes(UInt16, NetworkEndian) IO::ByteFormat::NetworkEndian.decode(UInt16, bytes)
end end
case value case value
@ -79,20 +88,18 @@ class Invidious::SigHelper
when 0xFFFF then return UpdateStatus::UpdateNotRequired when 0xFFFF then return UpdateStatus::UpdateNotRequired
when 0xF44F then return UpdateStatus::Updated when 0xF44F then return UpdateStatus::Updated
else else
raise Exception.new("SigHelper: Invalid status code received") code = value.nil? ? "nil" : value.to_s(base: 16)
raise Exception.new("SigHelper: Invalid status code received #{code}")
end end
end end
# Decrypt a provided n signature using the server's current nsig function # Decrypt a provided n signature using the server's current nsig function
# code, and return the result (or an error). # code, and return the result (or an error).
def decrypt_n_param(n : String) : String def decrypt_n_param(n : String) : String?
request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n)) request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n))
n_dec = send_request(request) do |io| n_dec = send_request(request) do |bytes|
StringPayload.from_io(io).string StringPayload.from_bytes(bytes).string
rescue ex
LOGGER.debug(ex.message)
nil
end end
return n_dec return n_dec
@ -103,11 +110,8 @@ class Invidious::SigHelper
def decrypt_sig(sig : String) : String? def decrypt_sig(sig : String) : String?
request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig)) request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig))
sig_dec = send_request(request) do |io| sig_dec = send_request(request) do |bytes|
StringPayload.from_io(io).string StringPayload.from_bytes(bytes).string
rescue ex
LOGGER.debug(ex.message)
nil
end end
return sig_dec return sig_dec
@ -117,29 +121,30 @@ class Invidious::SigHelper
def get_sts : UInt64? def get_sts : UInt64?
request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil)
return send_request(request) do |io| return send_request(request) do |bytes|
io.read_bytes(UInt64, NetworkEndian) IO::ByteFormat::NetworkEndian.decode(UInt64, bytes)
end end
end end
# Return the signature timestamp from the server's current player # Return the current player's version
def get_player : UInt32? def get_player : UInt32?
request = Request.new(Opcode::GET_PLAYER_STATUS, nil) request = Request.new(Opcode::GET_PLAYER_STATUS, nil)
send_request(request) do |io| send_request(request) do |bytes|
has_player = io.read_bytes(UInt8) == 0xFF has_player = (bytes[0] == 0xFF)
player_version = io.read_bytes(UInt32, NetworkEndian) player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4])
end end
return has_player ? player_version : nil return has_player ? player_version : nil
end end
private def send_request(request : Request, &block : IO) private def send_request(request : Request, &)
channel = Multiplexor.send(request) channel = Multiplexor::INSTANCE.send(request)
data_io = channel.receive slice = channel.receive
return yield data_io return yield slice
rescue ex rescue ex
LOGGER.debug(ex.message) LOGGER.debug("SigHelper: Error when sending a request")
LOGGER.trace(ex.inspect_with_backtrace)
return nil return nil
end end
end end
@ -152,18 +157,13 @@ class Invidious::SigHelper
alias TransactionID = UInt32 alias TransactionID = UInt32
record Transaction, channel = ::Channel(Bytes).new record Transaction, channel = ::Channel(Bytes).new
@prng = Random.new @prng = Random.new
@mutex = Mutex.new @mutex = Mutex.new
@queue = {} of TransactionID => Transaction @queue = {} of TransactionID => Transaction
@conn : Connection @conn : Connection
INSTANCE = new INSTANCE = new("")
def initialize
@conn = Connection.new
listen
end
def initialize(url : String) def initialize(url : String)
@conn = Connection.new(url) @conn = Connection.new(url)
@ -173,22 +173,24 @@ class Invidious::SigHelper
def listen : Nil def listen : Nil
raise "Socket is closed" if @conn.closed? raise "Socket is closed" if @conn.closed?
LOGGER.debug("SigHelper: Multiplexor listening")
# TODO: reopen socket if unexpectedly closed # TODO: reopen socket if unexpectedly closed
spawn do spawn do
loop do loop do
receive_data receive_data
Fiber.sleep Fiber.yield
end end
end end
end end
def self.send(request : Request) def send(request : Request)
transaction = Transaction.new transaction = Transaction.new
transaction_id = @prng.rand(TransactionID) transaction_id = @prng.rand(TransactionID)
# Add transaction to queue # Add transaction to queue
@mutex.synchronize do @mutex.synchronize do
# On a 64-bits random integer, this should never happen. Though, just in case, ... # On a 32-bits random integer, this should never happen. Though, just in case, ...
if @queue[transaction_id]? if @queue[transaction_id]?
raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!") raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!")
end end
@ -201,75 +203,92 @@ class Invidious::SigHelper
return transaction.channel return transaction.channel
end end
def receive_data : Payload def receive_data
# Read a single packet from socker transaction_id, slice = read_packet
transaction_id, data_io = read_packet
# Remove transaction from queue
@mutex.synchronize do @mutex.synchronize do
transaction = @queue.delete(transaction_id) if transaction = @queue.delete(transaction_id)
# Remove transaction from queue and send data to the channel
transaction.channel.send(slice)
LOGGER.trace("SigHelper: Transaction unqueued and data sent to channel")
else
raise Exception.new("SigHelper: Received transaction was not in queue")
end
end end
# Send data to the channel
transaction.channel.send(data)
end end
# Read a single packet from the socket # Read a single packet from the socket
private def read_packet : {TransactionID, IO} private def read_packet : {TransactionID, Bytes}
# Header # Header
transaction_id = @conn.read_u32 transaction_id = @conn.read_bytes(UInt32, NetworkEndian)
length = conn.read_u32 length = @conn.read_bytes(UInt32, NetworkEndian)
LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}")
if length > 67_000 if length > 67_000
raise Exception.new("SigHelper: Packet longer than expected (#{length})") raise Exception.new("SigHelper: Packet longer than expected (#{length})")
end end
# Payload # Payload
data_io = IO::Memory.new(1024) slice = Bytes.new(length)
IO.copy(@conn, data_io, limit: length) @conn.read(slice) if length > 0
# data = Bytes.new() LOGGER.trace("SigHelper: payload = #{slice}")
# conn.read(data) LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done")
return transaction_id, data_io return transaction_id, slice
end end
# Write a single packet to the socket # Write a single packet to the socket
private def write_packet(transaction_id : TransactionID, request : Request) private def write_packet(transaction_id : TransactionID, request : Request)
@conn.write_int(request.opcode) LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}")
@conn.write_int(transaction_id)
request.payload.to_io(@conn) io = IO::Memory.new(1024)
io.write_bytes(request.opcode.to_u8, NetworkEndian)
io.write_bytes(transaction_id, NetworkEndian)
if payload = request.payload
payload.to_io(io)
end
@conn.send(io)
@conn.flush
LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} - Done")
end end
end end
class Connection class Connection
@socket : UNIXSocket | TCPSocket @socket : UNIXSocket | TCPSocket
@mutex = Mutex.new
{% if flag?(:advanced_debug) %}
@io : IO::Hexdump
{% end %}
def initialize(host_or_path : String) def initialize(host_or_path : String)
if host_or_path.empty? if host_or_path.empty?
host_or_path = default_path host_or_path = "/tmp/inv_sig_helper.sock"
begin
case host_or_path
when.starts_with?('/')
@socket = UNIXSocket.new(host_or_path)
when .starts_with?("tcp://")
uri = URI.new(host_or_path)
@socket = TCPSocket.new(uri.host, uri.port)
else
uri = URI.new("tcp://#{host_or_path}")
@socket = TCPSocket.new(uri.host, uri.port)
end
socket.sync = false
rescue ex
raise ConnectionError.new("Connection error", cause: ex)
end end
end
private default_path case host_or_path
return "/tmp/inv_sig_helper.sock" when .starts_with?('/')
@socket = UNIXSocket.new(host_or_path)
when .starts_with?("tcp://")
uri = URI.new(host_or_path)
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
else
uri = URI.new("tcp://#{host_or_path}")
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
end
LOGGER.debug("SigHelper: Listening on '#{host_or_path}'")
{% if flag?(:advanced_debug) %}
@io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true)
{% end %}
@socket.sync = false
@socket.blocking = false
end end
def closed? : Bool def closed? : Bool
@ -284,20 +303,23 @@ class Invidious::SigHelper
end end
end end
def gets(*args, **options) def flush(*args, **options)
@socket.gets(*args, **options) @socket.flush(*args, **options)
end end
def read_bytes(*args, **options) def send(*args, **options)
@socket.read_bytes(*args, **options) @socket.send(*args, **options)
end end
def write(*args, **options) # Wrap IO functions, with added debug tooling if needed
@socket.write(*args, **options) {% for function in %w(read read_bytes write write_bytes) %}
end def {{function.id}}(*args, **options)
{% if flag?(:advanced_debug) %}
def write_bytes(*args, **options) @io.{{function.id}}(*args, **options)
@socket.write_bytes(*args, **options) {% else %}
end @socket.{{function.id}}(*args, **options)
{% end %}
end
{% end %}
end end
end end

View File

@ -17,6 +17,15 @@ struct Invidious::DecryptFunction
end end
end end
def decrypt_nsig(n : String) : String?
self.check_update
return SigHelper::Client.decrypt_n_param(n)
rescue ex
LOGGER.debug(ex.message || "Signature: Unknown error")
LOGGER.trace(ex.inspect_with_backtrace)
return nil
end
def decrypt_signature(str : String) : String? def decrypt_signature(str : String) : String?
self.check_update self.check_update
return SigHelper::Client.decrypt_sig(str) return SigHelper::Client.decrypt_sig(str)