Class: Aspera::WebServerSimple

Inherits:
WEBrick::HTTPServer
  • Object
show all
Defined in:
lib/aspera/web_server_simple.rb

Overview

Simple WEBrick server with HTTPS support

Direct Known Subclasses

Aspera::WebAuth::Server

Constant Summary collapse

PARAMS =
%i[cert key chain].freeze
DEFAULT_URL =
'http://localhost:8080'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(uri, cert: nil, key: nil, chain: nil) ⇒ WebServerSimple

Returns a new instance of WebServerSimple.

Parameters:

  • url (URI) —

    Local address where server will listen (use scheme, host and port only)

  • cert (String) (defaults to: nil) —

    Path to certificate file, either with extension .p12 or .pfx, else assumed PEM

  • key (String) (defaults to: nil) —

    Path to key file (PEM) or passphrase (pkcs12)

  • chain (String) (defaults to: nil) —

    Path to certificate chain file (PEM only)



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
# File 'lib/aspera/web_server_simple.rb', line 53

def initialize(uri, cert: nil, key: nil, chain: nil)
  Aspera.assert_type(uri, URI)
  @uri = uri
  # see https://www.rubydoc.info/stdlib/webrick/WEBrick/Config
  webrick_options = {
    BindAddress: @uri.host,
    Port:        @uri.port,
    Logger:      Log.log,
    AccessLog:   [[self, WEBrick::AccessLog::COMMON_LOG_FORMAT]] # replace default access log to call local method "<<" below
  }
  case @uri.scheme
  when 'http'
    Log.log.debug('HTTP mode')
  when 'https'
    # Required lazily: webrick/ssl.rb reads OpenSSL::OPENSSL_VERSION at load time,
    # a constant CosmoRuby's OpenSSL shim does not define, so requiring it
    # unconditionally at file load would break any command in a Cosmo build
    # even when no HTTPS server is ever started.
    require 'webrick/https'
    webrick_options[:SSLEnable] = true
    if cert.nil? && key.nil?
      webrick_options[:SSLCertName] = [['CN', WEBrick::Utils.getservername]]
    elsif cert && PKCS12_EXT.include?(File.extname(cert).downcase)
      # PKCS12
      Log.log.debug('Using PKCS12 certificate')
      Aspera.assert(!key.nil?, type: Error) { 'PKCS12 requires a key (password)' }
      pkcs12 = OpenSSL::PKCS12.new(File.read(cert), key)
      webrick_options[:SSLCertificate] = pkcs12.certificate
      webrick_options[:SSLPrivateKey] = pkcs12.key
      webrick_options[:SSLExtraChainCert] = pkcs12.ca_certs
    else
      Log.log.debug('Using PEM certificate')
      webrick_options[:SSLPrivateKey] = if key.nil?
        OpenSSL::PKey::RSA.new(4096)
      else
        OpenSSL::PKey::RSA.new(File.read(key))
      end
      webrick_options[:SSLCertificate] = if cert.nil?
        self.class.self_signed_cert(webrick_options[:SSLPrivateKey])
      else
        OpenSSL::X509::Certificate.new(File.read(cert))
      end
      webrick_options[:SSLExtraChainCert] = read_chain_file(chain) unless chain.nil?
      Aspera.assert(webrick_options[:SSLCertificate].public_key.to_der == webrick_options[:SSLPrivateKey].public_key.to_der, type: Error) { 'key and cert do not match' }
    end
  end
  # call constructor of parent class, but capture STDERR
  # self signed certificate generates characters on STDERR
  # see create_self_signed_cert in webrick/ssl.rb
  Log.capture_stderr { super(webrick_options) }
end

Class Method Details

.read_chain_file(chain) ⇒ Array<OpenSSL::X509::Certificate>

Returns list of Certificates from chain file.

Returns:

  • (Array<OpenSSL::X509::Certificate>) —

    list of Certificates from chain file



44
45
46
# File 'lib/aspera/web_server_simple.rb', line 44

def read_chain_file(chain)
  File.read(chain).scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m).map { |i| OpenSSL::X509::Certificate.new(i) }
end

.self_signed_cert(private_key, digest: 'SHA256') ⇒ Object

Generate or fill and self sign certificate



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/aspera/web_server_simple.rb', line 22

def self_signed_cert(private_key, digest: 'SHA256')
  cert = OpenSSL::X509::Certificate.new
  cert.subject = cert.issuer = OpenSSL::X509::Name.parse(GENERIC_ISSUER)
  cert.not_before = Time.now - CLOCK_SKEW_OFFSET_SEC
  cert.not_after  = cert.not_before + ONE_YEAR_SECONDS
  cert.public_key = private_key.public_key
  cert.serial = 0x0
  cert.version = 2
  ef = OpenSSL::X509::ExtensionFactory.new
  ef.issuer_certificate = cert
  ef.subject_certificate = cert
  cert.extensions = [
    ef.create_extension('basicConstraints', 'CA:TRUE', true),
    ef.create_extension('subjectKeyIdentifier', 'hash')
    # ef.create_extension('keyUsage', 'cRLSign,keyCertSign', true),
  ]
  cert.add_extension(ef.create_extension('authorityKeyIdentifier', 'keyid:always,issuer:always'))
  cert.sign(private_key, OpenSSL::Digest.new(digest))
  cert
end

Instance Method Details

#<<(access_log) ⇒ Object

log web server access ( option AccessLog )



115
116
117
# File 'lib/aspera/web_server_simple.rb', line 115

def <<(access_log)
  Log.log.debug { "webrick log #{access_log.chomp}" }
end

#start ⇒ Object

blocking



106
107
108
109
110
111
112
# File 'lib/aspera/web_server_simple.rb', line 106

def start
  Log.log.info { "Listening on #{@uri}" }
  # kill (-TERM) for graceful shutdown
  handler = proc { shutdown }
  %i{INT TERM}.each { |sig| trap(sig, &handler) }
  super
end