aboutsummaryrefslogtreecommitdiffstats
path: root/lib/plum/rack/listener.rb
blob: c650ff03268fad7eee5bc3571ce39a1a320384be (plain)
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
# -*- frozen-string-literal: true -*-
module Plum
  module Rack
    class BaseListener
      def stop
        @server.close
      end

      def to_io
        raise "not implemented"
      end

      def method_missing(name, *args)
        @server.__send__(name, *args)
      end
    end

    class TCPListener < BaseListener
      def initialize(lc)
        @server = ::TCPServer.new(lc[:hostname], lc[:port])
      end

      def to_io
        @server.to_io
      end

      def plum(sock)
        ::Plum::HTTPServerConnection.new(sock.method(:write))
      end
    end

    class TLSListener < BaseListener
      def initialize(lc)
        if lc[:certificate] && lc[:certificate_key]
          cert = File.read(lc[:certificate])
          key = File.read(lc[:certificate_key])
        else
          STDERR.puts "WARNING: using dummy certificate"
          cert, key = dummy_key
        end

        ctx = OpenSSL::SSL::SSLContext.new
        ctx.ssl_version = :TLSv1_2
        ctx.alpn_select_cb = -> protocols {
          if protocols.include?("h2")
            "h2"
          else
            protocols.first
          end
        }
        ctx.tmp_ecdh_callback = -> (sock, ise, keyl) { OpenSSL::PKey::EC.new("prime256v1") }
        ctx.cert = OpenSSL::X509::Certificate.new(cert)
        ctx.key = OpenSSL::PKey::RSA.new(key)
        tcp_server = ::TCPServer.new(lc[:hostname], lc[:port])
        @server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
        @server.start_immediately = false
      end

      def to_io
        @server.to_io
      end

      def plum(sock)
        raise ::Plum::LegacyHTTPError.new("client doesn't offered h2 with ALPN", nil) unless sock.alpn_protocol == "h2"
        ::Plum::ServerConnection.new(sock.method(:write))
      end

      private
      # returns: [cert, key]
      def dummy_key
        puts "WARNING: Generating new dummy certificate..."

        key = OpenSSL::PKey::RSA.new(2048)
        cert = OpenSSL::X509::Certificate.new
        cert.subject = cert.issuer = OpenSSL::X509::Name.parse("/C=JP/O=Test/OU=Test/CN=example.com")
        cert.not_before = Time.now
        cert.not_after = Time.now + 363 * 24 * 60 * 60
        cert.public_key = key.public_key
        cert.serial = rand((1 << 20) - 1)
        cert.version = 2

        ef = OpenSSL::X509::ExtensionFactory.new
        ef.subject_certificate = cert
        ef.issuer_certificate = cert
        cert.extensions = [
          ef.create_extension("basicConstraints", "CA:TRUE", true),
          ef.create_extension("subjectKeyIdentifier", "hash"),
        ]
        cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")

        cert.sign key, OpenSSL::Digest::SHA1.new

        [cert, key]
      end
    end

    class UNIXListener < BaseListener
      def initialize(lc)
        if File.exist?(lc[:path])
          begin
            old = UNIXSocket.new(lc[:path])
          rescue SystemCallError, IOError
            File.unlink(lc[:path])
          else
            old.close
            raise "Already a server bound to: #{lc[:path]}"
          end
        end

        @server = ::UNIXServer.new(lc[:path])

        File.chmod(lc[:mode], lc[:path]) if lc[:mode]
      end

      def stop
        super
        File.unlink(lc[:path])
      end

      def to_io
        @server.to_io
      end

      def plum(sock)
        ::Plum::ServerConnection.new(sock.method(:write))
      end
    end
  end
end