mirror of
				https://github.com/iv-org/invidious.git
				synced 2025-10-30 20:22:00 +00:00 
			
		
		
		
	Helpers: Add inv_sig_helper client
This commit is contained in:
		
							
								
								
									
										303
									
								
								src/invidious/helpers/sig_helper.cr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								src/invidious/helpers/sig_helper.cr
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,303 @@ | ||||
| require "uri" | ||||
| require "socket" | ||||
| require "socket/tcp_socket" | ||||
| require "socket/unix_socket" | ||||
|  | ||||
| private alias NetworkEndian = IO::ByteFormat::NetworkEndian | ||||
|  | ||||
| class Invidious::SigHelper | ||||
|   enum UpdateStatus | ||||
|     Updated | ||||
|     UpdateNotRequired | ||||
|     Error | ||||
|   end | ||||
|  | ||||
|   # ------------------- | ||||
|   #  Payload types | ||||
|   # ------------------- | ||||
|  | ||||
|   abstract struct Payload | ||||
|   end | ||||
|  | ||||
|   struct StringPayload < Payload | ||||
|     getter value : String | ||||
|  | ||||
|     def initialize(str : String) | ||||
|       raise Exception.new("SigHelper: String can't be empty") if str.empty? | ||||
|       @value = str | ||||
|     end | ||||
|  | ||||
|     def self.from_io(io : IO) | ||||
|       size = io.read_bytes(UInt16, NetworkEndian) | ||||
|       if size == 0 # Error code | ||||
|         raise Exception.new("SigHelper: Server encountered an error") | ||||
|       end | ||||
|  | ||||
|       if str = io.gets(limit: size) | ||||
|         return self.new(str) | ||||
|       else | ||||
|         raise Exception.new("SigHelper: Can't read string from socket") | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def self.to_io(io : IO) | ||||
|       # `.to_u16` raises if there is an overflow during the conversion | ||||
|       io.write_bytes(@value.bytesize.to_u16, NetworkEndian) | ||||
|       io.write(@value.to_slice) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   private enum Opcode | ||||
|     FORCE_UPDATE = 0 | ||||
|     DECRYPT_N_SIGNATURE = 1 | ||||
|     DECRYPT_SIGNATURE = 2 | ||||
|     GET_SIGNATURE_TIMESTAMP = 3 | ||||
|     GET_PLAYER_STATUS = 4 | ||||
|   end | ||||
|  | ||||
|   private struct Request | ||||
|     def initialize(@opcode : Opcode, @payload : Payload?) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # ---------------------- | ||||
|   #  High-level functions | ||||
|   # ---------------------- | ||||
|  | ||||
|   module Client | ||||
|     # Forces the server to re-fetch the YouTube player, and extract the necessary | ||||
|     # components from it (nsig function code, sig function code, signature timestamp). | ||||
|     def force_update : UpdateStatus | ||||
|       request = Request.new(Opcode::FORCE_UPDATE, nil) | ||||
|  | ||||
|       value = send_request(request) do |io| | ||||
|         io.read_bytes(UInt16, NetworkEndian) | ||||
|       end | ||||
|  | ||||
|       case value | ||||
|       when 0x0000 then return UpdateStatus::Error | ||||
|       when 0xFFFF then return UpdateStatus::UpdateNotRequired | ||||
|       when 0xF44F then return UpdateStatus::Updated | ||||
|       else | ||||
|         raise Exception.new("SigHelper: Invalid status code received") | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     # Decrypt a provided n signature using the server's current nsig function | ||||
|     # code, and return the result (or an error). | ||||
|     def decrypt_n_param(n : String) : String | ||||
|       request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n)) | ||||
|  | ||||
|       n_dec = send_request(request) do |io| | ||||
|         StringPayload.from_io(io).string | ||||
|       rescue ex | ||||
|         LOGGER.debug(ex.message) | ||||
|         nil | ||||
|       end | ||||
|  | ||||
|       return n_dec | ||||
|     end | ||||
|  | ||||
|     # Decrypt a provided s signature using the server's current sig function | ||||
|     # code, and return the result (or an error). | ||||
|     def decrypt_sig(sig : String) : String? | ||||
|       request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig)) | ||||
|  | ||||
|       sig_dec = send_request(request) do |io| | ||||
|         StringPayload.from_io(io).string | ||||
|       rescue ex | ||||
|         LOGGER.debug(ex.message) | ||||
|         nil | ||||
|       end | ||||
|  | ||||
|       return sig_dec | ||||
|     end | ||||
|  | ||||
|     # Return the signature timestamp from the server's current player | ||||
|     def get_sts : UInt64? | ||||
|       request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) | ||||
|  | ||||
|       return send_request(request) do |io| | ||||
|         io.read_bytes(UInt64, NetworkEndian) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     # Return the signature timestamp from the server's current player | ||||
|     def get_player : UInt32? | ||||
|       request = Request.new(Opcode::GET_PLAYER_STATUS, nil) | ||||
|  | ||||
|       send_request(request) do |io| | ||||
|         has_player = io.read_bytes(UInt8) == 0xFF | ||||
|         player_version = io.read_bytes(UInt32, NetworkEndian) | ||||
|       end | ||||
|  | ||||
|       return has_player ? player_version : nil | ||||
|     end | ||||
|  | ||||
|     private def send_request(request : Request, &block : IO) | ||||
|       channel = Multiplexor.send(request) | ||||
|       data_io = channel.receive | ||||
|       return yield data_io | ||||
|     rescue ex | ||||
|       LOGGER.debug(ex.message) | ||||
|       return nil | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # --------------------- | ||||
|   #  Low level functions | ||||
|   # --------------------- | ||||
|  | ||||
|   class Multiplexor | ||||
|     alias TransactionID = UInt32 | ||||
|     record Transaction, channel = ::Channel(Bytes).new | ||||
|  | ||||
|     @prng  = Random.new | ||||
|     @mutex = Mutex.new | ||||
|     @queue = {} of TransactionID => Transaction | ||||
|  | ||||
|     @conn : Connection | ||||
|  | ||||
|     INSTANCE = new | ||||
|  | ||||
|     def initialize | ||||
|       @conn = Connection.new | ||||
|       listen | ||||
|     end | ||||
|  | ||||
|     def initialize(url : String) | ||||
|       @conn = Connection.new(url) | ||||
|       listen | ||||
|     end | ||||
|  | ||||
|     def listen : Nil | ||||
|       raise "Socket is closed" if @conn.closed? | ||||
|  | ||||
|       # TODO: reopen socket if unexpectedly closed | ||||
|       spawn do | ||||
|         loop do | ||||
|           receive_data | ||||
|           Fiber.sleep | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def self.send(request : Request) | ||||
|       transaction = Transaction.new | ||||
|       transaction_id = @prng.rand(TransactionID) | ||||
|  | ||||
|       # Add transaction to queue | ||||
|       @mutex.synchronize do | ||||
|         # On a 64-bits random integer, this should never happen. Though, just in case, ... | ||||
|         if @queue[transaction_id]? | ||||
|           raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!") | ||||
|         end | ||||
|  | ||||
|         @queue[transaction_id] = transaction | ||||
|       end | ||||
|  | ||||
|       write_packet(transaction_id, request) | ||||
|  | ||||
|       return transaction.channel | ||||
|     end | ||||
|  | ||||
|     def receive_data : Payload | ||||
|       # Read a single packet from socker | ||||
|       transaction_id, data_io = read_packet | ||||
|  | ||||
|       # Remove transaction from queue | ||||
|       @mutex.synchronize do | ||||
|         transaction = @queue.delete(transaction_id) | ||||
|       end | ||||
|  | ||||
|       # Send data to the channel | ||||
|       transaction.channel.send(data) | ||||
|     end | ||||
|  | ||||
|     # Read a single packet from the socket | ||||
|     private def read_packet : {TransactionID, IO} | ||||
|       # Header | ||||
|       transaction_id = @conn.read_u32 | ||||
|       length = conn.read_u32 | ||||
|  | ||||
|       if length > 67_000 | ||||
|         raise Exception.new("SigHelper: Packet longer than expected (#{length})") | ||||
|       end | ||||
|  | ||||
|       # Payload | ||||
|       data_io = IO::Memory.new(1024) | ||||
|       IO.copy(@conn, data_io, limit: length) | ||||
|  | ||||
|       # data = Bytes.new() | ||||
|       # conn.read(data) | ||||
|  | ||||
|       return transaction_id, data_io | ||||
|     end | ||||
|  | ||||
|     # Write a single packet to the socket | ||||
|     private def write_packet(transaction_id : TransactionID, request : Request) | ||||
|       @conn.write_int(request.opcode) | ||||
|       @conn.write_int(transaction_id) | ||||
|       request.payload.to_io(@conn) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   class Connection | ||||
|     @socket : UNIXSocket | TCPSocket | ||||
|     @mutex = Mutex.new | ||||
|  | ||||
|     def initialize(host_or_path : String) | ||||
|       if host_or_path.empty? | ||||
|         host_or_path = default_path | ||||
|  | ||||
|       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 | ||||
|  | ||||
|     private default_path | ||||
|       return "/tmp/inv_sig_helper.sock" | ||||
|     end | ||||
|  | ||||
|     def closed? : Bool | ||||
|       return @socket.closed? | ||||
|     end | ||||
|  | ||||
|     def close : Nil | ||||
|       if @socket.closed? | ||||
|         raise Exception.new("SigHelper: Can't close socket, it's already closed") | ||||
|       else | ||||
|         @socket.close | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def gets(*args, **options) | ||||
|       @socket.gets(*args, **options) | ||||
|     end | ||||
|  | ||||
|     def read_bytes(*args, **options) | ||||
|       @socket.read_bytes(*args, **options) | ||||
|     end | ||||
|  | ||||
|     def write(*args, **options) | ||||
|       @socket.write(*args, **options) | ||||
|     end | ||||
|  | ||||
|     def write_bytes(*args, **options) | ||||
|       @socket.write_bytes(*args, **options) | ||||
|     end | ||||
|   end | ||||
| end | ||||
		Reference in New Issue
	
	Block a user
	 Samantaz Fox
					Samantaz Fox