# encoding: utf-8

# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File:	modules/IP.ycp
# Module:	yast2
# Summary:	IP manipulation routines
# Authors:	Michal Svec <msvec@suse.cz>
# Flags:	Stable
#
# $Id$
require "yast"
require "resolv"
require "ipaddr"

module Yast
  class IPClass < Module
    include Yast::Logger

    def main
      textdomain "base"

      Yast.import "Netmask"

      @ValidChars = "0123456789abcdefABCDEF.:"
      @ValidChars4 = "0123456789."
      @ValidChars6 = "0123456789abcdefABCDEF:"

      # helper list, each bit has its decimal representation
      @bit_weight_row = [128, 64, 32, 16, 8, 4, 2, 1]
    end

    # Describe a valid IPv4 address
    # @return [String] describtion a valid IPv4 address
    def Valid4
      # Translators: dot: "."
      _(
        "A valid IPv4 address consists of four integers\nin the range 0-255 separated by dots."
      )
    end

    # Check syntax of IPv4 address
    # @param [String] ip IPv4 address
    # @return true if correct
    def Check4(ip)
      IPAddr.new(ip).ipv4?
    rescue
      false
    end

    # Describe a valid IPv6 address
    # @return [String] describtion a valid IPv4 address
    def Valid6
      # Translators: colon: ":"
      _(
        "A valid IPv6 address consists of up to eight\n" \
          "hexadecimal numbers in the range 0 - FFFF separated by colons.\n" \
          "It can contain up to one double colon."
      )
    end

    # Check syntax of IPv6 address
    # @param [String] ip IPv6 address
    # @return true if correct
    def Check6(ip)
      IPAddr.new(ip).ipv6?
    rescue
      false
    end

    # If param contains IPv6 in one of its various forms, extracts it.
    #
    # if ip is closed in [ ] or contain % then it can be special case of IPv6 syntax,
    # so extract ipv6 (see description later) and continue with check.
    #
    # IPv6 syntax:
    # - pure ipv6 blob (e.g. f008::1)
    # - ipv6 blob with link local suffix (e.g. f008::1%eth0)
    # - dtto in square brackets (e.g. [f008::1%eth0] )
    #
    # @param [String] ip    a buffer with address
    # @return      IPv6 part of ip param, unchanged ip param otherwise
    def UndecorateIPv6(ip)
      if Builtins.regexpmatch(ip, "^\\[.*\\]") ||
          Builtins.regexpmatch(ip, "^[^][%]+(%[^][%]+){0,1}$")
        ip = Builtins.regexpsub(
          ip,
          "^\\[?([^][%]+)(%[^][%]+){0,1}(\\]|$)",
          "\\1"
        )
      end

      ip
    end

    # Check syntax of IP address
    # @param [String] ip IP address
    # @return true if correct
    def Check(ip)
      Check4(ip) || Check6(ip)
    end

    # Returns string of valid network definition.
    # Both IPv4 and IPv6.
    #
    # @return [String] describing the valid network.
    def ValidNetwork
      # TRANSLATORS: description of the valid network definition
      _(
        "A valid network definition can contain the IP,\n" \
          "IP/Netmask, IP/Netmask_Bits, or 0/0 for all networks.\n" \
          "\n" \
          "Examples:\n" \
          "IP: 192.168.0.1 or 2001:db8:0::1\n" \
          "IP/Netmask: 192.168.0.0/255.255.255.0 or 2001:db8:0::1/56\n" \
          "IP/Netmask_Bits: 192.168.0.0/24 or 192.168.0.1/32 or 2001:db8:0::1/ffff::0\n"
      )
    end

    # Convert IPv4 address from string to integer
    # @param [String] ip IPv4 address
    # @return ip address as integer
    def ToInteger(ip)
      return nil unless Check4(ip)
      IPAddr.new(ip).to_i
    end

    # Convert IPv4 address from integer to string
    # @param [Fixnum] ip IPv4 address
    # @return ip address as string
    def ToString(ip)
      IPAddr.new(ip, Socket::AF_INET).to_s
    end

    # Converts IPv4 address from string to hex format
    # @param [String] ip IPv4 address as string in "ipv4" format
    # @return [String] representing IP in Hex
    # @example IP::ToHex("192.168.1.1") -> "C0A80101"
    # @example IP::ToHex("10.10.0.1") -> "0A0A0001"
    def ToHex(ip)
      int = ToInteger(ip)
      return nil unless int
      format("%08X", int)
    end

    # Compute IPv4 network address from ip4 address and network mask.
    # @param [String] ip IPv4 address
    # @param [String] mask netmask
    # @return computed subnet
    def ComputeNetwork(ip, mask)
      i = ToInteger(ip)
      m = ToInteger(mask)
      ToString(Ops.bitwise_and(Ops.bitwise_and(i, m), 4_294_967_295))
    end

    # Compute IPv4 broadcast address from ip4 address and network mask.
    #
    # The broadcast address is the highest address of network address range.
    # @param [String] ip IPv4 address
    # @param [String] mask netmask
    # @return computed broadcast
    def ComputeBroadcast(ip, mask)
      i = ToInteger(ip)
      m = ToInteger(mask)
      ToString(
        Ops.bitwise_and(Ops.bitwise_or(i, Ops.bitwise_not(m)), 4_294_967_295)
      )
    end

    # Converts IPv4 into its 32 bit binary representation.
    #
    # @param [String] ipv4
    # @return [String] binary
    #
    # @see #BitsToIPv4()
    #
    # @example
    #     IPv4ToBits("80.25.135.2")    -> "01010000000110011000011100000010"
    #     IPv4ToBits("172.24.233.211") -> "10101100000110001110100111010011"
    def IPv4ToBits(ipv4)
      int = ToInteger(ipv4)
      return nil unless int
      format("%032b", int)
    end

    # Converts 32 bit binary number to its IPv4 representation.
    #
    # @param string binary
    # @return [String] ipv4
    #
    # @see #IPv4ToBits()
    #
    # @example
    #     BitsToIPv4("10111100000110001110001100000101") -> "188.24.227.5"
    #     BitsToIPv4("00110101000110001110001001100101") -> "53.24.226.101"
    def BitsToIPv4(bits)
      return nil unless /\A[01]{32}\z/ =~ bits
      ToString(bits.to_i(2))
    end

    def CheckNetworkShared(network)
      return false if network.nil? || network == ""

      # all networks
      network == "0/0" ? true : nil
    end

    # Checks the given IPv4 network entry.
    #
    # @see CheckNetwork for details.
    # @see CheckNetwork6 for IPv6 version of the same function.
    #
    # @example
    #   CheckNetwork("192.168.0.0/255.255.255.0") -> true
    #   CheckNetwork("192.168.1.22") -> true
    #   CheckNetwork("172.55.0.0/33") -> false
    def CheckNetwork4(network)
      generic_check = CheckNetworkShared(network)
      return generic_check unless generic_check.nil?

      # 192.168.0.0/20, 0.8.55/158
      if network =~ Regexp.new("^[" + @ValidChars4 + "]+/[0-9]+$")
        net_parts = network.split("/")
        return Check4(net_parts[0]) &&
            Netmask.CheckPrefix4(net_parts[1])
      # 192.168.0.0/255.255.255.0, 0.8.55/10.258.12
      elsif network =~ Regexp.new("^[" + @ValidChars4 + "]+/[" + @ValidChars4 + "]+$")
        net_parts = network.split("/")
        return Check4(net_parts[0]) &&
            Netmask.Check4(net_parts[1])
      # 192.168.0.1, 0.8.55.999
      elsif Check4(network)
        return true
      end

      false
    end

    # Checks the given IPv6 network entry.
    #
    # @see CheckNetwork for details.
    # @see CheckNetwork4 for IPv4 version of the same function.
    #
    # @example
    #   CheckNetwork("2001:db8:0::1/64") -> true
    #   CheckNetwork("2001:db8:0::1") -> true
    #   CheckNetwork("::1/257") -> false
    def CheckNetwork6(network)
      generic_check = CheckNetworkShared(network)
      return generic_check unless generic_check.nil?

      # 2001:db8:0::1/64
      if network =~ Regexp.new("^[" + @ValidChars6 + "]+/[" + Netmask.ValidChars6 + "]*$")
        net_parts = network.split("/")
        return Check6(net_parts[0]) &&
            Netmask.Check6(net_parts[1])
      # 2001:db8:0::1/ffff:ffff::0
      elsif network =~ Regexp.new("^[" + @ValidChars6 + "]+/[" + @ValidChars6 + "]+$")
        net_parts = network.split("/")
        return Check6(net_parts[0]) &&
            Check6(net_parts[1])
      # 2001:db8:0::1
      elsif Check6(network)
        return true
      end

      false
    end

    # Checks the given network entry which can be defined in several formats:
    #   - Single IPv4 or IPv6, e.g., 192.168.0.1 or 2001:db8:0::1
    #   - IP/Netmask, e.g., 192.168.0.0/255.255.255.0 or 2001:db8:0::1/ffff:ffff::0
    #   - IP/CIDR, e.g., 192.168.0.0/20 or 2001:db8:0::1/56
    #
    # @example
    #  CheckNetwork("192.168.0.1") -> true
    #  CheckNetwork("192.168.0.0/20") -> true
    #  CheckNetwork("192.168.0.0/255.255.255.0") -> true
    #  CheckNetwork("0/0") -> true
    #  CheckNetwork("::1/128") -> true
    #  CheckNetwork("2001:db8:0::1") -> true
    #  CheckNetwork("2001:db8:0::1/64") -> true
    #  CheckNetwork("2001:db8:0::1/ffff:ffff::0") -> true
    #  CheckNetwork("2001:db8:0::xyz") -> false
    #  CheckNetwork("::1/257") -> false
    #  CheckNetwork("172.55.0.0/33") -> false
    #  CheckNetwork("172.55.0.0/125.85.5.5") -> false
    def CheckNetwork(network)
      CheckNetwork4(network) || CheckNetwork6(network)
    end

    # Checks if given IPv4 address is reserved by any related RFC.
    #
    # RFCs covered by this method are #1700, #1918, #2544, #3068, #5735, #5737,
    # 5771, #6333 and #6598
    #
    # @param[String] ip IPv4 address
    # @return[true,false] if address is reserved
    #
    # @raise [RuntimeError] if ip address is invalid
    def reserved4(ip)
      raise "Invalid IP address passed '#{ip}'" unless Check4(ip)

      # RFC#1700
      return true if ip.start_with?("0.")

      # RFC#6598
      return true if private_carrier_grade_nat?(ip)

      # RFC#5735
      return true if ip.start_with?("127.")
      return true if ip.start_with?("169.254")

      # RFC#1918
      return true if private_network?(ip)

      # RFC#6333
      return true if ds_lite_address?(ip)

      # RFC#5737
      return true if ip.start_with?("192.0.2.")
      return true if ip.start_with?("198.51.100.")
      return true if ip.start_with?("203.0.113.")

      # RFC#3068
      return true if ip.start_with?("192.88.99.")

      # RFC#2544
      return true if ip.start_with?("192.18.")
      return true if ip.start_with?("192.19.")

      # all from 224. is covered by RFC#5771 and RFC#5735
      return true if (224..255).cover?(ip.split(".").first.to_i)

      false
    end

    publish variable: :ValidChars, type: "string"
    publish variable: :ValidChars4, type: "string"
    publish variable: :ValidChars6, type: "string"
    publish function: :Valid4, type: "string ()"
    publish function: :Check4, type: "boolean (string)"
    publish function: :Valid6, type: "string ()"
    publish function: :Check6, type: "boolean (string)"
    publish function: :UndecorateIPv6, type: "string (string)"
    publish function: :Check, type: "boolean (string)"
    publish function: :ValidNetwork, type: "string ()"
    publish function: :ToInteger, type: "integer (string)"
    publish function: :ToString, type: "string (integer)"
    publish function: :ToHex, type: "string (string)"
    publish function: :ComputeNetwork, type: "string (string, string)"
    publish function: :ComputeBroadcast, type: "string (string, string)"
    publish function: :IPv4ToBits, type: "string (string)"
    publish function: :BitsToIPv4, type: "string (string)"
    publish function: :CheckNetwork4, type: "boolean (string)"
    publish function: :CheckNetwork6, type: "boolean (string)"
    publish function: :CheckNetwork, type: "boolean (string)"
    publish function: :reserved4, type: "boolean (string)"

  private

    def private_carrier_grade_nat?(ip)
      return false unless ip.start_with?("100.")

      second_part = ip.split(".")[1].to_i
      (64..127).cover?(second_part)
    end

    def private_network?(ip)
      # 10.0.0.0/8
      return true if ip.start_with?("10.")

      # 192.168.0.0/8
      return true if ip.start_with?("192.168.")

      # 172.16.0.0/12
      return false unless ip.start_with?("172.")

      second_part = ip.split(".")[1].to_i
      (16..31).cover?(second_part)
    end

    def ds_lite_address?(ip)
      return false unless ip.start_with?("192.0.0.")

      fourth_part = ip.split(".")[3].to_i
      (0..7).cover?(fourth_part)
    end
  end

  IP = IPClass.new
  IP.main
end
