Skip to content

Fix the PowerShell script (via Prompt Engineering) #17

@coolaj86

Description

@coolaj86

Problem

We only have this for POSIX systems. It would be great to have for Windows

https://github.com/BeyondCodeBootcamp/DuckDNS.sh/blob/main/duckdns.sh

Who can solve it?

Anyone with a Windows computer and scripting or programming experience.

Solution

Get GPT to translate and check the results function-by-function as duckdns.ps1

Here's what GPT came up with so far. It's not correct, but it gets a lot of the boilerplate out of the way. Or at least it feels like it does.

$ErrorActionPreference = "Stop"

# DuckDNS API docs: https://duckdns.org/spec.jsp

$DUCKDNS_SH_COMMAND = $Env:DUCKDNS_SH_COMMAND
if (-not $DUCKDNS_SH_COMMAND) {
    $DUCKDNS_SH_COMMAND = $MyInvocation.MyCommand.Path
}

$DUCKDNS_SH_SUBCOMMAND = $args[0]
$DUCKDNS_SH_SUBDOMAIN = $args[1]
$DUCKDNS_SH_ARG_1 = $args[2]
$DUCKDNS_SH_ARG_2 = $args[3]

function cmd_auth {
    if (-not $DUCKDNS_SH_SUBDOMAIN) {
        Write-Output "You must provide a subdomain to authorize."
        Write-Output "For reference, you already have tokens for:"
        fn_list
        return
    }

    if (Test-Path -Path "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env") {
        Write-Output "'$DUCKDNS_SH_SUBDOMAIN.duckdns.org' is already ready to update"
        Write-Output "(to delete: 'rm ~/.config/duckdns.sh/${DUCKDNS_SH_SUBDOMAIN}.env')"
    } else {
        fn_read_token
        Write-Output "Created '~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env'"
    }
}

function cmd_clear {
    fn_require_subdomain
    fn_duckdns_clear
}

function cmd_ip {
    fn_require_subdomain
    fn_require_dig

    Write-Output "dig +short A $DUCKDNS_SH_SUBDOMAIN.duckdns.org"
    $my_domain_ipv4 = (dig +short A "$DUCKDNS_SH_SUBDOMAIN.duckdns.org")
    Write-Output "$DUCKDNS_SH_SUBDOMAIN.duckdns.org A $($my_domain_ipv4 -join ', ')"
    Write-Output ""

    Write-Output "dig +short AAAA $DUCKDNS_SH_SUBDOMAIN.duckdns.org"
    $my_domain_ipv6 = (dig +short AAAA "$DUCKDNS_SH_SUBDOMAIN.duckdns.org")
    Write-Output "$DUCKDNS_SH_SUBDOMAIN.duckdns.org AAAA $($my_domain_ipv6 -join ', ')"
    Write-Output ""
}

function cmd_launcher_install {
    fn_require_subdomain
    fn_require_curl

    if (-not (Test-Path -Path "$Env:UserProfile\.config\envman\PATH.env")) {
        Invoke-Expression ((Invoke-WebRequest -Uri 'https://webi.sh/serviceman').Content)
        . "$Env:UserProfile\.config\envman\PATH.env"
    }

    if (Test-Path -Path "C:\Windows\System32\launchctl.exe") {
        serviceman add --user --name "sh.duckdns.$DUCKDNS_SH_SUBDOMAIN" -- "$DUCKDNS_SH_COMMAND" run $DUCKDNS_SH_SUBDOMAIN
        return
    }

    if (Test-Path -Path "C:\Windows\System32\systemctl.exe") {
        Write-Output ""
        Write-Output "Running command: & 'C:\Windows\System32\serviceman.exe' add --system --path=\"$Env:Path\" --username $Env:UserName --name ""${DUCKDNS_SH_SUBDOMAIN}.duckdns-sh"" -- ""$DUCKDNS_SH_COMMAND"" run $DUCKDNS_SH_SUBDOMAIN"
        Write-Output ""
        & 'C:\Windows\System32\serviceman.exe' add --system --path=$Env:Path --username $Env:UserName --name "${DUCKDNS_SH_SUBDOMAIN}.duckdns-sh" -- "$DUCKDNS_SH_COMMAND" run $DUCKDNS_SH_SUBDOMAIN
        Write-Output ""
        Write-Output "Running command: & 'C:\Windows\System32\systemctl.exe' restart systemd-journald"
        Write-Output ""
        & 'C:\Windows\System32\systemctl.exe' restart systemd-journald
        return
    }

    Write-Output "'launchctl' (macOS) and 'systemd' (Linux) are the only currently supported launchers"
}

function cmd_launcher_uninstall {
    fn_require_subdomain

    if (Test-Path -Path "$Env:UserProfile\Library\LaunchAgents\sh.duckdns.$DUCKDNS_SH_SUBDOMAIN.plist") {
        Write-Output "launchctl unload -w $Env:UserProfile\Library\LaunchAgents\sh.duckdns.$DUCKDNS_SH_SUBDOMAIN.plist"
        & "launchctl unload -w $Env:UserProfile\Library\LaunchAgents\sh.duckdns.$DUCKDNS_SH_SUBDOMAIN.plist"
        Write-Output "Disabled login launcher."
    } elseif (Test-Path -Path "C:\Windows\System32\systemctl.exe") {
        Write-Output "Running command: & 'C:\Windows\System32\systemctl.exe' stop ""${DUCKDNS_SH_SUBDOMAIN}.duckdns-sh"""
        & 'C:\Windows\System32\systemctl.exe' stop "${DUCKDNS_SH_SUBDOMAIN}.duckdns-sh"
        Write-Output "Running command: & 'C:\Windows\System32\systemctl.exe' disable ""${DUCKDNS_SH_SUBDOMAIN}.duckdns-sh"""
        & 'C:\Windows\System32\systemctl.exe' disable "${DUCKDNS_SH_SUBDOMAIN}.duckdns-sh"
        Write-Output "Disabled system launcher."
    } else {
        Write-Output "'launchctl' (macOS) and 'systemd' (Linux) are the only currently supported launchers"
    }
}

function cmd_list {
    Write-Output ""
    fn_list
}

function cmd_myip {
    fn_require_curl

    Write-Output "Invoke-WebRequest -Uri 'https://api.ipify.org?format=text'"
    $my_current_ipv4 = (Invoke-WebRequest -Uri 'https://api.ipify.org?format=text').Content
    Write-Output "IPv4 $($my_current_ipv4)"
    Write-Output ""

    Write-Output "Invoke-WebRequest -Uri 'https://api64.ipify.org?format=text'"
    $my_current_ipv6 = (Invoke-WebRequest -Uri 'https://api64.ipify.org?format=text').Content
    if ($my_current_ipv4 -eq $my_current_ipv6) {
        $my_current_ipv6 = $null
    }
    Write-Output "IPv6 $($my_current_ipv6)"
}

function cmd_run {
    fn_require_subdomain
    fn_require_env
    fn_require_curl
    fn_require_dig

    $my_minutes = 1
    $my_wait = ($my_minutes * 60)
    while ($true) {
        fn_update_ips
        Write-Output ""
        Write-Output ""
        Write-Output "Waiting $my_minutes minute(s) to check '$DUCKDNS_SH_SUBDOMAIN' again..."
        Start-Sleep -Seconds $my_wait
        Write-Output ""
    }
}

function cmd_set {
    fn_require_subdomain
    fn_require_env
    fn_require_curl

    if ($DUCKDNS_SH_ARG_2) {
        fn_duckdns_update $DUCKDNS_SH_ARG_1 $DUCKDNS_SH_ARG_2
    } else {
        switch -wildcard ($DUCKDNS_SH_ARG_1) {
            *:[a-fA-F0-9]* {
                fn_duckdns_update '' $DUCKDNS_SH_ARG_1
            }
            *[0-9].[0-9]* {
                fn_duckdns_update $DUCKDNS_SH_ARG_1 ''
            }
            default {
                Write-Output "'$DUCKDNS_SH_ARG_1' is not a valid IPv4 or IPv6 address"
            }
        }
    }
}

function cmd_update {
    fn_require_subdomain
    fn_require_env
    fn_require_curl
    fn_require_dig
    fn_update_ips
}

function cmd_version {
    $my_year = '2023'
    $my_version = 'v1.0.3'
    $my_date = '2023-01-15 00:49:52 +0000'

    Write-Output "DuckDNS.sh $my_version ($my_date)"
    Write-Output "Copyright $my_year AJ ONeal"
}

function fn_check_env {
    if (-not (Test-Path -Path "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env")) {
        return $false
    }

    . "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env"
    if (-not $Env:DUCKDNS_TOKEN) {
        return $false
    }

    return $true
}

function fn_duckdns_clear {
    . "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env"

    Write-Output "Invoke-WebRequest -Uri 'https://www.duckdns.org/update?domains=$DUCKDNS_SH_SUBDOMAIN&token=****&clear=true'"

    Write-Output "Clearing IP address(es)..."

    Invoke-WebRequest -Uri "https://www.duckdns.org/update?domains=$DUCKDNS_SH_SUBDOMAIN&token=$Env:DUCKDNS_TOKEN&clear=true" -UseBasicParsing
}

function fn_duckdns_update {
    param(
        [string]$my_ipv4,
        [string]$my_ipv6
    )

    $my_ipv4_param = ""
    if ($my_ipv4) {
        $my_ipv4_param = "&ip=$my_ipv4"
    }

    $my_ipv6_param = ""
    if ($my_ipv6) {
        $my_ipv6_param = "&ipv6=$my_ipv6"
    }

    Write-Output "Invoke-WebRequest -Uri 'https://www.duckdns.org/update?domains=$DUCKDNS_SH_SUBDOMAIN&token=****$my_ipv4_param$my_ipv6_param'"

    if ($my_ipv6_param) {
        if ($my_ipv4_param) {
            Write-Output "Updating IPv4 ($my_ipv4) and IPv6 ($my_ipv6)..."
        } else {
            Write-Output "Updating IPv6 ($my_ipv6)..."
        }
    } else {
        if ($my_ipv4_param) {
            Write-Output "Updating IPv4 ($my_ipv4)..."
        } else {
            Write-Output "at least one of ipv4 or ipv6 is required to update"
            return
        }
    }

    . "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env"
    Invoke-WebRequest -Uri "https://www.duckdns.org/update?domains=$DUCKDNS_SH_SUBDOMAIN&token=$Env:DUCKDNS_TOKEN$my_ipv4_param$my_ipv6_param" -UseBasicParsing

    Write-Output ""
}

function fn_list {
    Write-Output "~/.config/duckdns.sh/:"

    if (-not (Test-Path -Path "~/.config/duckdns.sh")) {
        Write-Output "(directory does not exist)"
    }

    Get-ChildItem "~/.config/duckdns.sh" | ForEach-Object {
        $my_domainenv = $_.Name -replace '.env$'

        if ($my_domainenv -eq '*') {
            Write-Output "    (no subdomains have been configured)"
        } else {
            Write-Output "    $my_domainenv"
        }
    }

    Write-Output ""
}

function fn_read_secret {
    param(
        [string]$my_prompt
    )

    $password = Read-Host -Prompt $my_prompt -AsSecureString
    $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
    return $password
}

function fn_read_token {
    $tokenPath = "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env"

    if (-not (Test-Path -Path $tokenPath)) {
        New-Item -Path $tokenPath -ItemType File
    }

    Write-Output "(the token will NOT BE SHOWN - just HIT ENTER after you paste it)"
    $my_token = fn_read_secret "Duck DNS Token>"

    Add-Content -Path $tokenPath -Value "DUCKDNS_TOKEN=$my_token"
}

function fn_require_curl {
    if (-not (Test-Path -Path "C:\Windows\System32\curl.exe")) {
        Write-Output "command 'curl' not found: please install 'curl'"
        throw "Curl not found."
    }
}

function fn_require_dig {
    if (-not (Test-Path -Path "C:\Windows\System32\dig.exe")) {
        Write-Output "command 'dig' not found: please install 'dig' (part of 'dnsutils')"
        throw "Dig not found."
    }
}

function fn_require_env {
    if (-not (Test-Path -Path "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env")) {
        Write-Output "Missing '~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env'"
        throw "Environment file missing."
    }

    . "~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env"
    if (-not $Env:DUCKDNS_TOKEN) {
        Write-Output "Missing 'DUCKDNS_TOKEN=<uuid>' from '~/.config/duckdns.sh/$DUCKDNS_SH_SUBDOMAIN.env'"
        throw "DUCKDNS_TOKEN not found in environment."
    }
}

function fn_require_subdomain {
    if (-not $DUCKDNS_SH_SUBDOMAIN) {
        Write-Output "Missing <subdomain> argument. Try one of these in:"
        fn_list
        throw "Missing subdomain argument."
    }
}

function fn_update_ips {
    Write-Output "Invoke-WebRequest -Uri 'https://api.ipify.org?format=text'"
    $my_domain_ipv4 = (Invoke-WebRequest -Uri 'https://api.ipify.org?format=text').Content
    Write-Output "IPv4 $($my_domain_ipv4)"
    Write-Output ""

    Write-Output "Invoke-WebRequest -Uri 'https://api64.ipify.org?format=text'"
    $my_domain_ipv6 = (Invoke-WebRequest -Uri 'https://api64.ipify.org?format=text').Content
    if ($my_domain_ipv4 -eq $my_domain_ipv6) {
        $my_domain_ipv6 = $null
    }
    Write-Output "IPv6 $($my_domain_ipv6)"
    Write-Output ""

    # if either ip changed to be empty

And then it stopped generating.

Function fn_update_ips {
    Write-Output "dig +short A '${DUCKDNS_SH_SUBDOMAIN}.duckdns.org'"
    $my_domain_ipv4 = (dig +short A "${DUCKDNS_SH_SUBDOMAIN}.duckdns.org")
    Write-Output "${DUCKDNS_SH_SUBDOMAIN}.duckdns.org A ${my_domain_ipv4:-(NONE)}"
    Write-Output ""

    Write-Output "dig +short AAAA '${DUCKDNS_SH_SUBDOMAIN}.duckdns.org'"
    $my_domain_ipv6 = (dig +short AAAA "${DUCKDNS_SH_SUBDOMAIN}.duckdns.org")
    Write-Output "${DUCKDNS_SH_SUBDOMAIN}.duckdns.org AAAA ${my_domain_ipv6:-(NONE)}"
    Write-Output ""

    Write-Output "curl -fsSL 'https://api.ipify.org?format=text'"
    $my_current_ipv4 = (curl --max-time 5.5 -Uri 'https://api.ipify.org?format=text')
    Write-Output "IPv4 ${my_current_ipv4:-(NONE)}"
    Write-Output ""

    Write-Output "curl -fsSL 'https://api64.ipify.org?format=text'"
    $my_current_ipv6 = (curl --max-time 5.5 -Uri 'https://api64.ipify.org?format=text')
    if ($my_current_ipv4 -eq $my_current_ipv6) {
        $my_current_ipv6 = ""
    }
    Write-Output "IPv6 ${my_current_ipv6:-(NONE)}"
    Write-Output ""

    # if either IP changed to be empty, clear both
    if ($my_current_ipv4 -ne $my_domain_ipv4) {
        if (-not $my_current_ipv4) {
            fn_duckdns_clear
            $my_domain_ipv4 = ""
            $my_domain_ipv6 = ""
        }
    }
    if ($my_current_ipv6 -ne $my_domain_ipv6) {
        if (-not $my_current_ipv6) {
            fn_duckdns_clear
            $my_domain_ipv4 = ""
            $my_domain_ipv6 = ""
        }
    }

    # Note: at least one of the IPv4 or IPv6 *must* exist
    # (otherwise we wouldn't even be able to get an empty response)
    if ($my_current_ipv4 -ne $my_domain_ipv4) {
        if ($my_current_ipv6 -ne $my_domain_ipv6) {
            fn_duckdns_update $my_current_ipv4 $my_current_ipv6
        }
        else {
            fn_duckdns_update $my_current_ipv4 ""
        }
    }
    else {
        if ($my_current_ipv6 -ne $my_domain_ipv6) {
            fn_duckdns_update "" $my_current_ipv6
        }
        else {
            Write-Output "No change detected."
        }
    }
}

Function fn_help {
    Write-Output ""
    Write-Output "USAGE"
    Write-Output "    duckdns.sh <subcommand> [arguments...]"
    Write-Output ""
    Write-Output "SUBCOMMANDS"
    Write-Output "    myip                         - show this device's ip(s)"
    Write-Output "    ip <subdomain>               - show subdomain's ip(s)"
    Write-Output ""
    Write-Output "    list                         - show subdomains"
    Write-Output "    auth <subdomain>             - add Duck DNS token"
    Write-Output "    update <subdomain>           - update subdomain to device ip"
    Write-Output "    set <subdomain> <ip> [ipv6]  - set ipv4 and/or ipv6 explicitly"
    Write-Output "    clear <subdomain>            - unset ip(s)"
    Write-Output "    run <subdomain>              - check ip and update every 5m"
    Write-Output "    enable <subdomain>           - enable on boot (Linux) or login (macOS)"
    Write-Output "    disable <subdomain>          - disable on boot or login"
    Write-Output ""
    Write-Output "    help                         - show this menu"
    Write-Output "    version                      - show version and exit"
    Write-Output ""
}

Function main {
    switch ($DUCKDNS_SH_SUBCOMMAND) {
        "myip" { cmd_myip }
        "ip" { cmd_ip }
        "list" { cmd_list }
        "auth" { cmd_auth }
        "update" { cmd_update }
        "set" { cmd_set }
        "clear" { cmd_clear }
        "run" { cmd_run }
        "enable" { cmd_launcher_install }
        "disable" { cmd_launcher_uninstall }
        "help" { fn_help }
        "version" { cmd_version }
        default {
            fn_help
            exit 1
        }
    }
}

main

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions