## {{ salt['pillar.get']('salt_managed', default='Salt Managed') }}
{%- from "haproxy/map.jinja" import haproxy,fqdn,country,location with context %}

{%- set ns = namespace(default_backend='notdefined') %}
{%- for name, values in haproxy.config.vhosts.items() %}{% if values.default_backend|default(false) %}{% set ns.default_backend = name %}{% endif %}{% endfor %}

{%- macro internal() -%}
  acl internal src -f {{ haproxy.config.dir }}/maps/access
  http-response return status 403 content-type text/html string "<h1>403 forbidden</h1>" unless internal
{%- endmacro -%}

{%- macro head() -%}
  http-request return status 200 if METH_HEAD
{%- endmacro -%}

{%- macro statusresponses() -%}
http-response return status 403 content-type text/html string "<h1>403 forbidden</h1>" if { status 403 }
http-response return status 404 content-type text/html string "<h1>404 not found</h1>" if { status 404 }
{%- endmacro -%}

{%- macro httpcheckrules(layer="layer7",inter="1s",fall=5,rise=5) -%}check observe {{ layer }} inter {{ inter }} fall {{ fall }} rise {{ rise }}{%- endmacro -%}

{%- macro httpsslrules(h2=False) -%}ssl verify none{{ " alpn h2,http/1.1" if h2 }}{%- endmacro -%}

{%- macro httpendpoints(servers=[], check=True, disabled=False) -%}
  {%- if servers -%}
  {%- for server in servers %}
  {% if "addr" in server.keys() -%}
  {%- set addr = server.addr -%}
  {%- elif server.name in haproxy.servers.keys() -%}
  {%- set addr = haproxy.servers[server.name][0] -%}
  {%- else -%}
  {%- set addr = server.name -%}
  {%- endif -%}
  server {{ server.name }} {{ addr }}:{{ server.port }} weight {{ server.weight|default(100) }}{{ " " + httpcheckrules(inter=server.inter|default("1s"), fall=server.fall|default(5), rise=server.rise|default(5)) if check }}{{ " " + httpsslrules(server.h2|default(False)) if server.ssl|default(False) }}{{ " disabled" if server.disabled|default(False) }}{{ " send-proxy" if server.proxy|default(False) }}{{ " send-proxy-v2" if server.proxyv2|default(False) }}{{ " on-marked-down shutdown-sessions on-marked-up shutdown-backup-sessions" if server.killsessions|default(False) }}{{ " backup" if server.backup|default(False) }}
  {%- endfor %}
  {%- endif -%}
{%- endmacro -%}

{%- macro tcpendpoints(servers=[], check=True) -%}
  {%- if servers -%}
  {%- for server in servers %}
  {% if "addr" in server.keys() -%}
  {%- set addr = server.addr -%}
  {%- elif server.name in haproxy.servers.keys() -%}
  {%- set addr = haproxy.servers[server.name][0] -%}
  {%- else -%}
  {%- set addr = server.name -%}
  {%- endif -%}
  server {{ server.name }} {{ addr }}:{{ server.port }} weight {{ server.weight|default(100) }}{{ " check" if check }}{{ " backup" if server.backup|default(False) }} port {{ server.port }}{{ " on-marked-down shutdown-sessions" if server.killsessions|default(False) }}
  {%- endfor %}
  {%- endif -%}
{%- endmacro -%}

{%- macro cache() -%}
  http-request cache-use static if { path -m end {{ haproxy.config.cache.file_types|join(" ") }} }
  http-response cache-store static
{%- endmacro -%}

{%- macro compression() -%}
  compression algo gzip
  compression type {{ haproxy.config.compression_mime_types|join(' ') }}
{%- endmacro -%}

{%- macro admin() -%}
# Stats Backend
backend admin from {{ haproxy.config.namespace }}
  mode http
  stats enable
  stats admin if TRUE
  stats refresh 10s
  stats show-modules
  stats show-legends
  stats uri /
{%- endmacro -%}

{%- macro api() -%}
# Runtime API
  stats socket {{ haproxy.config.api.tcpsocket }} level admin
  stats socket {{ haproxy.config.api.filesocket }} mode 666 level admin
  {%- endmacro %}

{%- macro geoip() %}
  http-request set-var(txn.country) lua.country(req.src)
  http-request return status 200 content-type "text/html; charset=utf-8" lf-string "<html><body>ip: %[var(req.src)]<br/>country: %[lua.country(req.src)]<br/>city: %[lua.city(req.src)]</body></html>" if self_host path_location
  acl allowed_country var(txn.country),map_str(/etc/haproxy/maps/countries,OK) OK
  http-response deny deny_status 451 content-type text/plain string "Denied" if !allowed_country !internal
{%- endmacro %}

# Global config
global
  master-worker
  server-state-file /var/run/haproxy.state
  mworker-max-reloads 2
  maxconn 1000
  set-var proc.country str('{{ country }}')
  set-var proc.location str('{{ location }}')

  # Lua
  tune.lua.maxmem {{ haproxy.config.lua_max_mem }}
  tune.lua.bool-sample-conversion normal
  lua-prepend-path {{ haproxy.config.dir }}/mods/?.so cpath
  lua-prepend-path {{ haproxy.config.dir }}/scripts/?.lua
{%- for script,params in haproxy.config.scripts.items() %}
  {%- if not params.lib and params.enabled|default(true) %}
  lua-load {{ haproxy.config.dir }}/{{ params.path }} {% if "args" in params.keys() %}{{ params.args|join(" ") }}{% endif %}
  {%- endif %}
{%- endfor %}

{%- if haproxy.config.api.enable %}
  {{ api() }}
{%- endif %}

  crt-base {{ haproxy.config.acme_fullchains_dir }}
  ssl-dh-param-file {{ haproxy.config.acme_dh_dir }}/dh.pem

  ssl-default-bind-ciphers {{ haproxy.config.ssl_ciphers|join(":") }}
  ssl-default-bind-options {{ haproxy.config.ssl_options|join(" ") }}
  ssl-default-server-ciphers {{ haproxy.config.ssl_ciphers|join(":") }}
  ssl-default-server-options {{ haproxy.config.ssl_options|join(" ") }}

{% if haproxy.config.quic|default(true) %}
  expose-experimental-directives
.if feature(QUIC)
  limited-quic
.endif
{% endif %}

# Defaults values
defaults {{ haproxy.config.namespace }}
{%- for key, value in haproxy.config.defaults.items() %}
  {{ key }} {{ value }}
{%- endfor %}

{% if haproxy.config.peers.hosts -%}
peers paulbsd
  bind *:{{ haproxy.config.peers.port }} ssl crt {{ haproxy.config.acme_fullchains_dir }}
  default-server ssl verify none
  server {{ salt['grains.get']('fqdn') }}
{%- for host in haproxy.config.peers.hosts %}
  server {{ host[0] }} {{ host[1] }}:{{ haproxy.config.peers.port }}
{%- endfor %}
{%- endif %}

# Errors
http-errors global
  errorfile 400 /etc/haproxy/errors/400.http
  errorfile 403 /etc/haproxy/errors/403.http
  errorfile 404 /etc/haproxy/errors/404.http
  errorfile 408 /etc/haproxy/errors/408.http
  errorfile 500 /etc/haproxy/errors/500.http
  errorfile 502 /etc/haproxy/errors/502.http
  errorfile 503 /etc/haproxy/errors/503.http
  errorfile 504 /etc/haproxy/errors/504.http

# Cache
cache static
  total-max-size {{ haproxy.config.cache.total|default(64) }}
  max-object-size {{ (haproxy.config.cache.size|default(8))*1024*1024 }}
  max-age {{ haproxy.config.cache.age|default(3600) }}

# Default HTTP frontend
frontend fe_http from {{ haproxy.config.namespace }}
  bind *:{{ haproxy.config.http_port }},:::{{ haproxy.config.http_port }} v4v6 name http
  mode http

  option httplog clf
  option http-buffer-request

  monitor-uri /dead_or_alive
  unique-id-format "%{+X}o %ci:%cp_%fi:%fp_%Ts_%rt:%pid"
{% for name, service in haproxy.config.spoe.items() %}
  filter spoe engine {{ name }} config {{ haproxy.config.dir }}/spoe.cfg
{%- endfor %}

  ## ACLs
  acl http ssl_fc,not
  acl self_host req.hdr(Host) {{ fqdn }}
  acl internal src -f {{ haproxy.config.dir }}/maps/access
  acl domains req.hdr(Host),map_dom({{ haproxy.config.dir }}/maps/domains) -m found
  acl allowhttp req.hdr(Host),map_dom({{ haproxy.config.dir }}/maps/allowhttp,false) true
  acl ua req.hdr(User-Agent),map_beg(/etc/haproxy/maps/ua) -m found
  acl security_txt path /.well-known/security.txt
  acl robots_txt path /robots.txt
  acl max_req_rate sc_http_req_rate(0) gt {{ haproxy.config.ddos.maxrequests|default(200) }}
  acl path_root path /
  acl path_info path /info
  acl path_location path /location
  acl version_http10 req.ver 1.0

  ## Basic rules
  http-request set-var(txn.srchash) src,crc32,mod(100)
  http-request set-var(txn.httpdate) date,http_date()
  http-request set-var(req.src) src
  http-request set-var(req.host) req.hdr(Host)
  http-request set-var(req.accesshash) str(),concat(,req.src,),concat(-,req.host,)

  ## Silent drop all external requests with no host header or HTTP/1.0
  http-request silent-drop if !domains !internal
  http-request silent-drop if version_http10

  ## Returns
  http-request return status 200 content-type text/plain string "User-agent: *\r\nAllow: /" if robots_txt
  http-request return status 200 content-type text/plain string "Contact: mailto:{{ haproxy.config.syscontact }}" if security_txt
  http-request return status 200 content-type "text/html; charset=utf-8" lf-string "<html><body>host: %H<br/>location: %[var(proc.location)], %[var(proc.country)]<br/>date: %[var(txn.httpdate)]<br/>src: %[var(req.src)]<br/>srchash: %[var(txn.srchash)]</body></html>" if self_host path_info

{%- if haproxy.config.http_overrides|default([]) %}
{%- for override in haproxy.config.http_overrides %}
  {{ override }}
{%- endfor %}
{%- endif %}

{%- if haproxy.config.geoip.enabled %}
  ## GeoIP
  {{ geoip() }}
{%- endif %}

  http-request redirect scheme https if http !allowhttp
  use_backend %[req.hdr(Host),lower,map({{ haproxy.config.dir }}/maps/vhosts)] if domains
  use_backend %[req.hdr(User-Agent),map_beg({{ haproxy.config.dir }}/maps/ua)] if ua
  default_backend {{ ns.default_backend }}

# Default HTTPS frontend
frontend fe_https from {{ haproxy.config.namespace }}
{%- if haproxy.config.quic|default(true) %}
.if feature(QUIC)
  bind quic4@*:{{ haproxy.config.https_port }},quic6@:::{{ haproxy.config.https_port }} v4v6 ssl crt {{ haproxy.config.acme_fullchains_dir }}{% if haproxy.config.http2 %} alpn h3,h2,http/1.1{% endif %} name https
.endif
{%- endif %}
  bind *:{{ haproxy.config.https_port }},:::{{ haproxy.config.https_port }} v4v6 ssl crt {{ haproxy.config.acme_fullchains_dir }}{% if haproxy.config.http2 %} alpn h2,http/1.1{% endif %} name https
  mode http

  option httplog clf
  option http-buffer-request

  monitor-uri /dead_or_alive
  unique-id-format "%{+X}o %ci:%cp_%fi:%fp_%Ts_%rt:%pid"
{% for name, service in haproxy.config.spoe.items() %}
  filter spoe engine {{ name }} config {{ haproxy.config.dir }}/spoe.cfg
{%- endfor %}

  ## ACLs
  acl internal src -f {{ haproxy.config.dir }}/maps/access
  acl domains req.hdr(Host),map_dom({{ haproxy.config.dir }}/maps/domains) -m found
  acl ua req.hdr(User-Agent),map_beg(/etc/haproxy/maps/ua) -m found
  acl security_txt path /.well-known/security.txt
  acl robots_txt path /robots.txt
  acl max_req_rate sc_http_req_rate(0) gt {{ haproxy.config.ddos.maxrequests|default(200) }}
  acl self_host req.hdr(Host) {{ fqdn }}
  acl path_root path /
  acl path_info path /info
  acl path_location path /location
  acl version_http10 req.ver 1.0

  ## Basic rules
  #http-request set-var(txn.random) rand,mul(5)
  http-request set-var(txn.random) uuid(4)
  http-request set-var(txn.httpdate) date,http_date()
  http-request set-var(txn.srchash) src,crc32,mod(100)
  http-request set-var(req.src) src
  http-request set-var(req.host) req.hdr(Host)
  http-request set-var(req.accesshash) str(),concat(,req.src,),concat(-,req.host,)

  http-request track-sc0 var(req.accesshash) table per_ip_rates

  http-request capture req.hdr(User-Agent) len 200
  http-request capture req.hdr(Content-Type) len 50
  http-request capture sc_http_req_rate(0) len 4

  ## Silent drop all external requests with no host header
  http-request silent-drop if !domains !internal
  http-request silent-drop if version_http10

  ## DDoS
  http-request deny deny_status 429 if max_req_rate !internal

{%- if haproxy.config.geoip.enabled %}
  ## GeoIP
  {{ geoip() }}
{%- endif %}

  ## Returns
  http-request return status 200 content-type text/plain string "User-agent: *\r\nAllow: /" if robots_txt
  http-request return status 200 content-type text/plain lf-file /etc/haproxy/static/security.txt if security_txt
  http-request return status 200 content-type "text/html; charset=utf-8" lf-string "<html><body>host: %H<br/>location: %[var(proc.location)], %[var(proc.country)]<br/>date: %[var(txn.httpdate)]<br/>src: %[var(req.src)]<br/>srchash: %[var(txn.srchash)]</body></html>" if self_host path_info

  ## Headers
  http-request set-header X-Proxy-Id "%H"
  http-request set-header X-Proto https if { ssl_fc }

  http-response set-header Date "%[var(txn.httpdate)]"
  http-response set-header Server "{{ haproxy.config.servername }}"
  http-response set-header X-Random "%[var(txn.random)]"
  http-response set-header X-Robots-Tag "noindex,nofollow"
  http-request set-header X-SSL-JA3 %[ssl_fc_protocol_hello_id],%[ssl_fc_cipherlist_bin(1),be2dec(-,2)],%[ssl_fc_extlist_bin(1),be2dec(-,2)],%[ssl_fc_eclist_bin(1),be2dec(-,2)],%[ssl_fc_ecformats_bin,be2dec(-,1)]
{% if haproxy.config.quic|default(true) %}
.if feature(QUIC)
  http-response set-header Alt-Svc "h3=\":443\"; ma=3600"
.endif
{% endif %}

  http-request redirect location %[req.hdr(Host),map_dom({{ haproxy.config.dir }}/maps/redirects)] code 301 if { req.hdr(Host),map_dom({{ haproxy.config.dir }}/maps/redirects) -m found }
  http-request deny deny_status 404 unless domains || ua

{%- if haproxy.config.https_overrides|default([]) %}
{%- for override in haproxy.config.https_overrides %}
  {{ override }}
{%- endfor %}
{%- endif %}

{%- if haproxy.config.admin %}
  use_backend admin if self_host internal
{%- endif %}
  use_backend %[req.hdr(Host),lower,map({{ haproxy.config.dir }}/maps/vhosts)] if domains
  use_backend %[req.hdr(User-Agent),map_beg({{ haproxy.config.dir }}/maps/ua)] if ua
  default_backend {{ ns.default_backend }}


# HTTP Backends
{%- if haproxy.config.vhosts %}
{%- for name, values in haproxy.config.vhosts.items() %}
{%- if not values.redirect|default(False) %}
backend {{ name }} from {{ haproxy.config.namespace }}
  balance {{ values.balance|default(haproxy.config.balance) }}
  mode http

  option forwardfor

{%- if values.check|default(haproxy.config.check) %}
  option httpchk
{%- for step in values.check_steps|default([]) %}
  http-check {{ step }}
{%- endfor %}
{%- endif %}

{%- if values.usecache|default(True) %}
  {{ cache() }}
{%- endif %}

{%- if values.overrides|default([]) %}
{%- for override in values.overrides %}
  {{ override }}
{%- endfor %}
{%- endif %}
{%- if values.head|default(False) %}
  {{ head() }}
{%- endif %}

{%- if values.compression|default(True) %}
  {{ compression() }}
{%- endif %}
  default-server maxconn 100 weight 100 inter 1s fall 5 agent-check agent-port 7265 agent-send "{{ name }}\n"

{%- if values.internal|default(False) %}
  {{ internal() }}
{%- endif %}
  {{- httpendpoints(servers=values.servers|default([]), check=values.check|default(haproxy.config.check)) }}
{%- endif %}
{% endfor %}
{%- endif %}

# TCP services
{%- if haproxy.config.services %}
{%- for name, values in haproxy.config.services.items() %}
listen {{ name }} from {{ haproxy.config.namespace }}
  bind *:{{ values.port }},:::{{ values.port }} v4v6 name {{ name }}
  mode tcp

  option tcplog
  option tcpka

  stick-table type ip size 10k peers paulbsd
  stick on dst
{%- if values.type == "postgres" %}
  option pgsql-check user repmgr
{%- endif %}
{%- if values.type == "spoe" %}
  timeout connect 1s
  timeout server 1s
  option spop-check
{%- endif %}
  default-server maxconn 100 weight 100 inter 1s fall 5 agent-check agent-port 7265 agent-send "{{ name }}\n"
  {{- tcpendpoints(servers=values.servers|default([]), check=values.check|default(haproxy.config.check)) }}
{% endfor %}
{%- endif %}

# SPOE Agents
{%- for name, values in haproxy.config.spoe.items() %}
backend {{ name }} from {{ haproxy.config.namespace }}
  mode tcp

  timeout connect 5s
  timeout server 3m

  option spop-check
  option tcpka

  default-server maxconn 100 weight 100 inter 1s fall 5 agent-check agent-port 7265 agent-send "{{ name }}\n"
  {{- tcpendpoints(servers=values.servers, check=values.check|default(haproxy.config.check)) }}
{% endfor %}

{%- if haproxy.config.admin %}
{{ admin() if haproxy.config.admin }}
{%- endif %}

# Per IP rates stick table
backend per_ip_rates from {{ haproxy.config.namespace }}
  stick-table type string size {{ haproxy.config.ddos.size|default("1m") }} expire {{ haproxy.config.ddos.timeperiod|default("10s") }} store http_req_rate({{ haproxy.config.ddos.timeperiod|default("10s")}}) {{ "peers paulbsd" if haproxy.config.peers.hosts }}