Skip to content

Commit

Permalink
Implement SSL certificate verification, based on Faraday's implementa…
Browse files Browse the repository at this point in the history
  • Loading branch information
jcoglan committed Jul 20, 2020
1 parent f33fd69 commit 74de500
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 24 deletions.
2 changes: 2 additions & 0 deletions examples/client.rb
Expand Up @@ -6,9 +6,11 @@
EM.run {
url = ARGV[0]
proxy = ARGV[1]
ca = File.expand_path('../../spec/server.crt', __FILE__)

ws = Faye::WebSocket::Client.new(url, [],
:proxy => { :origin => proxy, :headers => { 'User-Agent' => 'Echo' } },
:tls => { :cert_authority_file => ca },
:headers => { 'Origin' => 'http://faye.jcoglan.com' },
:extensions => [PermessageDeflate]
)
Expand Down
7 changes: 4 additions & 3 deletions lib/faye/websocket.rb
Expand Up @@ -17,9 +17,10 @@ module Faye
class WebSocket
root = File.expand_path('../websocket', __FILE__)

autoload :Adapter, root + '/adapter'
autoload :API, root + '/api'
autoload :Client, root + '/client'
autoload :Adapter, root + '/adapter'
autoload :API, root + '/api'
autoload :Client, root + '/client'
autoload :SslVerifier, root + '/ssl_verifier'

ADAPTERS = {
'goliath' => :Goliath,
Expand Down
58 changes: 39 additions & 19 deletions lib/faye/websocket/client.rb
Expand Up @@ -17,20 +17,18 @@ def initialize(url, protocols = nil, options = {})
super(options) { ::WebSocket::Driver.client(self, :max_length => options[:max_length], :protocols => protocols) }

proxy = options.fetch(:proxy, {})
endpoint = URI.parse(proxy[:origin] || @url)
port = endpoint.port || DEFAULT_PORTS[endpoint.scheme]
@secure = SECURE_PROTOCOLS.include?(endpoint.scheme)
@endpoint = URI.parse(proxy[:origin] || @url)
port = @endpoint.port || DEFAULT_PORTS[@endpoint.scheme]
@origin_tls = options.fetch(:tls, {})
@socket_tls = proxy[:origin] ? proxy.fetch(:tls, {}) : @origin_tls

configure_proxy(proxy)

EventMachine.connect(endpoint.host, port, Connection) do |conn|
EventMachine.connect(@endpoint.host, port, Connection) do |conn|
conn.parent = self
end
rescue => error
emit_error("Network error: #{ url }: #{ error.message }")
finalize_close
on_network_error(error)
end

private
Expand All @@ -46,38 +44,60 @@ def configure_proxy(proxy)
end

@proxy.on(:connect) do
uri = URI.parse(@url)
secure = SECURE_PROTOCOLS.include?(uri.scheme)
@proxy = nil

if secure
origin_tls = { :sni_hostname => uri.host }.merge(@origin_tls)
@stream.start_tls(origin_tls)
end

start_tls(URI.parse(@url), @origin_tls)
@driver.start
end
end

def start_tls(uri, options)
return unless SECURE_PROTOCOLS.include?(uri.scheme)

tls_options = { :sni_hostname => uri.host, :verify_peer => true }.merge(options)
@ssl_verifier = SslVerifier.new(uri.host, tls_options)
@stream.start_tls(tls_options)
end

def on_connect(stream)
@stream = stream

if @secure
socket_tls = { :sni_hostname => URI.parse(@url).host }.merge(@socket_tls)
@stream.start_tls(socket_tls)
end
start_tls(@endpoint, @socket_tls)

worker = @proxy || @driver
worker.start
end

def on_network_error(error)
emit_error("Network error: #{ @url }: #{ error.message }")
finalize_close
end

def ssl_verify_peer(cert)
@ssl_verifier.verify(cert)
rescue => error
on_network_error(error)
end

def ssl_handshake_completed
@ssl_verifier.complete_handshake
rescue => error
on_network_error(error)
end

module Connection
attr_accessor :parent

def connection_completed
parent.__send__(:on_connect, self)
end

def ssl_verify_peer(cert)
parent.__send__(:ssl_verify_peer, cert)
end

def ssl_handshake_completed
parent.__send__(:ssl_handshake_completed)
end

def receive_data(data)
parent.__send__(:parse, data)
end
Expand Down
89 changes: 89 additions & 0 deletions lib/faye/websocket/ssl_verifier.rb
@@ -0,0 +1,89 @@
# This code is based on the implementation in Faraday:
#
# https://github.com/lostisland/faraday/blob/v1.0.1/lib/faraday/adapter/em_http_ssl_patch.rb
#
# Faraday is published under the MIT license as detailed here:
#
# https://github.com/lostisland/faraday/blob/v1.0.1/LICENSE.md
#
# Copyright (c) 2009-2019 Rick Olson, Zack Hobson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

require 'openssl'

module Faye
class WebSocket

SSLError = Class.new(OpenSSL::SSL::SSLError)

class SslVerifier
def initialize(hostname, ssl_opts)
@hostname = hostname
@ssl_opts = ssl_opts
@cert_store = OpenSSL::X509::Store.new

if ca = @ssl_opts[:cert_authority_file]
[ca].flatten.each { |ca_path| @cert_store.add_file(ca_path) }
else
@cert_store.set_default_paths
end
end

def verify(cert_text)
return true unless should_verify?

certificate = parse_cert(cert_text)
return false unless certificate

unless @cert_store.verify(certificate)
raise SSLError, "Unable to verify the server certificate for '#{ @hostname }'"
end

store_cert(certificate)
@last_cert = certificate

true
end

def complete_handshake
return unless should_verify?

unless last_certificate_verified?
raise SSLError, "Host '#{ @hostname }' does not match the server certificate"
end
end

private

def should_verify?
@ssl_opts[:verify_peer] != false
end

def parse_cert(cert_text)
OpenSSL::X509::Certificate.new(cert_text)
rescue OpenSSL::X509::CertificateError
nil
end

def store_cert(certificate)
@cert_store.add_cert(certificate)
rescue OpenSSL::X509::StoreError => error
raise error unless error.message == 'cert already in hash table'
end

def last_certificate_verified?
@last_cert and OpenSSL::SSL.verify_certificate_identity(@last_cert, @hostname)
end
end

end
end
8 changes: 6 additions & 2 deletions spec/faye/websocket/client_spec.rb
Expand Up @@ -39,14 +39,14 @@ def open_socket(url, protocols, &callback)
end
end

@ws = Faye::WebSocket::Client.new(url, protocols, :proxy => { :origin => proxy_url })
@ws = Faye::WebSocket::Client.new(url, protocols, :proxy => { :origin => proxy_url }, :tls => tls_options)

@ws.on(:open) { |e| resume.call(true) }
@ws.onclose = lambda { |e| resume.call(false) }
end

def open_socket_and_close_it_fast(url, protocols, &callback)
@ws = Faye::WebSocket::Client.new(url, protocols)
@ws = Faye::WebSocket::Client.new(url, protocols, :tls => tls_options)

@ws.on(:open) { |e| @open = @ever_opened = true }
@ws.onclose = lambda { |e| @open = false }
Expand Down Expand Up @@ -130,6 +130,10 @@ def wait(seconds, &callback)
let(:wrong_url) { "ws://#{ localhost }:9999/" }
let(:secure_url) { "wss://#{ localhost }:#{ port }/" }

let :tls_options do
{ :cert_authority_file => File.expand_path('../../../server.crt', __FILE__) }
end

shared_examples_for "socket client" do
before do
@ever_opened = @message = nil
Expand Down

0 comments on commit 74de500

Please sign in to comment.