I am trying to create a simple SSL client and server in Ruby. But I'm getting a cryptic error message and the documentation is of no help.
Here is my server code:
#!/usr/bin/ruby
require "gserver"
require "openssl"
listeningPort = Integer(ARGV[0])
class Server < GServer
def initialize(listeningPort)
#sslContext = OpenSSL::SSL::SSLContext.new
#sslContext.cert = OpenSSL::X509::Certificate.new(File.open("MyCert.pem"))
super(listeningPort, "0.0.0.0")
end
def serve(io)
begin
ssl = OpenSSL::SSL::SSLSocket.new(io, #sslContext)
ssl.sync_close = true
ssl.connect
while (lineIn = ssl.gets)
lineIn = lineIn.chomp
$stdout.puts "=> " + lineIn
lineOut = "You said: " + lineIn
$stdout.puts "<= " + lineOut
ssl.puts lineOut
end
rescue
$stderr.puts $!
end
end
end
server = Server.new(listeningPort)
server.start
server.join
The client code is similar:
#!/usr/bin/ruby
require "socket"
require "thread"
require "openssl"
host = ARGV[0]
port = Integer(ARGV[1])
socket = TCPSocket.new(host, port)
sslContext = OpenSSL::SSL::SSLContext.new
sslContext.cert = OpenSSL::X509::Certificate.new(File.open("MyCert.pem"))
ssl = OpenSSL::SSL::SSLSocket.new(socket, sslContext)
ssl.sync_close = true
ssl.connect
puts ssl.peer_cert # this is nil
Thread.new {
begin
while lineIn = ssl.gets
lineIn = lineIn.chomp
$stdout.puts lineIn
end
rescue
$stderr.puts "Error in input loop: " + $!
end
}
while (lineOut = $stdin.gets)
lineOut = lineOut.chomp
ssl.puts lineOut
end
When I connect, I get this error on both the server and the client:
in `connect': SSL_connect returned=1 errno=0 state=SSLv2/v3 read server hello A: unknown protocol (OpenSSL::SSL::SSLError)
The problem could be that it doesn't trust the certificate (self-signed). I'm not sure how to tell the client to trust that certificate. Above, I have put the server's cert in the context, but that was just a shot in the dark. I'm not even sure my certificate is in an acceptable format (it is in base64 with the cert and the private key in the file). The documentation is very scant and there doesn't seem to be much on the web in this area either.
Any ideas?
I figured it out, thanks to that link to some decent documentation.
For one thing, SSLSocket.connect() is only meant to be called on the client.
But the main problem is that I'm trying to take a GServer socket and upgrade it to SSL. Instead, I should use OpenSSL::SSL::SSLServer.
Also, I separated my certificate and private key into two files.
Here is the working server:
#!/usr/bin/ruby
require "socket"
require "openssl"
require "thread"
listeningPort = Integer(ARGV[0])
server = TCPServer.new(listeningPort)
sslContext = OpenSSL::SSL::SSLContext.new
sslContext.cert = OpenSSL::X509::Certificate.new(File.open("cert.pem"))
sslContext.key = OpenSSL::PKey::RSA.new(File.open("priv.pem"))
sslServer = OpenSSL::SSL::SSLServer.new(server, sslContext)
puts "Listening on port #{listeningPort}"
loop do
connection = sslServer.accept
Thread.new {
begin
while (lineIn = connection.gets)
lineIn = lineIn.chomp
$stdout.puts "=> " + lineIn
lineOut = "You said: " + lineIn
$stdout.puts "<= " + lineOut
connection.puts lineOut
end
rescue
$stderr.puts $!
end
}
end
And client:
#!/usr/bin/ruby
require "socket"
require "thread"
require "openssl"
host = ARGV[0]
port = Integer(ARGV[1])
socket = TCPSocket.new(host, port)
expectedCert = OpenSSL::X509::Certificate.new(File.open("cert.pem"))
ssl = OpenSSL::SSL::SSLSocket.new(socket)
ssl.sync_close = true
ssl.connect
if ssl.peer_cert.to_s != expectedCert.to_s
stderrr.puts "Unexpected certificate"
exit(1)
end
Thread.new {
begin
while lineIn = ssl.gets
lineIn = lineIn.chomp
$stdout.puts lineIn
end
rescue
$stderr.puts "Error in input loop: " + $!
end
}
while (lineOut = $stdin.gets)
lineOut = lineOut.chomp
ssl.puts lineOut
end
if you need to provide intermediate certificates, set sslContext.extra_chain_cert with them.
If use have letsencrypt certificates, use your fullchain.pem for sslContext.cert, privkey.pem for sslContext.key and ["chain.pem"] for sslContext.extra_chain_cert. Doing so, you can also test the connection to the server from your browser, with full chain verification.
Note: this complements the author's answer https://stackoverflow.com/a/5873796/533510 , but it was rejected by user #joe , because he understands that this is not a complement for the question. If you agree to him, try using this answer as solution to the question!
Related
I am fairly new to ruby and I am having issues using code that I found on this
github. It is meant to use the Whitepages API to use their service and I am using it to do a reverse phone lookup. Here is my whitepages.rb file:
require 'json'
require 'net/https'
require 'uri'
class Whitepages
#Initialize the Whitepages class
# * api_key - The API key obtained from the Whitpages Developer website
def initialize(api_key)
api_version = "1.0"
#api_key = api_key
#base_uri = "http://api.whitepages.com/"
#find_person_uri = #base_uri + "find_person/" + api_version + "/?"
#reverse_phone_uri = #base_uri + "reverse_phone/" + api_version + "/?"
#reverse_address_uri = #base_uri + "reverse_address/" + api_version + "/?"
#uri = URI.parse(#base_uri)
#http = Net::HTTP.new(#uri.host, #uri.port)
end
#Retrieves contact information about a telephone number
#Accepts a hash:
# * phone - May be 7 digits if state provided otherwise 10 (*REQUIRED*)
# * state - Two digit code for the state
#More details may be found here: http://developer.whitepages.com/docs/Methods/reverse_phone
def reverse_phone(options)
resp, data = #http.get(build_uri(options, "reverse_phone"))
if resp.code== 200
return JSON.parse(data)
else
raise Exception,"code",resp.code
end
end
#Build the appropriate URL
def build_uri(options, type)
case type
when "reverse_phone"
built_uri = #reverse_phone_uri
when "reverse_address"
built_uri = #reverse_address_uri
when "find_person"
built_uri = #find_person_uri
end
options.each do |key,value|
if value != nil
built_uri = built_uri + key + "=" + value.gsub(' ', '%20') + ";"
end
end
built_uri = built_uri + "api_key=" + #api_key + ";outputtype=JSON"
return built_uri
end
end
And here is the class I use to test the code:
require 'rubygems'
require './whitepages'
API_KEY = MY ACTUAL API KEY AS A STRING
wp = Whitepages.new(API_KEY)
data = wp.reverse_phone({ "phone" => "4155551212",
"state" => "CA" })
When I run this file, I get the error: rescue in block in connect': Failed to open TCP connection to api.whitepages.com:80 (getaddrinfo: Name or service not known) (SocketError)
I can't find anything online specifically about connecting to APIs that would help me. Please, any help is greatly appreciated. PS: If you need to test the code, it does not take long at all to get a free API key from whitepages.com
I have created a very simple server:
#!/bin/ruby
require 'socket'
server = TCPServer.open 2000
puts "Listening on port 2000"
loop {
client = server.accept
client.puts "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n"
response = "My super slim ruby http server"
client.puts response
received = client.recv(1024)
puts received
puts "\n"
client.close
}
So far, it serves my purpose, which is to print out the requests that might come from a given client. However, if I use, for example, the following curl command to create a request:
curl -F "data=someData" http://localhost:2000
My ruby server only prints out the HTTP headers, but not the body of the request.
Is there a way to do this?
Looks like you have to call recv again to get the body:
#!/bin/ruby
require 'socket'
server = TCPServer.open 2000
puts "Listening on port 2000"
loop {
client = server.accept
client.puts "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n"
response = "My super slim ruby http server"
client.puts response
headers = client.recv(1024)
headers =~ /Content-Length: (\d+)/ # get content length
body = $1 ? client.recv($1.to_i) : '' # <- call again to get body if there is one
puts headers + body
client.close
}
Thanks bundacia, I ended up mixing what you sent and some other findings and this is the result:
#!/bin/ruby
require 'socket'
server = TCPServer.open 2000
puts "Listening on port 2000"
loop {
client = server.accept
client.puts "HTTP/1.1 200/OK\r\nContent-type:text/xml\r\n\r\n"
response = "My super slim ruby http server"
client.puts response
all_data = []
i = 1024
firstrun = "yes"
while i > 0
partial_data = client.recv(i)
if (firstrun == "no")
i = 0
end
if (firstrun == "yes")
partial_data =~ /Content-Length: (\d+)/ # get content length
length = $1
if (nil != length && !length.empty?)
i = length.to_i
firstrun = "no"
end
end
all_data << partial_data
end
puts all_data.join()
client.close
}
I've got a SSL Echo Server working fine when I tested with
gnutls-cli --starttls --port 9002 --insecure localhost
My SSL Echo server is as below:
#!/usr/bin/ruby
require 'socket';
require 'openssl';
certfile = 'privkey.pem';
host = "127.0.0.1"
port = 9002;
server = TCPServer.new( host, port );
# Establish an SSL context
sslContext = OpenSSL::SSL::SSLContext.new
sslContext.cert = OpenSSL::X509::Certificate.new(File.open( "myssl.cert.cert" ))
sslContext.key = OpenSSL::PKey::RSA.new(File.open( "myssl.cert.key" ))
# Create SSL server
sslServer = OpenSSL::SSL::SSLServer.new( server, sslContext );
# Don't expect an immidate SSL handshake upon connection.
sslServer.start_immediately = false;
sslSocket = sslServer.accept;
sslSocket.puts( "Toast.." );
# Server loop
while line = sslSocket.gets
line.chomp!;
if "STARTTLS" == line
# Starting TLS
sslSocket.accept;
end
sslSocket.puts( "Got '#{line}'" );
end
sslSocket.close;
My Client is however, not working (which I borrow somewhere in StackOverflow) as below:
#!/usr/bin/env ruby
require "socket"
require "thread"
require "openssl"
host = "127.0.0.1"
port = 9002
socket = TCPSocket.new(host, port)
expectedCert = OpenSSL::X509::Certificate.new(File.open("myssl.cert.cert"))
ssl = OpenSSL::SSL::SSLSocket.new(socket)
ssl.sync_close = true
ssl.connect
if ssl.peer_cert.to_s != expectedCert.to_s
stderrr.puts "Unexpected certificate"
exit(1)
end
Thread.new {
begin
while lineIn = ssl.gets
lineIn = lineIn.chomp
$stdout.puts lineIn
end
rescue
$stderr.puts "Error in input loop: " + $!
end
}
while (lineOut = $stdin.gets)
lineOut = lineOut.chomp
ssl.puts lineOut
end
Error I've got.
./sslclient.rb:13:in `connect': SSL_connect returned=1 errno=0 state=SSLv2/v3 read server hello A: unknown protocol (OpenSSL::SSL::SSLError)
from ./sslclient.rb:13:in `<main>'
Why is there a error in ssl.connect? Did I miss anything?
On the server side you start with plain text, send a welcome message and expect a starttls command - only then you upgrade to TLS. But on the client side you immediatly try to upgrade to TLS after the TCP connect, e.g. without reading the server hello and without sending the starttls command.
I have been trying to get port forwarding to work correctly with Net::SSH. From what I understand I need to fork out the Net::SSH session if I want to be able to use it from the same Ruby program so that the event handling loop can actually process packets being sent through the connection. However, this results in the ugliness you can see in the following:
#!/usr/bin/env ruby -w
require 'net/ssh'
require 'httparty'
require 'socket'
include Process
log = Logger.new(STDOUT)
log.level = Logger::DEBUG
local_port = 2006
child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000
hostname = "www.example.com"
pid = fork do
parent_socket.close
Net::SSH.start("hostname", "username") do |session|
session.logger = log
session.logger.sev_threshold=Logger::Severity::DEBUG
session.forward.local(local_port, hostname, 80)
child_socket.send("ready", 0)
pidi = fork do
msg = child_socket.recv(maxlen)
puts "Message from parent was: #{msg}"
exit
end
session.loop do
status = waitpid(pidi, Process::WNOHANG)
puts "Status: #{status.inspect}"
status.nil?
end
end
end
child_socket.close
puts "Message from child: #{parent_socket.recv(maxlen)}"
resp = HTTParty.post("http://localhost:#{local_port}/", :headers => { "Host" => hostname } )
# the write cannot be the last statement, otherwise the child pid could end up
# not receiving it
parent_socket.write("done")
puts resp.inspect
Can anybody show me a more elegant/better working solution to this?
I spend a lot of time trying to figure out how to correctly implement port forwarding, then I took inspiration from net/ssh/gateway library. I needed a robust solution that works after various possible connection errors. This is what I'm using now, hope it helps:
require 'net/ssh'
ssh_options = ['host', 'login', :password => 'password']
tunnel_port = 2222
begin
run_tunnel_thread = true
tunnel_mutex = Mutex.new
ssh = Net::SSH.start *ssh_options
tunnel_thread = Thread.new do
begin
while run_tunnel_thread do
tunnel_mutex.synchronize { ssh.process 0.01 }
Thread.pass
end
rescue => exc
puts "tunnel thread error: #{exc.message}"
end
end
tunnel_mutex.synchronize do
ssh.forward.local tunnel_port, 'tunnel_host', 22
end
begin
ssh_tunnel = Net::SSH.start 'localhost', 'tunnel_login', :password => 'tunnel_password', :port => tunnel_port
puts ssh_tunnel.exec! 'date'
rescue => exc
puts "tunnel connection error: #{exc.message}"
ensure
ssh_tunnel.close if ssh_tunnel
end
tunnel_mutex.synchronize do
ssh.forward.cancel_local tunnel_port
end
rescue => exc
puts "tunnel error: #{exc.message}"
ensure
run_tunnel_thread = false
tunnel_thread.join if tunnel_thread
ssh.close if ssh
end
That's just how SSH in general is. If you're offended by how ugly it looks, you should probably wrap up that functionality into a port forwarding class of some sort so that the exposed part is a lot more succinct. An interface like this, perhaps:
forwarder = PortForwarder.new(8080, 'remote.host', 80)
So I have found a slightly better implementation. It only requires a single fork but still uses a socket for the communication. It uses IO#read_nonblock for checking if a message is ready. If there isn't one, the method throws an exception, in which case the block continues to return true and the SSH session keeps serving requests. Once the parent is done with the connection it sends a message, which causes child_socket.read_nonblock(maxlen).nil? to return false, making the loop exit and therefore shutting down the SSH connection.
I feel a little better about this, so between that and #tadman's suggestion to wrap it in a port forwarding class I think it's about as good as it'll get. However, any further suggestions for improving this are most welcome.
#!/usr/bin/env ruby -w
require 'net/ssh'
require 'httparty'
require 'socket'
log = Logger.new(STDOUT)
log.level = Logger::DEBUG
local_port = 2006
child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000
hostname = "www.example.com"
pid = fork do
parent_socket.close
Net::SSH.start("ssh-tunnel-hostname", "username") do |session|
session.logger = log
session.logger.sev_threshold=Logger::Severity::DEBUG
session.forward.local(local_port, hostname, 80)
child_socket.send("ready", 0)
session.loop { child_socket.read_nonblock(maxlen).nil? rescue true }
end
end
child_socket.close
puts "Message from child: #{parent_socket.recv(maxlen)}"
resp = HTTParty.post("http://localhost:#{local_port}/", :headers => { "Host" => hostname } )
# the write cannot be the last statement, otherwise the child pid could end up
# not receiving it
parent_socket.write("done")
puts resp.inspect
I'm trying to use built-in XMLRPC in Ruby 1.9. Unfortunately, XMLRPC is not documented on ruby-docs.org, so i have tried to build a test code based on examples found by google:
# Server.rb
require "xmlrpc/server"
server = XMLRPC::Server.new( 1234 )
server.add_handler( "test" ) { |msg| return "responce for #{msg}" }
server.serve()
# Client.rb
require "xmlrpc/client"
server = XMLRPC::Client.new( "localhost", "/", 1234 )
server.call( "test", 42 ) == "responce for 42"
Unfortunately, this is not working on both Windows and OSX. Server.rb fails with cryptic error:
C:/Ruby193/lib/ruby/1.9.1/xmlrpc/client.rb:414:in `call': Uncaught exception unexpected return in method test (XMLRPC::FaultException)
from client.rb:6:in `<main>'
Maybe anyone knows what is my error?
Its another way with block:
#server.rb:
require "xmlrpc/server"
server = XMLRPC::Server.new( 1234 )
server.add_handler('my_test.test') { |msg|"responce for #{msg}" }
#client.rb
require "xmlrpc/client"
client = XMLRPC::Client.new( "localhost", "/", 1234 )
s = client.call('my_test.test','asd')
You got it almost right. Here is a tutorial you can use. Your example needs a little modification, you have to pass an object to add_handler that will be used to serve your RPC calls:
# server.rb
require "xmlrpc/server"
class MyClass
def dosomething(a)
"response for #{a}"
end
end
server = XMLRPC::Server.new( 1234 )
server.add_handler( "test", MyClass.new )
server.serve
# client.rb
require "xmlrpc/client"
server = XMLRPC::Client.new( "localhost", "/", 1234 )
puts server.call( "test.dosomething", 42 ) == "response for 42"
Please note: the default xmlrpc/client.rb impl. doesn't support client certificates based https connections. If you want it you must either use different lib or patch client.rb with something like:
# HG changeset patch
# User Anonymous Coward <anonymous#gmail.com>
# Date 1338149770 -10800
# Node ID f0557306c8e4f113507fb3bab8567391949fa302
# Parent 3eae8e8f9e065ff6cdf1c95092ad5cca635c9eac
patch client.rb to support https with client certificate.
diff -r 3eae8e8f9e06 -r f0557306c8e4 client.rb
--- a/client.rb Sun May 27 22:20:18 2012 +0300
+++ b/client.rb Sun May 27 23:16:10 2012 +0300
## -292,8 +292,8 ##
# Constructors -------------------------------------------------------------------
- def initialize(host=nil, path=nil, port=nil, proxy_host=nil, proxy_port=nil,
- user=nil, password=nil, use_ssl=nil, timeout=nil)
+ def initialize(host=nil, path=nil, port=nil, proxy_host=nil, proxy_port=nil,
+ user=nil, password=nil, use_ssl=nil, timeout=nil, cacert=nil, cert=nil, key=nil)
#http_header_extra = nil
#http_last_response = nil
## -311,6 +311,10 ##
if use_ssl
require "net/https"
#port = port || 443
+ #cacert = cacert
+ #cert = cert
+ #key = key
+
else
#port = port || 80
end
## -325,8 +329,19 ##
# HTTP object for synchronous calls
Net::HTTP.version_1_2
- #http = Net::HTTP.new(#host, #port, #proxy_host, #proxy_port)
- #http.use_ssl = #use_ssl if #use_ssl
+ #http = Net::HTTP.new(#host, #port, #proxy_host, #proxy_port)
+ if #use_ssl
+ #http.use_ssl = #use_ssl
+ if nil != #cacert
+ #http.ca_file = #cacert
+ #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ #http.verify_depth = 5
+ else
+ #http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+ #http.cert = #cert
+ #http.key = #key
+ end
#http.read_timeout = #timeout
#http.open_timeout = #timeout
## -366,7 +381,7 ##
hash.each { |k,v| h[k.to_s.downcase] = v }
self.new(h['host'], h['path'], h['port'], h['proxy_host'], h['proxy_port'], h['user'], h['password'],
I think this could help:
http://www.ntecs.de/ruby/xmlrpc4r/howto.html
#server.rb
require "xmlrpc/server"
server = XMLRPC::Server.new( 1234 )
class MyHandler
def test(msg)
"message #{msg}"
end
end
server.add_handler(XMLRPC::iPIMethods("my_test"), MyHandler.new)
server.serve
#client.rb
require "xmlrpc/client"
server = XMLRPC::Client.new( "localhost", "/", 1234 )
s = server.call('my_test.test','hello!')