require "uri" require "http" require "system/user" module PQ struct ConnInfo SOCKET_SEARCH = %w(/run/postgresql/.s.PGSQL.5432 /tmp/.s.PGSQL.5432 /var/run/postgresql/.s.PGSQL.5432) SUPPORTED_AUTH_METHODS = %w[cleartext md5 scram-sha-256 scram-sha-256-plus] # The host. If starts with a / it is assumed to be a local Unix socket. getter host : String # The port, defaults to 5432. It is ignored for local Unix sockets. getter port : Int32 # The database name. getter database : String # The user. getter user : String # The password. Optional. getter password : String? # The sslmode. Optional (:prefer is default). getter sslmode : Symbol # The sslcert. Optional. getter sslcert : String? # The sslkey. Optional. getter sslkey : String? # The sslrootcert. Optional. getter sslrootcert : String? # The application name. Optional (defaults to "crystal"). getter application_name : String getter auth_methods : Array(String) = %w[scram-sha-256-plus scram-sha-256 md5] # Create a new ConnInfo from all parts def initialize(host : String? = nil, database : String? = nil, user : String? = nil, password : String? = nil, port : Int | String? = nil, sslmode : String | Symbol? = nil, application_name : String? = nil) @host = default_host host db = default_database database @database = db.lchop('/') @user = default_user user @port = (port || ENV.fetch("PGPORT", "5432")).to_i @sslmode = default_sslmode sslmode @password = password || ENV.fetch("PGPASSWORD", PgPass.locate(@host, @port, @database, @user)) @application_name = default_application_name application_name end # Initialize with either "postgres://" urls or postgres "key=value" pairs def self.from_conninfo_string(conninfo : String) if conninfo.starts_with?("postgres://") || conninfo.starts_with?("postgresql://") new(URI.parse(conninfo)) else return new if conninfo == "" args = Hash(String, String).new conninfo.split ' ' do |pair| begin k, eq, v = pair.partition('=') if eq.empty? raise ArgumentError.new("invalid paramater: #{pair}") end args[k] = v end end new(args) end end # Initializes with a `URI` def initialize(uri : URI) params = URI::Params.parse(uri.query.to_s) hostname = uri.hostname.presence || params.fetch("host", "") initialize(hostname, uri.path, uri.user, uri.password, uri.port, :prefer, params.fetch("application_name", nil)) if q = uri.query HTTP::Params.parse(q) do |key, value| handle_sslparam(key, value) end end end # Initialize with a `Hash` # # Valid keys match Postgres "conninfo" keys and are `"host"`, `"dbname"`, # `"user"`, `"password"`, `"port"`, `"sslmode"`, `"sslcert"`, `"sslkey"`, # `"sslrootcert"` and `"application_name"`. def initialize(params : Hash) initialize(params["host"]?, params["dbname"]?, params["user"]?, params["password"]?, params["port"]?, params["sslmode"]?, params["application_name"]?) params.each do |key, value| handle_sslparam(key, value) end end private def handle_sslparam(key : String, value : String) case key when "sslmode" @sslmode = default_sslmode value when "sslcert" @sslcert = value when "sslkey" @sslkey = value when "sslrootcert" @sslrootcert = value when "auth_methods" methods = value.split(",").compact_map(&.underscore.presence).uniq methods.each do |method| unless method.in?(SUPPORTED_AUTH_METHODS) raise "invalid auth_method #{method}" end end @auth_methods = methods else # ignore end end private def default_host(h) return h if h && !h.blank? if pghost = ENV["PGHOST"]? return pghost[0] == '/' ? "#{pghost}/.s.PGSQL.5432" : pghost end SOCKET_SEARCH.each do |s| return s if File.exists?(s) end "localhost" end private def default_database(db) if db && db != "/" && !db.empty? db else ENV.fetch("PGDATABASE", current_user_name) end end private def default_application_name(application_name, fallback_application_name = "crystal") application_name || ENV.fetch("PGAPPNAME", nil) || fallback_application_name end private def default_user(u) u || ENV.fetch("PGUSER", current_user_name) end private def default_sslmode(mode) case mode when nil, :prefer, "prefer" :prefer when :disable, "disable" :disable when :allow, "allow" :allow when :require, "require" :require when :"verify-ca", "verify-ca" :"verify-ca" when :"verify-full", "verify-full" :"verify-full" else raise ArgumentError.new("sslmode #{mode} not supported") end end private def current_user_name {% if flag?(:windows) %} # NOTE: actually getting the current username on windows would be better # https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getusernamew "postgres" {% else %} System::User.find_by(id: LibC.getuid.to_s).username {% end %} end end end