<% require "openssl" %>---
<%=
def property_or_link(description, property, link_path, link_name=nil, optional=false)
  link_name ||= link_path.split('.').first
  if_p(property) do |prop|
    return prop
  end.else do
    if_link(link_name) do |link_object|
      link_object.if_p(link_path) do |prop|
        return prop
      end
    end
  end
  if optional
    return nil
  end
  raise RuntimeError, "#{description} not found in properties nor in \"#{link_name}\" link. This value can be specified using the \"#{property}\" property."
end

def parse_ip (ip, var_name)
  unless ip.empty?
    ip = ip.split(":")[0]
      begin
        parsed = IPAddr.new ip
      rescue  => e
        raise "Invalid #{var_name} '#{ip}': #{e}"
      end
  end
end

def validate_max_header_kb (kb)
  if kb < 1 or kb > 1024
    raise "Invalid router.max_header_kb value. Must be between 1 and 1,024 kb"
  end
  kb * 1024
end

def validate_balancing_algorithm (algorithm)
  valid_balancing_algorithms = ['round-robin', 'least-connection']
  unless valid_balancing_algorithms.include?(algorithm)
    raise "Invalid router.balancing_algorithm \"#{algorithm}\". Must be \"round-robin\" or \"least-connection\""
  end
  algorithm
end

def validate_az_preference (az_preference)
  valid_az_preferences = ['none', 'locally-optimistic']
  unless valid_az_preferences.include?(az_preference)
    raise "Invalid router.balancing_algorithm_az_preference \"#{az_preference}\". Must be \"none\" or \"locally-optimistic\""
  end
  az_preference
end

def get_valid_ca_certs
  ca_certs = p('router.ca_certs')

  if !ca_certs.is_a?(Array)
    raise 'ca_certs must be provided as an array of strings containing one or more certificates in PEM encoding'
  end

  ca_certs.select{ |v| !v.nil? && !v.strip.empty? && v.length > 50 }
end

parse_ip(p('router.debug_address'), 'router.debug_address')

access_log_file = p('router.write_access_logs_locally') ? '/var/vcap/sys/log/gorouter/access.log' : ''

def status_tls
  tls = {}
    if p('router.status.tls.certificate', "") == ""
      raise 'router.status.tls.certificate must be provided when router.status.tls.port is set'
    end
    if p('router.status.tls.key', "") == ""
      raise 'router.status.tls.key must be provided when router.status.tls.port is set'
    end

    tls = {
      'port' => p('router.status.tls.port'),
      'certificate' => p('router.status.tls.certificate'),
      'key' => p('router.status.tls.key'),
    }
  return tls
end


params = {
  'zone' => spec.az,
  'status' => {
    'enable_nontls_health_checks' => p('router.status.enable_nontls_health_checks'),
    'enable_deprecated_varz_healthz_endpoints' => p('router.status.enable_deprecated_varz_healthz_endpoints'),
    'port' => p('router.status.port'),
    'user' => p('router.status.user'),
    'pass' => p('router.status.password'),
    'routes' => {
      'port' => p('router.status.routes.port'),
    },
    'tls' => status_tls
  },
  'tracing' => {
    'enable_zipkin' => p('router.tracing.enable_zipkin'),
    'enable_w3c' => p('router.tracing.enable_w3c'),
  },
  'port' => p('router.port'),
  'index' => p('router.offset') + spec.index,
  'go_max_procs' => p('router.number_of_cpus'),
  'trace_key' => p('router.trace_key'),
  'debug_addr' => p('router.debug_address'),
  'secure_cookies' => p('router.secure_cookies'),
  'sticky_session_cookie_names' => p('router.sticky_session_cookie_names'),
  'sticky_sessions_for_auth_negotiate' => p('router.sticky_sessions_for_auth_negotiate'),
  'access_log' => {
    'file' => access_log_file,
    'enable_streaming' => p('router.enable_access_log_streaming')
  },
  'publish_start_message_interval' => '30s',
  'prune_stale_droplets_interval' => '30s',
  'droplet_stale_threshold' => '120s',
  'publish_active_apps_interval' => '0s', # 0 means disabled
  'suspend_pruning_if_nats_unavailable' => p("router.suspend_pruning_if_nats_unavailable"),
  'route_latency_metric_muzzle_duration' => '30s',
  'frontend_idle_timeout' => "#{p('router.frontend_idle_timeout')}s",
  'read_header_timeout' => "#{p('router.read_header_timeout')}s",
  'drain_wait' => "#{p('router.drain_wait')}s",
  'drain_timeout' => "#{p('router.drain_timeout')}s",
  'healthcheck_user_agent' => p('router.healthcheck_user_agent'),
  'endpoint_timeout' => "#{p('request_timeout_in_seconds')}s",
  'endpoint_dial_timeout' => "#{p('endpoint_dial_timeout_in_seconds')}s",
  'websocket_dial_timeout' => "#{p('websocket_dial_timeout_in_seconds') != "endpoint_dial_timeout_in_seconds" ? p('websocket_dial_timeout_in_seconds') : p('endpoint_dial_timeout_in_seconds')}s",
  'tls_handshake_timeout' => "#{p('tls_handshake_timeout_in_seconds')}s",
  'start_response_delay_interval' => "#{p('router.requested_route_registration_interval_in_seconds')}s",
  'load_balancer_healthy_threshold' => "#{p('router.load_balancer_healthy_threshold')}s",
  'balancing_algorithm' => validate_balancing_algorithm(p('router.balancing_algorithm')),
  'balancing_algorithm_az_preference' => validate_az_preference(p('router.balancing_algorithm_az_preference')),
  'only_trust_client_ca_certs' => p('router.only_trust_client_ca_certs'),
  'skip_ssl_validation' => p('router.ssl_skip_validation'),
  'cipher_suites' => p('router.cipher_suites'),
  'forwarded_client_cert' => p('router.forwarded_client_cert'),
  'isolation_segments' => p('router.isolation_segments'),
  'routing_table_sharding_mode' => p('router.routing_table_sharding_mode'),
  'route_services_timeout' => "#{p('router.route_services_timeout')}s",
  'route_services_secret' => p('router.route_services_secret'),
  'route_services_secret_decrypt_only' => p('router.route_services_secret_decrypt_only'),
  'route_services_recommend_https' => p('router.route_services_recommend_https'),
  'route_services_hairpinning' => p('router.route_services_internal_lookup'),
  'route_services_internal_server_port' => p('router.route_services_internal_server_port'),
  'route_services_hairpinning_allowlist' => p('router.route_services_internal_lookup_allowlist'),
  'extra_headers_to_log' => p('router.extra_headers_to_log'),
  'max_header_bytes' => validate_max_header_kb(p('router.max_header_kb')),
  'token_fetcher_max_retries' => 3,
  'token_fetcher_retry_interval' => '5s',
  'token_fetcher_expiration_buffer_time' => 30,
  'enable_proxy' => p('router.enable_proxy'),
  'force_forwarded_proto_https' => p('router.force_forwarded_proto_https'),
  'sanitize_forwarded_proto' => p('router.sanitize_forwarded_proto'),
  'hop_by_hop_headers_to_filter' => p('router.hop_by_hop_headers_to_filter'),
  'pid_file' => '/var/vcap/sys/run/gorouter/gorouter.pid',
  'ip_local_port_range' => p('router.ip_local_port_range'),
  'empty_pool_response_code_503' => p('for_backwards_compatibility_only.empty_pool_response_code_503'), # backwards compatibility only section
  'per_request_metrics_reporting' => p('router.per_request_metrics_reporting'),
  'per_app_prometheus_http_metrics_reporting' => p('router.per_app_prometheus_http_metrics_reporting'),
  'send_http_start_stop_server_event' => p('router.send_http_start_stop_server_event'),
  'send_http_start_stop_client_event' => p("router.send_http_start_stop_client_event"),
  'empty_pool_timeout' => p('for_backwards_compatibility_only.empty_pool_timeout'),
}

if_p('router.prometheus.port') do |port|
  params['prometheus'] = {
    'port' => port,
    'cert_path' => '/var/vcap/jobs/gorouter/config/certs/prometheus/prometheus.crt',
    'key_path' => '/var/vcap/jobs/gorouter/config/certs/prometheus/prometheus.key',
    'ca_path' => '/var/vcap/jobs/gorouter/config/certs/prometheus/prometheus_ca.crt'
  }
end.else do
  if p('router.per_app_prometheus_http_metrics_reporting')
    raise 'per_app_prometheus_http_metrics_reporting should not be set without configuring prometheus'
  end
end

nats = {}

nats_link_name = 'nats'
if p("nats.tls_enabled")
  nats_link_name = 'nats-tls'

  nats_ca_certs = property_or_link(
    'NATS server CA certificate',
    'nats.ca_certs',
    "nats.external.tls.ca",
    link_name=nats_link_name,
  )
  if !nats_ca_certs.is_a?(String)
    raise 'nats.ca_certs must be provided as a single string block'
  end

  nats_cert_chain = ''
  nats_private_key = ''
  if_p("nats.cert_chain") { |val| nats_cert_chain = val }
  if_p("nats.private_key") { |val| nats_private_key = val }

  if (nats_cert_chain != '') ^ (nats_private_key != '')
    raise 'nats.cert_chain and nats.private_key must be both provided or not at all'
  end
  if !nats_cert_chain.is_a?(String)
    raise 'nats.cert_chain must be provided as a single string block'
  end

  nats['tls_enabled'] = true
  nats['ca_certs'] = nats_ca_certs
  nats['cert_chain'] = nats_cert_chain
  nats['private_key'] = nats_private_key
end

nats_port = property_or_link(
  'NATS server port number',
  'nats.port',
  "nats.port",
  link_name=nats_link_name,
)

nats_machines = nil
if_p('nats.machines') do |ips|
  nats_machines = ips.compact
end.else do
  link(nats_link_name).if_p('nats.hostname') do |hostname|
    nats_machines = [hostname]
  end.else do
    nats_machines = link(nats_link_name).instances.map { |instance| instance.address }
  end
end
nats['hosts'] = nats_machines.map { |hostname| {'hostname' => hostname, 'port' => nats_port} }

nats_user = property_or_link(
  'NATS server username',
  'nats.user',
  "nats.user",
  link_name=nats_link_name,
  optional=true,
)
if nats_user
  nats['user'] = nats_user
end
nats_pass = property_or_link(
  'NATS server username',
  'nats.password',
  "nats.password",
  link_name=nats_link_name,
  optional=true,
)
if nats_pass
  nats['pass'] = nats_pass
end

params['nats'] = nats

if p("router.max_idle_connections") && p("router.max_idle_connections") > 0
  params['disable_keep_alives'] = false
  params['max_idle_conns'] = p("router.max_idle_connections")
  params['max_idle_conns_per_host'] = 100
  params['endpoint_keep_alive_probe_interval'] = p("router.keep_alive_probe_interval")
else
  params['disable_keep_alives'] = true
end
params['keep_alive_100_continue_requests'] = p('router.keep_alive_100_continue_requests')

if p('router.tracing.enable_w3c')
  params['tracing']['w3c_tenant_id'] = p('router.tracing.w3c_tenant_id') == '' ? nil : p('router.tracing.w3c_tenant_id')
end

backend_cert_chain = ''
backend_private_key = ''
if_p('router.backends.cert_chain') { |val| backend_cert_chain = val }
if_p('router.backends.private_key') { |val| backend_private_key = val }

if (backend_cert_chain != '') ^ (backend_private_key != '')
  raise 'backends.cert_chain and backends.private_key must be both provided or not at all'
end

backend_attempts = 3
if_p('router.backends.max_attempts') { |val| backend_attempts = val }

if (backend_attempts < 1 )
  raise 'router.backends.max_attempts must maintain a minimum value of 1'
end

backends = {
  'max_attempts' => backend_attempts,
  'max_conns' => p('router.backends.max_conns'),
  'cert_chain' => backend_cert_chain,
  'private_key' => backend_private_key,
}

params['backends'] = backends

route_services_cert_chain = ''
route_services_private_key = ''
if_p('router.route_services.cert_chain') { |val| route_services_cert_chain  = val }
if_p('router.route_services.private_key') { |val| route_services_private_key = val }

if (route_services_cert_chain != '') ^ (route_services_private_key != '')
  raise 'route_services.cert_chain and route_services.private_key must be both provided or not at all'
end

route_service_attempts = 3
if_p('router.route_services.max_attempts') { |val| route_service_attempts = val }

if (route_service_attempts < 1 )
  raise 'router.route_services.max_attempts must maintain a minimum value of 1'
end

strict_signature_validation = p('router.route_services_strict_signature_validation')

route_services = {
  'max_attempts' => route_service_attempts,
  'cert_chain' => route_services_cert_chain,
  'private_key' => route_services_private_key,
  'strict_signature_validation' => strict_signature_validation,
}

params['route_services'] = route_services

params['oauth'] = {
  'token_endpoint' => p('uaa.token_endpoint'),
  'client_name' => 'gorouter',
  'client_secret' => p('uaa.clients.gorouter.secret'),
  'port' => p('uaa.ssl.port'),
  'skip_ssl_validation' => p('router.ssl_skip_validation'),
}

if p('uaa.ca_cert') != ''
  params['oauth']['ca_certs'] = '/var/vcap/jobs/gorouter/config/certs/uaa/ca.crt'
end

if p('routing_api.enabled')
  ca_certs = property_or_link(
    'Routing API server CA certificate',
    'routing_api.ca_certs',
    'routing_api.mtls_ca',
  )
  private_key = property_or_link(
    'Routing API client private key',
    'routing_api.private_key',
    'routing_api.mtls_client_key',
  )
  cert_chain = property_or_link(
    'Routing API client certificate',
    'routing_api.cert_chain',
    'routing_api.mtls_client_cert',
  )

  if !ca_certs.is_a?(String)
    raise 'routing_api.ca_certs must be provided as a single string block'
  end
  if !cert_chain.is_a?(String)
    raise 'routing_api.cert_chain must be provided as a single string block'
  end

  port = property_or_link(
    'Routing API port',
    'routing_api.port',
    'routing_api.mtls_port',
  )

  params['routing_api'] = {
    'uri' => p('routing_api.uri'),
    'port' => port,
    'auth_disabled' => p('routing_api.auth_disabled'),
    'private_key' => private_key,
    'ca_certs' => ca_certs,
    'cert_chain' => cert_chain,
  }
end

if !['rfc3339', 'deprecated', 'unix-epoch'].include?(p('router.logging.format.timestamp'))
  raise "'#{p('router.logging.format.timestamp')}' is not a valid timestamp format for the property 'router.logging.format.timestamp'. Valid options are: 'rfc3339', 'deprecated', and 'unix-epoch'."
end

timestamp_format=p('router.logging.format.timestamp')
if timestamp_format == 'deprecated'
  timestamp_format = 'unix-epoch'
end

params['logging'] = {
  'syslog' => p('router.logging.syslog_tag'),
  'syslog_addr' => p('router.logging.syslog_addr'),
  'syslog_network' => p('router.logging.syslog_network'),
  'level' => p('router.logging_level'),
  'loggregator_enabled' => true,
  'metron_address' => "localhost:#{p('metron.port')}",
  'disable_log_forwarded_for' => p('router.disable_log_forwarded_for'),
  'disable_log_source_ip' => p('router.disable_log_source_ip'),
  'redact_query_params' => p('router.redact_query_parameters'),
  'enable_attempts_details' => p('router.enable_log_attempts_details'),
  'format' => {
    'timestamp' => timestamp_format,
  },
}

params['enable_ssl'] = p('router.enable_ssl')
if p('router.enable_ssl')
  p('router.tls_pem').each_with_index { |cert_pair, i|
    if !cert_pair.is_a?(Hash) || !cert_pair.key?('cert_chain') || !cert_pair.key?('private_key')
      raise 'must provide cert_chain and private_key with tls_pem'
    end

    cert = OpenSSL::X509::Certificate.new cert_pair['cert_chain']
    has_san = cert.extensions.map { |ext|
      x509ext = OpenSSL::X509::Extension.new ext
      x509ext.oid == "2.5.29.17" || x509ext.oid == "subjectAltName" # https://oidref.com/2.5.29.17
    }.reduce(:|)

    if ! has_san
      raise "tls_pem[#{i}].cert_chain must include a subjectAltName extension"
    end
  }

  params['tls_pem'] = p('router.tls_pem')

  allowed_client_cert_validations = ["none", "request", "require"]
  client_cert_validation = p("router.client_cert_validation")

  if !allowed_client_cert_validations.include?(client_cert_validation)
    raise 'router.client_cert_validation must be "none", "request", or "require"'
  end

  params['client_cert_validation'] = client_cert_validation

  allowed_min_tls_versions = ["TLSv1.0", "TLSv1.1", "TLSv1.2", "TLSv1.3"]
  min_tls_version = p("router.min_tls_version")

  if !allowed_min_tls_versions.include?(min_tls_version)
    raise 'router.min_tls_version must be "TLSv1.0", "TLSv1.1", "TLSv1.2" or "TLSv1.3"'
  end

  params['min_tls_version'] = p('router.min_tls_version')

  allowed_max_tls_versions = ['TLSv1.2', 'TLSv1.3']
  max_tls_version = p('router.max_tls_version')

  if !allowed_max_tls_versions.include?(max_tls_version)
    raise 'router.max_tls_version must be "TLSv1.2" or "TLSv1.3"'
  end

  params['max_tls_version'] = p('router.max_tls_version')
  params['ssl_port'] = p('router.tls_port')
end

params['disable_http'] = p('router.disable_http')
params['enable_http2'] = p('router.enable_http2')
params['enable_http1_concurrent_read_write'] = p('router.enable_http1_concurrent_read_write')

if p('router.ca_certs')
  params['ca_certs'] = get_valid_ca_certs
end

if p('router.client_ca_certs')
  client_ca_certs = p('router.client_ca_certs')
  if !client_ca_certs.is_a?(String)
    raise 'client_ca_certs must be provided as a single string block'
  end

  ca_certs = get_valid_ca_certs

  if p('router.only_trust_client_ca_certs') == false && ca_certs
    client_ca_certs += "\n" + ca_certs.join("\n")
  end

  params['client_ca_certs'] = client_ca_certs
end

if_p('router.http_rewrite') do |r|
  params['http_rewrite'] = r
end

if_p('router.html_error_template') do |t|
  params['html_error_template_file'] = t == '' ? nil : '/var/vcap/jobs/gorouter/config/error.html'
end

# Verification check for client certificate metadata. Only enabled if client_ca_certs
if_p('router.enable_verify_client_certificate_metadata', 'router.verify_client_certificate_metadata', 'router.client_ca_certs') do |enable, rules, client_ca_certs|
  if enable and rules.length > 0 then
    # Check consistency between client_ca_certs and rules.

    # Find pems in `client_ca_certs`, raise an error if none are defined.
    pems = client_ca_certs.scan(/(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/m)
    raise "client certificate rules defined, but no client CA defined in `client_ca_certs`" unless pems.length > 0

    field_map = {
      'common_name' => 'CN',
      'serial_number' => 'SN',
      'organization' => 'O',
      'organization_name' => 'ON',
      'locality' => 'L',
      'country' => 'C',
      'province' => 'ST',
      'street_address' => 'STREET',
    }

    # convert the issuer_in_chain of each rule to a X.509 name with its fields sorted alphabetically.
    rule_subjects = rules.map { |rule|
      fields = []
      # convert properties to X.509 DN field names. Multi-value fields create a tuple for each entry.
      rule['issuer_in_chain'].each { |k, v|
        mapping = field_map[k]
        if v.kind_of?(Array)
          v.each { |val| fields.push [ mapping, val] }
        else
          fields.push [ mapping, v ]
        end
      }

      # fields are sorted for the configuration and the subject name of the certificate.
      sorted_fields = fields.sort{|a,b|a[0] <=> b[0]}
      OpenSSL::X509::Name.new sorted_fields
    }

    # Get the client CA certificates' subject names in the same alphabetical order as from the configuration.
    cert_subjects = pems.map { |pem|
      cert = OpenSSL::X509::Certificate.new pem[0]
      sorted_fields = cert.subject.to_a.sort{|a,b|a[0] <=> b[0]}
      OpenSSL::X509::Name.new sorted_fields
    }

    # Check for each of the rules if there is _at least one_ client CA certificate with the same subject.
    # Raise an error if there isn't and show which client CA subjects _are_ configured.
    rule_subjects.each{ |rule|
      unless [rule].intersect?(cert_subjects) then
        raise <<~EOF
          no CA certificate subjects in `client_ca_certs` matches the rule's subject: #{rule}. \
          `ca_client_certs` subjects: #{cert_subjects.map { |c| c.to_s }.join(", ")}"
        EOF
      end
    }

    # now that consistency is checked, assign the values.
    params['enable_verify_client_certificate_metadata'] = enable
    params['verify_client_certificate_metadata'] = rules
  end
end

params.to_yaml[3..-1]
%>
