samapi.rb 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # Copyright 2022 Marek Küthe
  2. # GNU GPLv3
  3. require "socket"
  4. require "io/wait"
  5. # Documentation about the SAM API can be found at https://geti2p.net/en/docs/api/samv3.
  6. # This class is just my attempt to build it into Ruby as a kind of wrapper.
  7. class SamApi
  8. # Creates a SAM command based on the first command, the second command, and
  9. # the arguments
  10. #
  11. # @param first [String, Symbol]
  12. # @param second [String, Symbol]
  13. # @param args [Hash]
  14. # @return [String]
  15. # @example
  16. # SamApi.create_command :hello, :version, { "MIN" => "3.0" } # => "HELLO VERSION MIN=3.0"
  17. def self.create_command first, second = nil, args = {}
  18. cmd = "#{first.to_s.upcase}"
  19. cmd += " #{second.to_s.upcase}" if second
  20. args.each_pair { |key, value|
  21. if key && value
  22. value = value.to_s
  23. value = value.include?(" ") ? "\"#{value}\"" : value
  24. cmd += " #{key.to_s}=#{value}"
  25. end
  26. }
  27. return cmd
  28. end
  29. # Extracts the first and second command and arguments from a SAM command and
  30. # returns them as a hash.
  31. #
  32. # @param cmd [String]
  33. # @return [Hash]
  34. # @example
  35. # SamApi.parse_command "HELLO REPLY RESULT=OK VERSION=3.3" # => {:first=>"HELLO", :second=>"REPLY", :args=>{:result=>"OK", :version=>"3.3"}}
  36. def self.parse_command cmd
  37. parsed = cmd.scan(/(?:\"(.*?)\")|([^" =]+)/).map { |arr|
  38. arr.compact[0]
  39. }
  40. # thanks to https://stackoverflow.com/questions/71010013/regex-does-not-return-all-the-argument
  41. first = parsed[0]
  42. second = parsed[1]
  43. args = {}
  44. for i in (2...parsed.length).step 2
  45. args[parsed[i].downcase.to_sym] = parsed[i + 1]
  46. end
  47. return {first: first, second: second, args: args}
  48. end
  49. attr_reader :version
  50. attr_accessor :socket
  51. # Initializes a SAM session but does not shake hands (HELLO VERSION)
  52. #
  53. # @param _host [String] Host on which the SAM Server is running.
  54. # @param _port [Integer] Port on which the SAM Server is running.
  55. def initialize host: "127.0.0.1", port: 7656
  56. @host = host
  57. @port = port
  58. @socket = TCPSocket.new @host, @port
  59. end
  60. # Checks whether there is still a connection to the SAM server.
  61. #
  62. # @return [TrueClass, FalseClass] true if there is still a connection, otherwise false
  63. def is_open?
  64. return ! @socket.closed?
  65. end
  66. # Sends a command directly to the SAM server and returns an evaluated response.
  67. #
  68. # @param first [String, Symbol] see SamApi.create_command
  69. # @param second [String, Symbol] see SamApi.create_command
  70. # @param args [Hash] see SamApi.create_command
  71. # @return [Hash] see SamApi.parse_command
  72. def send_cmd first, second, args
  73. cmd = SamApi.create_command first, second, args
  74. @socket.puts cmd
  75. ans = @socket.gets.chomp
  76. ans_parsed = SamApi.parse_command ans
  77. return ans_parsed
  78. end
  79. # It can happen that the SAM server sends a ping. The client is instructed to
  80. # respond with a pong. This function checks whether the server requests a pong
  81. # and sends one if it does. This should always be called up when you are not
  82. # actively communicating with the SAM server.
  83. def check_ping
  84. if @socket.ready? && @socket.ready? != 0 && ! @socket.closed?
  85. ans = @socket.gets.chomp.split " "
  86. cmd = ans[0].downcase
  87. arg = ans[1]
  88. if cmd == "ping"
  89. @socket.puts "PONG #{arg}"
  90. end
  91. end
  92. end
  93. def send_ping _test = nil
  94. test = _test
  95. test = Time.now.to_i.to_s if ! test
  96. @socket.puts "PING #{test}"
  97. ans = @socket.gets.chomp.split " "
  98. cmd = ans[0].downcase
  99. arg = ans[1]
  100. return cmd == "pong" && arg == test
  101. end
  102. # Performs a HELLO VERSION handshake with the SAM server.
  103. #
  104. # @param args [Hash] could min, max, user and password
  105. # @return [Array] The first element contains either true or false depending on
  106. # whether the handshake was successful. The second element contains the
  107. # evaluated answer.
  108. def handshake args = {}
  109. ans = send_cmd :hello, :version, args
  110. @version = ans[:args][:version]
  111. status = ans[:args][:result] == "OK"
  112. return [status, ans]
  113. end
  114. def session_create args = {}
  115. ans = send_cmd :session, :create, args
  116. priv_key = ans[:args][:destination]
  117. status = ans[:args][:result] == "OK"
  118. return [status, priv_key, ans]
  119. end
  120. def session_add args = {}
  121. ans = send_cmd :session, :add, args
  122. priv_key = ans[:args][:destination]
  123. status = ans[:args][:result] == "OK"
  124. return [status, priv_key, ans]
  125. end
  126. def session_remove args = {}
  127. ans = send_cmd :session, :remove, args
  128. status = ans[:args][:result] == "OK"
  129. return [status, ans]
  130. end
  131. def stream_connect args = {}, check = true
  132. ans = send_cmd :stream, :connect, args
  133. status = check ? ans[:args][:result] == "OK" : nil
  134. return [status, @socket, ans]
  135. end
  136. def stream_accept args = {}
  137. ans = send_cmd :stream, :accept, args
  138. status = ans[:args][:result] == "OK"
  139. return [status, @socket, ans]
  140. end
  141. def stream_forward args = {}
  142. ans = send_cmd :stream, :forward, args
  143. status = ans[:args][:result] == "OK"
  144. return [status, ans]
  145. end
  146. def naming_lookup name
  147. ans = send_cmd :naming, :lookup, { "NAME" => name }
  148. status = ans[:args][:result] == "OK"
  149. name = ans[:args][:name]
  150. return [status, name, ans]
  151. end
  152. def dest_generate args
  153. ans = send_cmd :dest, :generate, args
  154. pub_key = ans[:args][:pub]
  155. priv_key = ans[:args][:priv]
  156. return [pub_key, priv_key]
  157. end
  158. # Closes the connection to the SAM server
  159. def close cmd = "QUIT"
  160. @socket.puts cmd
  161. @socket.close if ! @socket.closed?
  162. end
  163. end