PortalPage.ps1

# Set-StrictMode -Version Latest

# From https://net-auth.shanghaitech.edu.cn:19008/material/custom/lang/common-zh.js

$Script:LEGACY_IPADDR = "10.15.145.16"
$Script:LEGACY_NETAUTH = "net-auth.shanghaitech.edu.cn"
$Script:LEGACY_HEADER = [System.Collections.IDictionary]@{Host = ${Script:LEGACY_NETAUTH} }
$Script:LEGACY_VALIDCODE_URL = "https://${Script:LEGACY_IPADDR}:19008/portalauth/verificationcode"
$Script:LEGACY_LOGIN_URL = "https://${Script:LEGACY_IPADDR}:19008/portalauth/login"
$Script:LEGACY_LOGOUT_URL = "https://${Script:LEGACY_IPADDR}:19008/portalauth/logout"
$Script:LEGACY_SYNC_URL = "https://${Script:LEGACY_IPADDR}:19008/portalauth/syncPortalResult"

enum LinkType {
    Ethernet
    Wireless
    Unknown
}

$Script:CachedLang = $null
$Script:CachedL10n = $null

function Get-ShanghaiTechLocalizedString {
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $Key,
        [Parameter()]
        [string]
        $Language = [cultureinfo]::CurrentCulture.Name
    )

    $lang = Join-Path $PSScriptRoot lang
    if ($Script:CachedLang -ne $Language) {
        $file = Join-Path $lang "$Language.json"
        if (-not (Test-Path $file)) { $file = Join-Path $lang "default.json" }
        $Script:CachedL10n = Get-Content $file | ConvertFrom-Json
    }

    $Script:CachedL10n.$Key
}

function Get-ShanghaiTechLinkType {
    [CmdletBinding()]
    [OutputType([LinkType])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$LocalIp
    )

    process {
        # This is a naive implementation, may not work if it-support updates the network topology
        # 10.19 is ethernet, 10.20 is wireless
        if ($LocalIp -like "10.19.*.*") {
            [LinkType]::Ethernet
        }
        elseif ($LocalIp -like "10.20.*.*") {
            [LinkType]::Wireless
        }
        else {
            [LinkType]::Unknown
        }
    }
}

function Get-ShanghaiTechLocalIPAddress {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    # Using cmdlets in NetTCPIP module is more reliable, but need to be cross-platform
    # https://stackoverflow.com/questions/6803073/get-local-ip-address

    try {
        $socket = [System.Net.Sockets.Socket]::new([System.Net.Sockets.AddressFamily]::InterNetwork, [System.Net.Sockets.SocketType]::Dgram, 0)
        $socket.Connect($Script:Config.DnsServer, 53)
        $localIP = $socket.LocalEndPoint.Address.ToString()
    }
    finally {
        if ($socket) {
            $socket.Dispose()
        }
    }

    return $localIP
}

function Read-ShanghaiTechValidCode {
    [CmdletBinding()]
    [OutputType([string])]
    param (
        # User IP Address or URL to get the image
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Type your IP Address",
            ValueFromPipeline = $true)]
        [string]
        $UserIpOrUrl,
        # User MAC Address
        [Parameter(Mandatory = $false)]
        [string]
        $UserMac = "null"
    )
    process {
        $url = Out-ShanghaiTechValidCode -UserIpOrUrl $UserIpOrUrl -UserMac $UserMac -OutFormat url
        $displayUrl = $url.Replace($Script:LEGACY_IPADDR, $Script:LEGACY_NETAUTH)

        do {
            Write-Host "Enter CAPTCHA from $displayUrl"
            Write-Host "or 'i' to get a image, 'q' to show a QR code, 's img.jpg' to save the image"
            $i = Read-Host -Prompt "CAPTCHA"
            switch ($i.Trim().ToLower()) {
                'i' {
                    Out-ShanghaiTechValidCode -UserIpOrUrl $url -OutFormat image | Write-Host
                    $retry = $true
                }
                'q' {
                    Out-ShanghaiTechValidCode -UserIpOrUrl $url -OutFormat qrcode | Write-Host
                    $retry = $true
                }
                { $_ -like 's *' } {
                    Out-ShanghaiTechValidCode $url -OutFile $i.Substring(1).Trim() | Write-Host
                    $retry = $true
                }
                Default {
                    $retry = $i -notmatch "^[0-9a-zA-Z]{4}$"
                }
            }
        } while ($retry)

        $i
    }
}

function Out-ShanghaiTechValidCode {
    param (
        # User IP Address or URL to get the image
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Type your IP Address",
            ValueFromPipeline = $true)]
        [string]
        $UserIpOrUrl,
        # User MAC Address
        [Parameter(Mandatory = $false)]
        [string]
        $UserMac = "null",
        [Parameter()]
        [string]
        $OutFile,
        # Display format
        [Parameter()]
        [ValidateSet("url", "image", "qrcode")]
        $OutFormat = "url"
    )

    process {
        if ([ipaddress]::TryParse($UserIpOrUrl, [ref]0)) {
            $Url = Get-ShanghaiTechValidCode -UserIp $UserIpOrUrl -UserMac $UserMac
        }
        elseif ([uri]::TryCreate($UserIpOrUrl, [urikind]::Absolute, [ref]0)) {
            $Url = $UserIpOrUrl
        }
        else {
            throw "Invalid IP Address or URL: $UserIpOrUrl"
        }

        if ($OutFile) {
            Invoke-RestMethod -Uri $Url -Headers $Script:LEGACY_HEADER -OutFile $OutFile
        }
        switch ($OutFormat) {
            "url" {
                $Url
            }
            "image" {
                $image = (Invoke-WebRequest -Uri $Url -Headers $Script:LEGACY_HEADER).Content
                [System.Drawing.ImageConverter]::new().ConvertFrom($image) | Out-ConsolePicture
            }
            "qrcode" {
                $qrGenerater = [QRCoder.QRCodeGenerator]::new()
                $qrCodeData = $qrGenerater.CreateQrCode($Url, [QRCoder.QRCodeGenerator+ECCLevel]::M)
                $asciiQRCode = [QRCoder.AsciiQRCode]::new($qrCodeData)
                $asciiQRCode.GetGraphic(1)
            }
            Default {
                Write-Error "Invalid OutFormat: $OutFormat"
            }
        }
    }
}

function Get-ShanghaiTechValidCode {
    param (
        # User IP Address
        [Parameter(Mandatory = $true, HelpMessage = "Type your IP Address", ValueFromPipeline = $true)]
        [ValidateScript(
            { [ipaddress]::TryParse($_, [ref]0) }
            # , ErrorMessage = "Invalid IP Address: {0}"
        )]
        [string]
        $UserIp,
        # User MAC Address
        [Parameter(Mandatory = $false)]
        [string]
        $UserMac = "null",
        # Write Output to File. If not specified, output will be written to console.
        [Parameter()]
        [string]
        $OutFile
    )

    process {
        $date = [System.DateTimeOffset]::Now.ToUnixTimeMilliseconds()
        $url = "${Script:LEGACY_VALIDCODE_URL}?date=${date}&uaddress=${UserIp}&umac=${UserMac}"

        if ($OutFile) {
            Invoke-RestMethod -Uri $url -Headers $Script:LEGACY_HEADER -OutFile $OutFile
        }
        else {
            $url
        }
    }
}

function Get-ShanghaiTechLoginSSID {
    param (
        # Link type
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [LinkType]
        $LinkType
    )

    process {
        switch ($LinkType) {
        ([LinkType]::Ethernet) {
                [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('=LswSsidPlaceholder='))
            }
        ([LinkType]::Wireless) {
                "ShanghaiTech"
            }
            Default {
                Write-Warning "Unknown link type $LinkType, assuming Ethernet. If failed, please manually specify the SSID."
                [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('=LswSsidPlaceholder='))
            }
        }
    }
}

# May not correct
enum AuthType {
    VIP = 1
    SMS = 3
    FacebookTwitter = 4
    WeChat = 5
    Passcode = 6
    Twitter = 12
    Google = 13
    QQ = 14
    Weibo = 15
    QRCode = 16
    PublicQRCode = 22
}

#region API

function ParseNetAuthResult {
    [CmdletBinding()]
    param (
        # Payload
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [pscustomobject]
        $Payload,
        # Session
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.PowerShell.Commands.WebRequestSession]
        $Session
    )
    process {
        if (-not $Payload) { return }

        if ($Payload.PSObject.Properties['errorcode']) {
            $errorMessage = Get-ShanghaiTechLocalizedString $Payload.errorcode
            if ($errorMessage) {
                Add-Member -InputObject $Payload -MemberType NoteProperty -Name "errorMessage" -Value $errorMessage
            }
        }

        if ($Payload.PSObject.Properties['psessionid'] -and $Session) {
            $Session.Cookies.Add([System.Net.Cookie]::new("PSESSIONID", $Payload.psessionid, "/", $Script:LEGACY_NETAUTH))
        }

        if ($Payload.PSObject.Properties['token']) {
            $Session.Headers.Add("X-CSRF-TOKEN", $Payload.token)
        }
    }
}

function SetSession {
    [CmdletBinding()]
    [OutputType([Microsoft.PowerShell.Commands.WebRequestSession])]
    param (
        # Session
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Microsoft.PowerShell.Commands.WebRequestSession]
        $Session,
        # Session ID
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $SessionId
    )

    process {
        if (-not $Session) {
            $Session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
        }
        if ($SessionId) {
            $Session.Cookies.Add([System.Net.Cookie]::new("PSESSIONID", $SessionId, "/", $Script:LEGACY_NETAUTH))
        }
        $Session
    }
}

function Invoke-ShanghaiTechLogin {
    [CmdletBinding()]
    # https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/add-credentials-to-powershell-functions

    param (
        # Credential
        [Parameter(Mandatory = $true, HelpMessage = "Type your username and password")]
        [ValidateNotNull()]
        [pscredential]
        [System.Management.Automation.Credential()]
        $Credential,
        # # User IP Address
        # [Parameter(Mandatory = $true, HelpMessage = "Type your IP Address", ValueFromPipeline = $true)]
        # [ValidateScript(
        # { [ipaddress]::TryParse($_, [ref]0) }
        # # , ErrorMessage = "Invalid IP Address: {0}"
        # )]
        # [Alias('uaddress')]
        # [string]
        # $UserIp,
        # # User MAC Address
        # [Parameter(Mandatory = $false)]
        # [ValidateScript(
        # { [regex]::IsMatch($_, "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") }
        # # , ErrorMessage = "Invalid MAC Address: {0}"
        # )]
        # [string]
        # $UserMac = $null,
        # # Link Type
        # [Parameter(Mandatory = $false)]
        # [string]
        # [Alias('ssid')]
        # $SSID,
        # Captcha
        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [AllowNull()]
        $Captcha = $null,
        # Session ID
        [Parameter(HelpMessage = "Type your session ID")]
        [ValidateNotNullOrEmpty()]
        [Alias('psessionid')]
        [string]
        $SessionId,
        # User session
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.PowerShell.Commands.WebRequestSession]
        $Session,
        # Extra parameters
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [hashtable]
        $ExtraParameters = @{}
    )

    dynamicparam {

        $userIpAttribute = [System.Collections.ObjectModel.Collection[System.Attribute]]@(
            $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{
                Mandatory         = -not ($ExtraParameters -and $ExtraParameters.Contains('uaddress'))
                ValueFromPipeline = $true
                HelpMessage       = "Type your IP Address"
            }
            $parameterAttribute
            [System.Management.Automation.ValidateScriptAttribute] {
                [ipaddress]::TryParse($_, [ref]0)
            }
            [System.Management.Automation.AliasAttribute]::new('uaddress')
        )
        $userIpParameter = [System.Management.Automation.RuntimeDefinedParameter]::new('UserIp', [string], $userIpAttribute)

        $userMacAttribute = [System.Collections.ObjectModel.Collection[System.Attribute]]@(
            $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{
                Mandatory   = $true
                HelpMessage = "Type your MAC Address"
            }
            $parameterAttribute
            [System.Management.Automation.AliasAttribute]::new('umac')
        )
        $userMacParameter = [System.Management.Automation.RuntimeDefinedParameter]::new('UserMac', [string], $userMacAttribute)

        $ssidAttribute = [System.Collections.ObjectModel.Collection[System.Attribute]]@(
            $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{
                Mandatory   = $false
                HelpMessage = "Type your SSID"
            }
            $parameterAttribute
        )
        $ssidParameter = [System.Management.Automation.RuntimeDefinedParameter]::new('SSID', [string], $ssidAttribute)

        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        $paramDictionary.Add('UserIp', $userIpParameter)
        $paramDictionary.Add('UserMac', $userMacParameter)
        $paramDictionary.Add('SSID', $ssidParameter)

        return $paramDictionary
    }

    begin {
        $NetworkCredential = $Credential.GetNetworkCredential()
        $UserIp = $PSBoundParameters['UserIp']
        $UserMac = $PSBoundParameters['UserMac']
        $SSID = $PSBoundParameters['SSID']
        $Session = SetSession -Session $Session -SessionId $SessionId
    }

    process {
        # Set PostData

        $PostData = @{
            userName = $NetworkCredential.UserName
            userPass = $NetworkCredential.Password
            authType = [int][AuthType]::VIP
            agreed   = 1
        }

        if ($ExtraParameters -is [hashtable]) {
            foreach ($key in $ExtraParameters.Keys) {
                $PostData[$key] = $ExtraParameters[$key]
            }
        }
        elseif ($ExtraParameters -is [pscustomobject]) {
            foreach ($p in $ExtraParameters.PSObject.Properties) {
                $PostData[$p.Name] = $p.Value
            }
        }
        elseif ($ExtraParameters) {
            Write-Warning "ExtraParameters is not a hashtable or pscustomobject, ignored."
        }

        if ($UserIp) { $PostData.uaddress = $UserIp }
        if ($UserMac) { $PostData.umac = $UserMac }
        # elseif ($null -eq $PostData.Item('umac')) { $PostData.umac = "null" }
        if ($SSID) { $PostData.ssid = $SSID } elseif ($PostData.uaddress) {
            $PostData.ssid = Get-ShanghaiTechLinkType -LocalIp $PostData.uaddress | Get-ShanghaiTechLoginSSID
        }

        $PostData.validCode = $Captcha

        Write-Verbose ($PostData | ConvertTo-Json -Compress)

        # Post request

        $login = Invoke-RestMethod -Uri $Script:LEGACY_LOGIN_URL -Method Post -Headers $Script:LEGACY_HEADER -Body $PostData -SessionVariable $Session
        ParseNetAuthResult -Payload $login -Session $Session

        $login
    }
}
function Invoke-ShanghaiTechLogout {
    [CmdletBinding()]

    param (
        # Session ID
        [Parameter(ValueFromPipelineByPropertyName = $true, HelpMessage = "Type your session ID")]
        [ValidateNotNullOrEmpty()]
        [Alias('psessionid')]
        [string]
        $SessionId,
        # User session
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.PowerShell.Commands.WebRequestSession]
        $Session
    )

    process {
        $Session = SetSession -Session $Session -SessionId $SessionId

        $logout = Invoke-RestMethod -Uri $Script:LEGACY_LOGOUT_URL -Method Post -Headers $Script:LEGACY_HEADER -SessionVariable $Session
        ParseNetAuthResult -Payload $logout -Session $Session

        $logout
    }
}
function Invoke-ShanghaiTechSync {
    [CmdletBinding()]

    param (
        # Session ID
        [Parameter(ValueFromPipelineByPropertyName = $true, HelpMessage = "Type your session ID")]
        [ValidateNotNullOrEmpty()]
        [Alias('psessionid')]
        [string]
        $SessionId,
        # User session
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.PowerShell.Commands.WebRequestSession]
        $Session
    )

    process {
        $Session = SetSession -Session $Session -SessionId $SessionId

        $sync = Invoke-RestMethod -Uri $Script:LEGACY_SYNC_URL -Method Post -Headers $Script:LEGACY_HEADER -SessionVariable $Session
        ParseNetAuthResult -Payload $sync -Session $Session

        $sync
    }
}

#endregion