# frozen_string_literal: true require "faraday" require "cgi" module Streams module DnsProviders # Hetzner Cloud DNS (Console) — zone mltv-stream.net. # # ENV: # HCLOUD_TOKEN (stesso del Cloud) # STREAM_DNS_ZONE (default mltv-stream.net) # STREAM_DNS_TTL (default 60) class Hetzner < Base # Trailing slash obbligatorio: path assoluti altrimenti droppano /v1. API = "https://api.hetzner.cloud/v1/" def initialize(token: ENV.fetch("HCLOUD_TOKEN"), zone: nil, conn: nil) @token = token @zone = zone || ENV.fetch("STREAM_DNS_ZONE", "mltv-stream.net") @ttl = ENV.fetch("STREAM_DNS_TTL", "60").to_i @conn = conn end def upsert_a(name, ip) rr_name = relative_name(name) delete_a(name) post("zones/#{CGI.escape(@zone)}/rrsets", { name: rr_name, type: "A", ttl: @ttl, records: [{ value: ip.to_s, comment: "matchlivetv stream-node" }], labels: { "matchlivetv" => "true", "role" => "stream-node" } }) true end def delete_a(name) rr_name = relative_name(name) encoded = CGI.escape(rr_name) response = conn.delete("zones/#{CGI.escape(@zone)}/rrsets/#{encoded}/A") do |req| req.headers.update(auth_headers) end return true if response.status == 404 || response.status == 204 || response.success? raise Error, "Hetzner DNS API #{response.status}: #{response.body.inspect}" end def resolve(name) rr_name = relative_name(name) data = get("zones/#{CGI.escape(@zone)}/rrsets", name: rr_name, type: "A") rrset = Array(data["rrsets"]).first Array(rrset&.dig("records")).first&.dig("value") rescue Error nil end private def relative_name(name) host = name.to_s.strip.downcase.delete_suffix(".") suffix = ".#{@zone}" return "@" if host == @zone return host.delete_suffix(suffix) if host.end_with?(suffix) host end def conn @conn ||= Faraday.new(url: API) do |f| f.request :json f.response :json, content_type: /\bjson$/ f.adapter Faraday.default_adapter end end def auth_headers { "Authorization" => "Bearer #{@token}" } end def get(path, params = {}) response = conn.get(path) do |req| req.headers.update(auth_headers) req.params.update(params) end unwrap!(response) end def post(path, body) response = conn.post(path) do |req| req.headers.update(auth_headers) req.body = body end unwrap!(response) end def unwrap!(response) unless response.success? raise Error, "Hetzner DNS API #{response.status}: #{response.body.inspect}" end response.body.is_a?(Hash) ? response.body : {} end end end end