1module MCollective
2  module Util
3    module PuppetServerAddressValidation
4
5      class Host
6        require 'ipaddr'
7        attr_reader :name, :port
8
9        INT_REGEX         = /\A\d+\Z/
10        PORT_REGEX        = /\A\d+\Z/
11        IPV6_FORMAT_REGEX = /\A\[[a-fA-F0-9\:\.]*\](\:\d*)?/
12        NAME_REGEX        = /\A(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])\Z/
13
14        def initialize(server)
15          @server = server
16          @name, @port = parse_name_and_port
17        end
18
19        private
20
21        def parse_name_and_port
22          name, port = nil
23          if ipv6_server?
24            # IPv6 address, without a port
25            name = @server
26          elsif @server =~ IPV6_FORMAT_REGEX
27            # IPv6 url "[<ipv6_address>]:<port>", as in rfc2732
28            name, port = @server[1..@server.size].split(']')
29            if port
30              # Remove the initial colon
31              port = port[1..port.size]
32            end
33          else
34            # Parse the port only if we have a single colon,
35            # to avoid splitting invalid IPv6 addresses
36            if @server && @server.count(':') == 1
37              name, port = @server.split(':')
38            else
39              name = @server
40            end
41          end
42          return name, port
43        end
44
45        def process_ip (addr)
46          # NB: an int is a valid IPv6 addr for the ipaddr lib on ruby 1.8.7!
47          unless addr =~ INT_REGEX
48            begin
49              IPAddr.new(addr)
50            rescue
51            end
52          end
53        end
54
55        public
56
57        def valid_text_name?
58          @name =~ NAME_REGEX
59        end
60
61        def ipv6_server?
62          ip = process_ip(@server)
63          ip && ip.ipv6?
64        end
65
66        def valid_ip_name?
67          ip = process_ip(@name)
68          ip && (ip.ipv4? || ip.ipv6?)
69        end
70
71        def valid_port?
72          @port =~ PORT_REGEX
73        end
74      end
75
76
77      def self.validate_server(server)
78
79        host = Host.new(server)
80
81        if host.name && !(host.valid_ip_name? || host.valid_text_name?)
82          raise "The hostname '%s' is not a valid hostname" % host.name
83        end
84
85        if host.port && !host.valid_port?
86          raise "The port '%s' is not a valid puppet master port" % host.port
87        end
88      end
89
90      def self.parse_name_and_port_of(server)
91        host = Host.new(server)
92        return host.name, host.port
93      end
94
95    end
96  end
97end
98