dev.core.dns.psm1

Import-Module "$PSScriptRoot\dev.core.utils.psm1" -DisableNameChecking;

$hostsFile = "${env:SystemRoot}\system32\drivers\etc\hosts";

[regex]$hostEntryRegex = "(?<ip>[\d\.]+)\s+(?<host>[\w\d\.\-_]+)";

#
# Private functions
#
function Update-HostsFile
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][object[]]$Value,
        [Parameter(Mandatory=$true, ParameterSetName="Update")][switch]$Update, 
        [Parameter(Mandatory=$true, ParameterSetName="Append")][switch]$Append);

    $backupHostsFile = "${env:SystemRoot}\system32\drivers\etc\hosts.$(New-Guid).bak";
            
    # Backup the hosts file before making any change
    Write-Verbose "Copying hosts file to ${backupHostsFile}...";
    Copy-Item -Path $script:hostsFile -Destination $backupHostsFile;

    $error = "The ${script:hostsFile} file is not updated successfully, please restore hosts file using back file ${backupHostsFile}.";

    try
    {
        if ($PSCmdlet.ParameterSetName -eq "Update")
        {
            $expected = $Value;
            Write-Verbose "Updating hosts file to ${script:hostsFile}...";
            Set-Content -Path $script:hostsFile -Value $Value;            
        }
        else # Append
        {
            $expected = (Get-Content -Path $script:hostsFile) + $Value;
            Write-Verbose "Appending hosts file to ${script:hostsFile}...";
            Add-Content -Path $script:hostsFile -Value $appendLines;
        }

        Write-Verbose "Waiting 1 second for the change to be picked up by windows";
        Start-Sleep -Seconds 1;
    }
    catch
    {
        Write-Error $_;
        throw $error;
    }

    # Read content from hosts again to make sure the change is successful.
    # Here do not try to restore the hosts file as most likely the restore will fail again.
    $newValue =  Get-Content -Path $script:hostsFile;
    if (($expected -join "`r`n") -ne ($newValue -join "`r`n"))
    {
        Write-Error "The new hosts file doesn't match expected content";
        throw $error;
    }
    else
    {
        # If the new content matches expected content, then delete the backup file.
        Write-Verbose "Deleting backup file ${backupHostsFile}...";
        Remove-Item -Path $backupHostsFile;
    }
}


#
# Public functions
#

function New-LocalDnsEntry
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][string]$HostName, 
        [Parameter(Mandatory=$true)][string]$IPAddress);

    $entry = [PSCustomObject]@{ 
        HostName = $HostName; 
        IPAddress = $IPAddress 
    };
    
    Write-Output $entry;
}

function Get-LocalDnsEntry
{
    [CmdletBinding()]
    param([string]$HostName);

    $lines = Get-Content -Path $script:hostsFile;
    
    for ($i = 0; $i -lt $lines.Length; $i++)
    {
        $line = $lines[$i];

        if ($line.Trim().StartsWith("#"))
        {
            continue;
        }

        $match = $hostEntryRegex.Match($line);
        if ($match.Success)
        {
            $capturedHost = $match.Groups["host"].value;
            $capturedIp = $match.Groups["ip"].Value;

            if (!$HostName -or $capturedHost -eq $HostName)
            {
                New-LocalDnsEntry -HostName $capturedHost -IPAddress $capturedIp;
            }
        }
    }
}

function Set-LocalDnsEntry
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)][string]$HostName, 
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)][string]$IPAddress);

    begin
    {
        if (!(Test-RunAsAdmin))
        {
            throw "The Set-LocalDnsEntry command must be executed with administrator privilege."
        }

        $lines = Get-Content -Path $script:hostsFile;

        $updateLines = $lines;
        $appendLines = @();

        $shouldUpdate = $false;
        $shouldAppend = $false;
    }

    process
    {
        $hostEntry = "${IPAddress}`t${HostName}";
        $found = $false;

        for ($i = 0; $i -lt $updateLines.Length; $i++)
        {
            $line = $updateLines[$i];

            if ($line.Trim().StartsWith("#"))
            {
                continue;
            }

            $match = $hostEntryRegex.Match($line);
            if ($match.Success)
            {
                $capturedHost = $match.Groups["host"].value;
                $capturedIP = $match.Groups["ip"].value;
                if ($capturedHost -eq $HostName)
                {
                    if ($capturedIP -ne $IPAddress)
                    {
                        Write-Verbose "Updating DNS entry from `"$line`" to `"$hostEntry`"...";
                       
                        $updateLines[$i] = $hostEntry;
                        $shouldUpdate = $true;
                    }
                    
                    $found = $true;
                }
            }
        }
        
        if (!$found)
        {
            Write-Verbose "Appending DNS entry `"$hostEntry`"...";
            $appendLines += $hostEntry;
            $shouldAppend = $true;
        }
    }

    end
    {
        if ($shouldUpdate)
        {
            Update-HostsFile -Value ($updateLines + $appendLines) -Update;
        }
        elseif ($shouldAppend)
        {
            Update-HostsFile -Value $appendLines -Append;
        }
        else
        {
            Write-Warning "Skip updating ${script:hostsFile} file as there is no change detected";
        }
    }
}

function Remove-LocalDnsEntry
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param([Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)][string]$HostName);

    begin
    {
    Confirm-RunAsAdmin -Command $MyInvocation.MyCommand;

        $lines = Get-Content -Path $script:hostsFile;
        $shouldUpdate = $false;     
    }

    process
    {
        for ($i = 0; $i -lt $lines.Length; $i++)
        {
            $line = $lines[$i];

            if ($line -ne $null -and !$line.Trim().StartsWith("#"))
            {
                $match = $hostEntryRegex.Match($line);
                if ($match.Success)
                {
                    $capturedHost = $match.Groups["host"].value;
                    if ($capturedHost -eq $HostName)
                    {
                        Write-Verbose "Removing DNS entry from `"$line`"...";
                        $lines[$i] = $null; # Set the line to $null for deletion.
                        $shouldUpdate = $true;
                    }
                }
            }
        }
    }

    end
    {
        if ($shouldUpdate)
        {
            Update-HostsFile -Value ($lines | ? { $_ -ne $null }) -Update;
        }
        else
        {
            Write-Warning "Skip updating ${script:hostsFile} file as there is no change detected";
        }
    }
}

#
# Module Initialization
#

Export-ModuleMember -Function New-LocalDnsEntry;
Export-ModuleMember -Function Get-LocalDnsEntry;
Export-ModuleMember -Function Set-LocalDnsEntry;
Export-ModuleMember -Function Remove-LocalDnsEntry;