Как кодировать декодировать url-адрес с использованием power shell

А решение, как оказалось, достаточно простое:

powershell -nologo -noprofile "%{[Net.ServicePointManager]::SecurityProtocol = 'Tls12, Tls11, Tls, Ssl3'} ;(Invoke-WebRequest -UseBasicParsing -Uri <ULR>).StatusCode;exit [int]$Error[0].Exception.Status"

Результат считывается из значение свойства “Status” члена “Exception” в переменной $Error. Это помогает обработать ситуации типа такой

Invoke-WebRequest : Невозможно разрешить удаленное имя: ‘cle.linux.org.tw’
строка:1 знак:77
+ %{[Net.ServicePointManager]::SecurityProtocol = ‘Tls12, Tls11, Tls, Ssl3’} ;Invo …
+ ~
~~~
+ CategoryInfo: InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

Status           Property   System.Net.WebExceptionStatus Status {get;}

, которое и даёт ответ, было соединение или нет.

Ну, вот, как‐то так. Почти полный аналог “curl”.

Время на прочтение

Все привет! В продолжение статьи о возможностях PowerShell, хочу поделиться несложной реализацией создания REST API и простого Web-сервера, используя только PowerShell на базе класса .NET HttpListener. Такая реализация позволяет настроить endpoint-ы (конечные точки) для обработки GET и POST запросов, которые принимают параметры в заголовке запроса, использовать Basic-авторизацию (на основе Base64), обрабатывать любые коды возврата, а так же без лишнего кода, отдавать информацию в разных форматах: json, xml, html, csv. Хочу акцентировать, что данное решение, возможно, не является самым правильным, тем не менее успешно помогло мне покрыть потребности нескольких задач и хотел лишний раз продемонстрировать возможности языка.

Для начала расскажу, кому и зачем данная реализация может понадобиться, далее приведу пример работы с готовым решение и разберем основные моменты для создания своего сервера. Стоит сразу упомянуть, что существует кросс-платформенный Web Framework Pode, и это не единственное решение, успел попробовать как минимум три, но, пожалуй, самое интересное, поддерживаемое и задокументированное. Мне же хотелось иметь свою реализацию, где будут отсутствовать сторонние зависимости, и немного больше понимать, что происходит на стороне сервера во время его работы, в частности, для отладки.

Кому и зачем данная реализация может понадобиться? Приведу свой пример, у меня была задача удаленно реализовать доступ к десктопному приложению, у которого для специфического взаимодействия с ним была возможность выполнять только локальные команды через консоль. Из условий, не было возможности настроить и использовать на машинах WinRM и OpenSSH, ввиду ограничений в компании со стороны ИБ, в то же самое время HTTP был валидным и стандартизированным (после HTTPS) решением. В результате, выполнение и вывод команд получилось автоматизировать, а в дополнение к этому, добавить настройки реестра, чистку temp и логов, что расширило возможности и позволило инженеру DevOps внедрить их в свой Pipeline, используя привычный интерфейс.

Во-вторых, работая системным администратором, в качестве интерфейса для автоматизации задач я использовал WinForms, редко Telegram. Тогда мне очень хотелось попробовать реализовать свой Web-интерфейс для визуализации подобных задач. Важно заметить, что не обладаю сильными познаниями в области систем CI/CD, и конечно, рациональнее использовать, например, интерфейс Jenkins для подобных целей. Тем не менее подобное решение имеет место, т.к. Jenkins все таки не содержит такой кастомизации, как собственный интерфейс.

В-третьих. У меня есть небольшой проект, целью которого является поиск и доставка контента из конкретного torrent-трекера Кинозал до телевизора с Plex (на эту тему у меня есть отдельная статья на Habr). Так сложилось, что за основу я выбрать язык Bash, т.к. планировал запускать бота удаленно и использовать только REST API интерфейс для взаимодействия со всеми сервисами. В течении всего времени эксплуатации, мне не хватало такого функционала, как остановка и повторный запустить torrent-клиента (qBittorrent), или просматривать свободное место на диске, узнать размер конкретных директорий и файлов, а так же возможности их удаления. По аналогии с другими сервисами, мне хотелось использовать единый интерфейс (REST API), в попытках найти готовое решение в виде десктопного приложения для Windows, вспоминая, что уже взаимодействовал с Open Hardware Monitor, используя его как клиент (в режиме HTTP), уже писал модуль для получения метрик через REST API (с возможностью отправки их в InfluxDB и визуализацией в Grafana). Но этого было мало, например для просмотра и удаления файлов можно настроить сервер Everything (который тоже имеет HTTP-сервер). Тут я понял, для покрытия нескольких специфических и не сложных задачи устанавливать дополнительно 2-3 сервиса нерационально, по этому решил написать отдельное решение.

Далее, речь пойдет про WinAPI, решение, с помощью которого у меня получилось покрыть все мои потребности, а конкретно: удаленная остановка и запуск служб и процессов, вывод метрик, максимально приближенных к диспетчеру задач (список физических и логических дисков, показатели IOps, потребление RAM, нагрузка CPU, количество запущенных процессов, потоков, дескрипторов и т.п.), а так же просматривать список директорий и файлов, их размер и количество, с возможностью удаления.

Как это выглядит на практике. Для установки и запуска данного сервиса я попробовал реализовать два решения. Запуск в виде исполняемого файла (используя модуль ps2exe), в таком варианте можно запускается отдельная консоль, где можно наблюдать весь лог и завершать процесс при закрытии консоли. Второй вариант, это запуск фонового процесса, в таком случае для чтения лога используется файл. Но такое решение не самое удачное, т.к. модуль имеет ограничение, которое позволяет запускать любой скрипт только в PowerShell 5.1 и командлеты из версии Core попросту не буду работать.

Второй вариант, это запуск в качестве службы, процесс установки получилось так же автоматизировать, достаточно на любой машине, где есть доступ в интернет запустить этот скрипт. Настроить свои данные для авторизации и задать свой номер порта (предварительно открыть его в firewall) в конфигурационном файле, после чего, начать взаимодействовать на любой системе, используя Invoke-RestMethod или Curl:

Пример удаленной остановки используя PowerShell и запуска службы из Linux (последним запросом я проверил статус службы самого WinAPI).
Пример удаленной остановки используя PowerShell и запуска службы из Linux (последним запросом я проверил статус службы самого WinAPI).
lifailon@hv-devops-01:~$ user="rest"
lifailon@hv-devops-01:~$ pass="api"
lifailon@hv-devops-01:~$ curl -s -X GET -u $user:$pass http://192.168.3.100:8443/api/service/winrm # запрашиваем статус службы WinRM
{
  "Name": "WinRM",
  "DisplayName": "Служба удаленного управления Windows (WS-Management)",
  "Status": "Stopped",
  "StartType": "Automatic"
}
lifailon@hv-devops-01:~$ curl -s -X POST -u $user:$pass --data '' http://192.168.3.100:8443/api/service/winrm -H "Status: Start" # запускаем службу
{
  "Name": "winrm",
  "DisplayName": "Служба удаленного управления Windows (WS-Management)",
  "Status": "Running",
  "StartType": "Automatic"
}

Пример простого Web-сервера был скорее эксперимент, чем необходимость (очень уж хотелось закрыть старый гештальт). Тем не менее выглядит решение так:

Список служб с отображением в формате HTML и возможностью их остановки и запуска.
Список служб с отображением в формате HTML и возможностью их остановки и запуска.

Естественно, для обработки кнопок в браузере не обошлось без JavaScript, особых познаний языка тут не требуется, нашел буквально первый пример в интернете как создать кнопки и обработать их действие при нажатии, ознакомившись с основами HTML-синтаксиса и додумав логику, все получилось сделать достаточно просто. Вот пример с комментариями:

# Типовое условие для проверки вхождения на соответствие метода (GET) и конечной точки (/service)
elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/service") {
    # Получаем массив из списока служб, используя кастомную функцию для вывода с подробной информацией
    $Services = Get-ServiceDescription *
    # Формируем текст HTML-документа, задаем заголовок страницы и открываем тело страницы
    $GetService = "<html><head><title>Service</title></head><body>"
    # Добавляем заготовленные кнопки, которые перенаправляет на другие url
    $GetService += $BodyButtons
    # Указываем на создание таблицы и задаем имена столбцов
    $GetService += "<table border='1'>"
    $GetService += "<tr><th>Name</th><th>Status</th><th>Action</th><th>Start Type</th></tr>"
    # Передаем в цикл список служб и забираем значения
    foreach ($Service in $Services) {
        $name   = "<b>$($Service.Name)</b>"
        $status = $Service.Status
        # Проверяем статус службы, если работает, красим в зеленый цвет
        if ($status -eq "Running") {
            $status = "<font color='green'><b>$status</b></font>"
        } else {
            $status = "<font color='red'><b>$status</b></font>"
        }
        $StartType  = $Service.StartType
        # Заполняем значения столбцов, по анологии с наименованием столбцов (в блоке <tr>)
        $GetService += "<tr><td>$name</td><td>$status</td>"
        # Создаем кпноки, которые при нажатии ссылаются на функции startService и stopService, которые в качестве параметра передают наименование службы
        $GetService += "<td><button onclick='startService(""$($Service.Name)"")'>Start</button> "
        $GetService += "<button onclick='stopService(""$($Service.Name)"")'>Stop</button></td>"
        $GetService += "<td>$StartType</td></tr>"
    }
    $GetService += "</table>"
    $GetService += '
    # Формируем в блоке <script> функции, для обработки нажатия на кнопки
    <script>
        function startService(serviceName) {
            sendServiceAction("Start", serviceName);
        }
        function stopService(serviceName) {
            sendServiceAction("Stop", serviceName);
        }
        # Данная функция принимает действие и отправляет соответствующий POST-запрос, для его обработки другой конечной точкой
        function sendServiceAction(action, serviceName) {
            var request = new XMLHttpRequest();
            request.open("POST", "/api/service/" + serviceName, true);
            # В заголовок запроса передаем статус с содержимым действия (Status: <Stop/Start>) и обновляем страницу (reload)
            request.setRequestHeader("Status", action);
            request.onreadystatechange = function () {
                if (request.readyState === 4 && request.status === 200) {
                    console.log("True");
                    location.reload();
                }
            };
            request.send();
        }
    </script>
    </body></html>
    '
    # Передаем сформированные данные и код ответа в функцию, для отправки ответва клиенту
    Send-Response -Data $GetService -Code 200 -v2
}

Для сравнения интерфейса, приведу пример управления службами, используя простой Jenkins Pipeline. Из явных преимуществ, такой интерфейс универсален для обеих систем (Windows и Linux), логика преимущественно на PowerShell и Bash (в моем случае), а доступ настраивается централизованно через Ansible, где в свою очередь используя ssh и winrm. Такой доступ можно заменить на REST-запросы, при наличии подобного сервера на каждой удаленной машине (например, в виде установленной службы). Безусловно, это более современное и правильное решение, но не взаимозаменяемое, речь только про интерфейс взаимодействия, где мы можем в одном интерфейсе управлять сразу с несколькими машинами.

:/>  Как изменить имя компьютера Windows 10, за минуту
Jenkins Pipeline для запуска и остановки служб
Jenkins Pipeline для запуска и остановки служб

По аналогии со службами, обработал остановку и запуск процессов.

Список процессов с возможностью их завершения и запуска по имени.
Список процессов с возможностью их завершения и запуска по имени.

Из интересного на мой взгляд, написал простую функцию для поиска исполняемого файла в системе, который отвечает за запуск процесса конкретного приложения. Если такой процесс не получается найти, то мы получим в ответ код 400: Bad Request. Process <$ProcessName> could not be found. В таком случае, можно воспользоваться заголовком Path, который принимает путь до исполняемого файла.

function Find-Process {
    param (
        $ProcessName
    )
    $ProcessPath = (Get-ChildItem "C:\Program Files" | Where-Object Name -match $ProcessName).FullName
    if ($null -eq $ProcessPath) {
        $ProcessPath = (Get-ChildItem "C:\Program Files (x86)" | Where-Object Name -match $ProcessName).FullName
    }
    if ($null -eq $ProcessPath) {
        $ProcessPath = (Get-ChildItem "$home\AppData\Roaming" | Where-Object Name -match $ProcessName).FullName
    }
    $ProcessNameExec = "$ProcessName"+".exe"
    (Get-ChildItem $ProcessPath -Recurse | Where-Object Name -eq $ProcessNameExec).FullName
}

> Find-Process qbittorrent
C:\Program Files\qBittorrent\qbittorrent.exe

> Find-Process nmap
C:\Program Files (x86)\Nmap\nmap.exe

Find-Process telegram
C:\Users\lifailon\AppData\Roaming\Telegram Desktop\Telegram.exe

Для сбора метрик используется CIM (Common Information Model). Сам скрипт сервера, описание с примерами, как и набор функций опубликованы на GitHub.

Получение информации о системе через CIM.
Получение информации о системе через CIM.

Так как PowerShell Core является кросс-платформенным решением, class System.Net.HttpListener работает и в системе Linux, используя такую же логику и возможности сразу нескольких языков (например, Bash), можно управлять службами на платформе Windows через systemctl используя REST API.

Что важно, при возникновении ошибки, мне хотелось, что бы данный сервер только логировал ее, но при этом продолжал функционировать (фактически, перезапускался). Для это достаточно вынести слушателя с циклом в отдельную функцию и запускать ее внутри еще одного бесконечного цикла, где присутствует дополнительная обработка ошибок в блоках try-catch-finally.

Вот базовый пример, без лишнего кода с описанием:

# Заполняем переменны с номером порта и данными для авторизации
$port = 8443
$user = "rest"
$pass = "api"
# Формируем строку Base64 из данных логина и пароля
$cred = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("${user}:${pass}"))

# Функция для логирования запросов
function Get-Log {
    ### Debug (Get all Request, Headers and Response parameters):
	# Используя содержимое запросов (Request), чтение передаваемых заголовков и ответа (Response), можно расширить возможности логирования для отладки процесса
    # $context.Request | Out-Default
    # foreach ($header in $context.Request.Headers) {
    #     Write-Host "$header = $($context.Request.Headers[$header])"
    # }
    # $context.Response | Out-Default
	# Забираем содержимое из запроса: адрес клиента, наименование агента, метод и url конечной точки
    $remote_host   = $context.Request.RemoteEndPoint
    $client_agent  = $context.Request.UserAgent
    $method        = $context.Request.HttpMethod
    $endpoint      = $context.Request.RawUrl
    $response_code = $context.Response.StatusCode
    $date = Get-Date -Format "dd.MM.yyyy hh:mm:ss"
	# Выводим в консоль или в файл
    "$date $remote_host $client_agent => $method $endpoint => $response_code"
    # "$date $remote_host $client_agent => $method $endpoint => $response_code" | Out-File $Log_Path -Encoding utf8 -Append
}

# Функция для ответа клиенту
function Send-Response {
    param (
        $Data,
        [int]$Code
    )
    # Проверяем код ответа, если он равен 200 (успех), то конвертируем данные перед отправкой клиенту
    if ($Code -eq 200) {
        # Дополнительно можем проверить название агента на клиентской стороне, который может выступать в роли браузера или явно задан тип данных HTML
        if (($context.Request.UserAgent -match "Chrome") -or ($context.Request.ContentType -match "html")) {
            # Конвертируем полученные данные в HTML и указываем тип контента в ответе
			$Data = $Data | ConvertTo-Html
            $context.Response.ContentType = "text/html; charset=utf-8"
        }
		# Далее проверяем только тип контента из заголовка (если он задан явным образом), и конвертируем вывод в соответствующий тип данных
        elseif ($context.Request.ContentType -match "xml") {
            $Data = ($Data | ConvertTo-Xml).OuterXml
            $context.Response.ContentType = "text/xml; charset=utf-8"
        }
        elseif ($context.Request.ContentType -match "csv") {
            $Data = $Data | ConvertTo-Csv
            $context.Response.ContentType = "text/csv; charset=utf-8"
        }
		# По умолчанию, конвертируем в JSON
        else {
            $Data = $Data | ConvertTo-Json
            $context.Response.ContentType = "text/json; charset=utf-8"
        }
    }
    # Указываем код статуса для ответа
    $context.Response.StatusCode = $Code
    # Преобразуем данные в массив байтов, используя кодировку UTF-8 (особенно важно, при передачи в формате HTML)
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($Data)
    # Выполняем функцию логирования
    Get-Log
    # Забираем число количества байт буффера для записи в поток, который передается в параметр ответа. Это является важным условием, что все даныне были переданы и прочитаны на стороне клиента.
    $context.Response.ContentLength64 = $buffer.Length
    # Передаем массив байтов (наш буффер ответа с данными) в поток ответа, обязательно нужно передать параметры смешения (если бы нужно было начать запись с определенного места в массиве) и длинны буффера
    $context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
	# Данный метод обновляет буфер вывода, убеждаясь, что все данные из буфера отправлены клиенту
    $context.Response.OutputStream.Flush()
	# Закрываем поток ответа
    $context.Response.OutputStream.Close()
}

# Создаем сокет слушателя
Add-Type -AssemblyName System.Net.Http
$http = New-Object System.Net.HttpListener
# Указываем адрес слушателя (+ что бы слушать на всех интерфейсах) и порт
$http.Prefixes.Add("http://+:$port/")
# Указываем использование базового метода аутентификации
$http.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Basic
# Запускаем сокет (начинаем слушать запросы на указанном порту)
$http.Start()
# Обработчик try-finally нужен для закрытия сокета в случае его непредвиденного завершения
try {
    # Отправляем в бесконечный цикл прослушивание входящих запросов, пока свойство IsListening объекта $http равно true
    while ($http.IsListening) {
	    # Используем асинхронный режим, для ожидания новых запросов
        $contextTask = $http.GetContextAsync()
        # Синхронно ожидает завершения асинхронной задачи, чтобы дождаться завершения асинхронной операции, прежде чем продолжить выполнение кода
        while (-not $contextTask.AsyncWaitHandle.WaitOne(200)) { }
		# Получение результата асинхронной задачи
        $context = $contextTask.GetAwaiter().GetResult()
        # Проверяем полученные данные авторизации (в формате Base64) из заголовка запроса на соответветствие переменной $cred
        $CredRequest = $context.Request.Headers["Authorization"]
        # Write-Host $CredRequest
        $CredRequest = $CredRequest -replace "Basic\s"
        if ( $CredRequest -ne $cred ) {
            # Если авторизационные данные не прошли проверку (неверно передан логин или пароль), передаем в функцию ответа параметры с текстом ошибки и кодом возравата 401
            $Data = "Unauthorized (login or password is invalid)"
            Send-Response -Data $Data -Code 401
        }
        else {
            # Если авторизация прошла, проверяем метод и url конечной точки на соответветствие, что бы его обработать
            if ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/service") {
                $GetService = Get-Service -ErrorAction Ignore
                Send-Response -Data $GetService -Code 200
            }
            # Дальше по аналогии дополнительными условиями (elseif) добавляем обработку других конечных точек
            elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/process") {
                $GetService = Get-Process
                Send-Response -Data $GetService -Code 200
            }
	        # Если не одно из методов не прошел соответветствие, отправляем ответ с кодом 405
            elseif ($context.Request.HttpMethod -ne "GET") {
                $Data = "Method not allowed"
                Send-Response -Data $Data -Code 405
            }
            # Если не одно из условий не подошло, отправляем ответ с кодом 404
            else {
                $Data = "Not found endpoint"
                Send-Response -Data $Data -Code 404
            }
        }
    }
}
finally {
    # Освобождаем сокет
    $http.Stop()
}
Пример работы базового REST API сервера с обработкой ошибок.
Пример работы базового REST API сервера с обработкой ошибок.

Итог. Взяв за основу такой скелет и доработав под себя логику, можно автоматизировать процессы конкретного приложения, у которого не предусмотрен внешний интерфейс для подобных задач. В свою очередь, на стороне клиента взаимодействовать с ним, используя любой удобный интерфейс с REST-клиентом.

A very common requirement is that you will get to download a file from a URL in PowerShell. In this PowerShell tutorial, we’ll explore different methods to download files from URLs using PowerShell, providing examples and complete scripts for each method.

To download a file from a URL in PowerShell, you can use the Invoke-WebRequest cmdlet with its -Uri parameter for the file’s URL and -OutFile parameter for the destination path. For instance:

Invoke-WebRequest -Uri "https://powershellfaqs.com/wp-content/uploads/2024/02/TestFile.zip" -OutFile "C:\Bijay\TestFile.zip"

This one-liner is a simple and effective way to download files directly from the internet using PowerShell.

1. Using Invoke-WebRequest

The Invoke-WebRequest cmdlet is the most straightforward method to download files in PowerShell. It’s similar to the wget or curl commands in Unix/Linux systems and is part of the standard PowerShell cmdlets.

Here’s an example of how to use Invoke-WebRequest to download a file using PowerShell:

# Define the URL and the destination path
$url = "https://powershellfaqs.com/wp-content/uploads/2024/02/TestFile.zip"
$destination = "C:\Bijay\TestFile.zip"

# Use Invoke-WebRequest to download the file
Invoke-WebRequest -Uri $url -OutFile $destination

This script sets the URL of the file you want to download and the local path where you want to save it, then uses Invoke-WebRequest to download the file from the URL to the specified destination.

You can see the output in the screenshot below after I executed the PowerShell script using Visual Studio code.

Download File from URL in PowerShell

2. Using Start-BitsTransfer

Another method to download files in PowerShell is using the Start-BitsTransfer cmdlet. This cmdlet is part of the BITS (Background Intelligent Transfer Service), which is more robust and suitable for network transfers. It’s especially useful for large files or when you need more control over the transfer process.

Here’s how you can use Start-BitsTransfer to download a file in PowerShell:

# Define the source URL and the destination path
$source = "https://powershellfaqs.com/largefile.iso"
$destination = "C:\Bijay\largefile.iso"

# Use Start-BitsTransfer to download the file
Start-BitsTransfer -Source $source -Destination $destination

This script works similarly to the previous example, but it uses Start-BitsTransfer for the download, which can handle network interruptions and resume downloads.

:/>  Установка любых Android-приложений на Windows Subsystem for Android, замечания по этому поводу и в целом

3. Using New-Object System.Net.WebClient

For those who prefer a more object-oriented approach, you can use the System.Net.WebClient class to download files in PowerShell. This method provides a high level of control over the download process and allows you to manipulate headers, use proxy servers, and more.

Here’s an example using System.Net.WebClient in PowerShell.

# Create a new WebClient object
$webClient = New-Object System.Net.WebClient

# Define the URL and the destination file
$url = "https://powershellfaqs.com/document.pdf"
$destination = "C:\Bijay\document.pdf"

# Download the file
$webClient.DownloadFile($url, $destination)

This script creates a WebClient object, sets the URL and destination, and then downloads the file with the DownloadFile method.

4. Handling Secure Connections (HTTPS)

When you’re downloading files from secure connections (HTTPS), you might encounter issues with SSL/TLS certificates. To handle this, you can adjust the ServicePointManager settings in PowerShell to bypass the certificate check:

# Ignore SSL/TLS certificate errors (use with caution)
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }

# Now proceed with the download using any of the methods above
# Create a new WebClient object
$webClient = New-Object System.Net.WebClient

# Define the URL and the destination file
$url = "https://powershellfaqs.com/document.pdf"
$destination = "C:\Bijay\document.pdf"

# Download the file
$webClient.DownloadFile($url, $destination)

Remember that bypassing SSL/TLS checks can expose you to security risks, such as man-in-the-middle attacks. Use this approach only when you’re certain of the source’s safety and integrity.

Conclusion

Downloading files from a URL is a common task that PowerShell makes easy. You can use Invoke-WebRequest, Start-BitsTransfer, or System.Net.WebClient in PowerShell to download a file from a URL in PowerShell.

In this PowerShell tutorial, I have explained how to download file from URL in PowerShell using various methods and examples.

You may also like:

Key Takeaways

  • The script automates the creation of URL shortcuts on desktops for individual or multiple users in Windows environments.
  • Flexibility is offered through parameters for creating shortcuts for all users, all existing users, or specific users.
  • Admin rights are required for creating shortcuts in the C:UsersPublicDesktop directory, enhancing security.
  • The script supports customization of shortcut names, URLs, and icons, allowing for a personalized user experience.
  • Use cases include streamlined deployment of shortcuts for company resources on employee workstations.
  • This approach is more efficient than manual shortcut creation, especially in large organizations.

Background

The Script:

<#
.SYNOPSIS
    This script creates a URL desktop shortcut with your specified options. It can create a shortcut for all users (including new ones) or for existing ones only.
.DESCRIPTION
    This script creates a URL desktop shortcut with your specified options. 
    It can create a shortcut for all users (including new ones) or for existing ones only.
.EXAMPLE
    To create a URL shortcut that opens in the default browser:
    
    -Name "Test" -URL "https://www.google.com" -AllUsers

    Creating Shortcut at C:UsersJohnSmithDesktopTest.url

.PARAMETER NAME
    The name of the shortcut, e.g., "Login Portal".

.PARAMETER URL
    The website URL to open, e.g., "https://www.google.com".

.PARAMETER AllExistingUsers
    Creates the shortcut for all existing users but not for new users, e.g., C:Users*Desktopshortcut.url.

.PARAMETER AllUsers
    Creates the shortcut in C:UsersPublicDesktop.

.OUTPUTS
    None
.NOTES
    Minimum OS Architecture Supported: Windows 7, Windows Server 2008
    Release Notes: Split the script into three separate scripts and added Script Variable support.
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
    Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
    Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
    Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
    Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
    Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
    Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
#>

[CmdletBinding()]
param (
    [Parameter()]
    [String]$Name,
    [Parameter()]
    [String]$Url,
    [Parameter()]
    [Switch]$AllExistingUsers,
    [Parameter()]
    [Switch]$AllUsers
)
begin {
    # If Form Variables are used, replace the existing params with them.
    if ($env:shortcutName -and $env:shortcutName -notlike "null") { $Name = $env:shortcutName }
    if ($env:createTheShortcutFor -and $env:createTheShortcutFor -notlike "null") { 
        if ($env:createTheShortcutFor -eq "All Users") { $AllUsers = $True }
        if ($env:createTheShortcutFor -eq "All Existing Users") { $AllExistingUsers = $True }
    }
    if ($env:linkForUrlShortcut -and $env:linkForUrlShortcut -notlike "null") { $Url = $env:linkForUrlShortcut }

    # Double-check that a user was specified for shortcut creation.
    if (!$AllUsers -and !$AllExistingUsers) {
        Write-Error "You must specify which desktop to create the shortcut on!"
        exit 1
    }

    # Double-check that a shortcut name was given.
    if (-not $Name) {
        Write-Error "You must specify a name for the shortcut!"
        exit 1
    }

    # Creating a shortcut at C:UsersPublicDesktop requires admin rights.
    function Test-IsElevated {
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }

    if (!(Test-IsElevated)) {
        Write-Error -Message "Access Denied. Please run with Administrator privileges."
        exit 1
    }

    # This will get all the registry paths for all actual users (not system or network service accounts, but actual users).
    function Get-UserHives {
        param (
            [Parameter()]
            [ValidateSet('AzureAD', 'DomainAndLocal', 'All')]
            [String]$Type = "All",
            [Parameter()]
            [String[]]$ExcludedUsers,
            [Parameter()]
            [switch]$IncludeDefault
        )

        # User account SIDs follow a particular pattern depending on whether they're Azure AD, a Domain account, or a local "workgroup" account.
        $Patterns = switch ($Type) {
            "AzureAD" { "S-1-12-1-(d+-?){4}$" }
            "DomainAndLocal" { "S-1-5-21-(d+-?){4}$" }
            "All" { "S-1-12-1-(d+-?){4}$" ; "S-1-5-21-(d+-?){4}$" } 
        }

        # We'll need the NTuser.dat file to load each user's registry hive. So, we grab it if their account SID matches the above pattern.
        $UserProfiles = Foreach ($Pattern in $Patterns) { 
            Get-ItemProperty "HKLM:SOFTWAREMicrosoftWindows NTCurrentVersionProfileList*" |
                Where-Object { $_.PSChildName -match $Pattern } | 
                Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, 
                @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)NTuser.dat" } }, 
                @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } },
                @{Name = "Path"; Expression = { $_.ProfileImagePath } }
        }

        # There are some situations where grabbing the .Default user's info is needed.
        switch ($IncludeDefault) {
            $True {
                $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path
                $DefaultProfile.UserName = "Default"
                $DefaultProfile.SID = "DefaultProfile"
                $DefaultProfile.Userhive = "$env:SystemDriveUsersDefaultNTUSER.DAT"
                $DefaultProfile.Path = "C:UsersDefault"

                $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName }
            }
        }

        $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName }
    }

    # The actual shortcut creation
    function New-Shortcut {
        [CmdletBinding()]
        param(
            [Parameter()]
            [String]$Arguments,
            [Parameter()]
            [String]$IconPath,
            [Parameter(ValueFromPipeline = $True)]
            [String]$Path,
            [Parameter()]
            [String]$Target,
            [Parameter()]
            [String]$WorkingDir
        )
        process {
            Write-Host "Creating Shortcut at $Path"
            $ShellObject = New-Object -ComObject ("WScript.Shell")
            $Shortcut = $ShellObject.CreateShortcut($Path)
            $Shortcut.TargetPath = $Target
            if ($WorkingDir) { $Shortcut.WorkingDirectory = $WorkingDir }
            if ($Arguments) { $ShortCut.Arguments = $Arguments }
            if ($IconPath) { $Shortcut.IconLocation = $IconPath }
            $Shortcut.Save()

            if (!(Test-Path $Path -ErrorAction SilentlyContinue)) {
                Write-Error "Unable to create Shortcut at $Path"
                exit 1
            }
        }
    }
}
process {
    $ShortcutPath = New-Object System.Collections.Generic.List[String]

    # Creating the filenames for the path
    if ($Url) { 
        $File = "$Name.url"
        $Target = $Url 
    }

    # Building the path's and adding it to the ShortcutPath list
    if ($AllUsers) { $ShortcutPath.Add("$env:PublicDesktop$File") }

    if ($AllExistingUsers) {
        $UserProfiles = Get-UserHives
        # Loop through each user profile
        $UserProfiles | ForEach-Object { $ShortcutPath.Add("$($_.Path)Desktop$File") }
    }

    $ShortcutPath | ForEach-Object { New-Shortcut -Target $Target -Path $_ }

    exit 0
}end {
    
    
    
}

Access 300+ scripts in the NinjaOne Dojo

Detailed Breakdown

The script functions in a PowerShell environment and employs several parameters to customize the shortcut creation process. Here’s a step-by-step breakdown:

  • Parameter Initialization: The script starts by defining parameters such as the shortcut’s name, URL, icon, and the scope of its deployment (either all users, all existing users, or specific users).
  • Environment Variable Check: It checks for specific environment variables, allowing it to adapt based on external inputs or deployment conditions.
  • Prerequisites Validation: Before proceeding, the script verifies if the user has specified the desktop (all users, existing users, or a particular user) and the shortcut name. It also checks for administrative privileges as creating a shortcut in C:UsersPublicDesktop requires such rights.
  • User Hive Extraction: It employs a function to identify user profiles on the system, excluding system or network service accounts.
  • Shortcut Creation Logic: Depending on the chosen parameters, it constructs the appropriate file path and uses a COM object to create the shortcut, setting its target to the specified URL.
  • Execution and Cleanup: After creating the shortcuts, the script exits gracefully, providing the opportunity to pass metadata for logging or audit purposes.
:/>  Почему host process for windows services отключен?

Potential Use Cases

Consider an MSP managing IT infrastructure for a company that recently adopted a new online project management tool. To ensure easy access for all employees, the MSP can use this script to create a desktop shortcut to the tool’s URL on every employee’s computer, saving time and reducing potential confusion or miscommunication about the tool’s web address.

Comparisons

Traditionally, URL shortcuts are either created manually or through group policy settings in a domain environment. Manual creation is labor-intensive and not scalable. Group policies, while powerful, can be complex to configure and are not suited for non-domain or mixed-environment setups. This PowerShell script provides a more direct, flexible, and scriptable approach that can be easily integrated into larger automation workflows.

FAQs

Q1: Will this script work on non-Windows systems? 
A: No, it’s specifically designed for Windows environments.

Q3: Can the script be modified to add custom icons to shortcuts? 
A: Yes, it includes parameters for setting custom icons.

Implications

While the script enhances operational efficiency, it also poses certain security implications. Improper use could lead to the unauthorized distribution of shortcuts, potentially to malicious sites. It’s crucial to ensure that the URLs provided are safe and from trusted sources.

Recommendations

  • Always run the script in a test environment before deployment.
  • Regularly review and update the list of URLs to maintain shortcut relevance and security.
  • Monitor the script’s execution as part of your organization’s broader security audit.

Final Thoughts

In the fast-paced IT environment, efficiency and automation are key. A tool like NinjaOne, offering comprehensive IT management and automation solutions, complements such scripts by providing a platform for deploying, managing, and monitoring such automation tasks across diverse IT infrastructures. The integration of such scripts into a unified management solution like NinjaOne can significantly enhance IT operational efficiency and reliability.

#Parameter
$URL = "https%3A%2F%2Fcrescent.sharepoint.com%2Fsites%2Fmarketing%2F2018%2FDocuments%2FInfo%20Mgmt%20v2%2Epdf"

#Decode URL
[System.Web.HttpUtility]::UrlDecode($URL)

#Output: 
https://crescent.sharepoint.com/sites/marketing/2018/Documents/Info Mgmt v2.pdf
$EncodedURL = "https%3A%2F%2Fcrescent.sharepoint.com%2Fpersonal%2Fsalaudeen_crescent_com%Documents%2FAssets%20%26%20Inventory.xlsx"

[system.uri]::UnescapeDataString($EncodedURL)
#Import PoweShell Module for SharePoint Online
Import-Module Microsoft.Online.SharePoint.PowerShell -DisableNameChecking

#Function to Decode URL
Function Decode-URL([string]$URL)
{
    Try {
        #Decode the URL
        Return [Microsoft.SharePoint.Client.Utilities.HttpUtility]::UrlKeyValueDecode($URL)
    }
    catch {
        Return "Error Getting De-codedURL: $($_.Exception.Message)"
    }
}

#Parameter
$URL = "https%3A%2F%2Fcrescent.sharepoint.com%2Fsites%2Fmarketing%2F2018%2FShared%20Documents%2FInformation%20Management%20v2%2Epdf"

#Call the function to decode URL
Decode-URL $URL 

Encode URL in PowerShell

$URL = "https://crescent.sharepoint.com/personal/salaudeen_crescent_com/Shared Documents/Assets & Inventory.xlsx"
Write-host([URI]::EscapeUriString($URL)) 
encode URL in powershell
$URL = "https://crescent.sharepoint.com/personal/salaudeen_crescent_com/Shared Documents/Assets & Inventory.xlsx"
[System.Web.HttpUtility]::UrlPathEncode($URL)
sharepoint url decode online


Salaudeen Rajack

Как кодировать декодировать url-адрес с использованием power shell

I am receiving an email that contains a link to a website that immediately starts a download of a file. I am able to successfully get the email and the URL, and when I paste the URL into the browser it automatically starts a download. The web page is below:

the web page for the URL

Unfortunately, the file can only be sent in an .xls format, but my end goal is to convert it to a CSV.

I know that Invoke-WebRequest is supposed to do this, and my command for that is:

Invoke-WebRequest -Uri $ExcelLink -OutFile 'C:\Temp\FileName.xls'.

I have tried setting the export to be both .xls and .csv, and it appears I can only get the raw HTML code, instead of the file to download. In the screenshots below the left is exporting as .csv, and the right is .xls:

output from the web requests

I have done a decent amount of research already, and the most helpful link was this Stackoverflow post.

The link from the email does not contain the file name. I stripped out a good amount from the URL, but it looks something like this:

I have tried adding the file name to the end of the URL and for some reason it just redirects to Google.

Does anybody know of a way to get only the file content that automatically starts downloading when entering the URL in a browser?

11 min read

Bright Data favicon

Invoke-Webrequest With a Proxy

At the end of this Invoke-WebRequest PowerShell proxy guide, you will know:

Let’s dive in! 

What Is PowerShell Invoke-WebRequest?

Invoke-WebRequest is a PowerShell cmdlet for sending HTTP, HTTPS, and FTP requests to web servers and web services. By default, it automatically parses the response produced by the server and returns collections of forms, links, images, or other significant HTML elements. 

Usually, it is used for accessing REST APIs, downloading files from the web, or interacting with web services. This is the basic syntax of an Invoke-WebRequest request:

Invoke-WebRequest [-Uri] <Uri> [-Method <WebRequestMethod>] [-Headers <IDictionary>] [-Body <Object>]

The key parameters to remember are:

  • -Uri: The URI of the web resource to which the request is sent.
  • -Method: The HTTP method to use for the request (e.g., GET, POST, PUT, DELETE). Invoke-WebRequest sends GET requests by default.
  • -Headers: The additional HTTP headers to include in the request.
  • -Body: The body of the request to send to the server.

As you can see, the only required argument is <Uri>. Thus, in short, the simplest syntax to perform a GET request to a given URI is:

Invoke-WebRequest <Uri>

This cmdlet was introduced in PowerShell 3.0, in 2012. 

Installing Invoke-WebRequest

To use Invoke-WebRequest, you need PowerShell. So, let’s learn how to install PowerShell and get access to the Invoke-WebRequest cmlet!

Windows

You can verify the current version of PowerShell installed on your Windows machine with this command:

$PSVersionTable

On PowerShell 7.x, that should print something similar to:

PSVersion                      7.4.1

PSEdition                      Core

GitCommitId                    7.4.1

OS                             Microsoft Windows 10.0.22631

Platform                       Win32NT

PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}

PSRemotingProtocolVersion      2.3

SerializationVersion           1.1.0.1

WSManStackVersion              3.0

macOS and Linux

Prerequisites to Get Started with a Proxy in PowerShell

A proxy acts as an intermediary between a client and the destination server. It intercepts your requests, forwards them to the server, receives the responses from the server, and sends them back to you. This way, the destination server will see the requests as coming from the IP and location of the chosen proxy server and not from you.

To get started using a PowerShell proxy with Invoke-WebRequest, you need to understand what a proxy server URL looks like. 

This is the URL of a PowerShell Invoke-WebRequest proxy:

<PROTOCOL>://[<USERNAME>:<PASSWORD>]@<HOST>[:<PORT>]

That consists of:

  • <PROTOCOL>: The protocol to use to connect to the proxy server.
  • <HOST>: The IP address or URL of the proxy server’s hostname.
  • <PORT>: The port number the proxy server listens to.
  • <USERNAME>: The optional username to specify when proxy authentication is required.
  • <PASSWORD>: The optional password to specify when proxy authentication is required.

Invoke-WebRequest : This operation is not supported for a relative URI.

As for proxy protocols, the most popular ones are HTTP, HTTPS, and SOCKS. Invoke-WebRequest in PowerShell 5.1 only supports HTTP while in PowerShell 7.x it also supports HTTPS and SOCKS.

Time to retrieve a valid HTTP proxy!

You can find one for free online, as below:

Protocol: HTTP; IP Address: 190.6.23.219; Port: 999
http://190.6.23.219:999

Warning

Opting for free proxies is okay for learning purposes, but you cannot rely on them in real-world scenarios. Free proxies are unreliable, error-prone, slow, data-greedy, and short-lived. Do not use them!

The solution? Premium proxies from Bright Data, the best provider in the market. Subscribe and try our reliable proxies for free.

http://admin-4521:@rUuH3tJqf45.103.203.109:9571

Оставьте комментарий