yahns.git  about / heads / tags
sleepy, multi-threaded, non-blocking application server for Ruby
blob d3caacb4c742457af4ce4ad02c255a1dabb20aca 4019 bytes (raw)
$ git show HEAD:lib/yahns/openssl_client.rb	# shows this blob on the CLI

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
 
# -*- encoding: binary -*-
# Copyright (C) 2013-2016 all contributors <yahns-public@yhbt.net>
# License: GPL-3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
# frozen_string_literal: true

# this is to be included into a Kgio::Socket-derived class
# this requires Ruby 2.1 and later for "exception: false"
module Yahns::OpenSSLClient # :nodoc:
  def self.included(cls)
    # Forward these methods to OpenSSL::SSL::SSLSocket so hijackers
    # can rely on stdlib methods instead of ugly kgio stuff that
    # we hope to phase out.
    # This is a bit weird, since OpenSSL::SSL::SSLSocket wraps
    # our actual socket, too, so we must take care to not blindly
    # use method_missing and cause infinite recursion
    %w(sync= read write readpartial write_nonblock read_nonblock
       print printf puts gets readlines readline getc
       readchar ungetc eof eof? << flush
       sysread syswrite).map!(&:to_sym).each do |m|
      cls.__send__(:define_method, m) { |*a| @ssl.__send__(m, *a) }
    end

    # block captures, ugh, but nobody really uses them
    %w(each each_line each_byte).map!(&:to_sym).each do |m|
      cls.__send__(:define_method, m) { |*a, &b| @ssl.__send__(m, *a, &b) }
    end
  end

  # this is special, called during IO initialization in Ruby
  def sync
    defined?(@ssl) ? @ssl.sync : super
  end

  def yahns_init_ssl(ssl_ctx)
    @need_accept = true
    @ssl = OpenSSL::SSL::SSLSocket.new(self, ssl_ctx)
    @ssl_blocked = nil
  end

  def kgio_trywrite(buf)
    len = buf.bytesize
    return if len == 0

    case @ssl_blocked
    when nil # likely
      buf = @ssl_blocked = buf.dup
    when Exception
      raise @ssl_blocked
    when String
      if @ssl_blocked != buf
        pfx = object_id
        warn("#{pfx} BUG: ssl_blocked != buf\n" \
             "#{pfx} ssl_blocked=#{@ssl_blocked.inspect}\n" \
             "#{pfx} buf=#{buf.inspect}\n")
        raise 'BUG: ssl_blocked} != buf'
      end
    end

    case rv = @ssl.write_nonblock(buf, exception: false)
    when :wait_readable, :wait_writable
      rv # do not clear ssl_blocked
    when Integer
      @ssl_blocked = len == rv ? nil : buf.byteslice(rv, len - rv)
    end
  rescue SystemCallError => e # ECONNRESET/EPIPE
    e.set_backtrace([])
    raise(@ssl_blocked = e)
  end

  def kgio_trywritev(buf)
    kgio_trywrite(buf.join)
  end

  def kgio_syssend(buf, flags)
    kgio_trywrite(buf)
  end

  def kgio_tryread(len, buf)
    if @need_accept
      # most protocols require read before write, so we start the negotiation
      # process here:
      case rv = accept_nonblock(@ssl)
      when :wait_readable, :wait_writable, nil
        return rv
      end
      @need_accept = false
    end
    @ssl.read_nonblock(len, buf, exception: false)
  end

  def trysendio(io, offset, count)
    return 0 if count == 0

    case buf = @ssl_blocked
    when nil
      buf = do_pread(io, count, offset) or return # nil for EOF
      buf = @ssl_blocked = buf.dup
    when Exception
      raise buf
    # when String # just use it as-is
    end

    # call write_nonblock directly since kgio_trywrite allocates
    # an unnecessary string
    len = buf.size
    case rv = @ssl.write_nonblock(buf, exception: false)
    when :wait_readable, :wait_writable
      return rv # do not clear ssl_blocked
    when Integer
      @ssl_blocked = len == rv ? nil : buf.byteslice(rv, len - rv)
    end
    rv
  rescue SystemCallError => e # ECONNRESET/EPIPE
    e.set_backtrace([])
    raise(@ssl_blocked = e)
  end

  def shutdown # we never call this with a how=SHUT_* arg
    @ssl.sysclose
  end

  alias trysendfile trysendio

  def close
    @ssl.close # flushes SSLSocket
    super # IO#close
  end

  if RUBY_VERSION.to_f >= 2.3
    def accept_nonblock(ssl)
      ssl.accept_nonblock(exception: false)
    end
  else
    def accept_nonblock(ssl)
      ssl.accept_nonblock
    rescue IO::WaitReadable
      :wait_readable
    rescue IO::WaitWritable
      :wait_writable
    rescue OpenSSL::SSL::SSLError
      nil
    end
  end
end

git clone git://yhbt.net/yahns.git
git clone https://yhbt.net/yahns.git